config

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

ledger-report.el (27055B)


      1 ;;; ledger-report.el --- Helper code for use with the "ledger" command-line tool  -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2003-2016 John Wiegley (johnw AT gnu DOT org)
      4 
      5 ;; This file is not part of GNU Emacs.
      6 
      7 ;; This is free software; you can redistribute it and/or modify it under
      8 ;; the terms of the GNU General Public License as published by the Free
      9 ;; Software Foundation; either version 2, or (at your option) any later
     10 ;; version.
     11 ;;
     12 ;; This is distributed in the hope that it will be useful, but WITHOUT
     13 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     14 ;; FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     15 ;; for more details.
     16 ;;
     17 ;; You should have received a copy of the GNU General Public License
     18 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
     19 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
     20 ;; MA 02110-1301 USA.
     21 
     22 
     23 ;;; Commentary:
     24 ;;  Provide facilities for running and saving reports in Emacs
     25 
     26 ;;; Code:
     27 
     28 (require 'ledger-xact)
     29 (require 'ledger-navigate)
     30 (require 'ledger-commodities)
     31 (require 'ledger-complete)
     32 (declare-function ledger-read-string-with-default "ledger-mode" (prompt default))
     33 (declare-function ledger-read-account-with-prompt "ledger-mode" (prompt))
     34 (declare-function ledger-read-payee-with-prompt "ledger-mode" (prompt))
     35 
     36 (require 'easymenu)
     37 (require 'ansi-color)
     38 (require 'font-lock)
     39 (eval-when-compile
     40   (require 'rx)
     41   (require 'subr-x))
     42 
     43 (defgroup ledger-report nil
     44   "Customization option for the Report buffer."
     45   :group 'ledger)
     46 
     47 (defcustom ledger-reports
     48   '(("bal" "%(binary) -f %(ledger-file) bal")
     49     ("reg" "%(binary) -f %(ledger-file) reg")
     50     ("payee" "%(binary) -f %(ledger-file) reg @%(payee)")
     51     ("account" "%(binary) -f %(ledger-file) reg %(account)"))
     52   "Definition of reports to run.
     53 
     54 Each element has the form (NAME CMDLINE).  The command line can
     55 contain format specifiers that are replaced with context sensitive
     56 information.  Format specifiers have the format '%(<name>)' where
     57 <name> is an identifier for the information to be replaced.  The
     58 `ledger-report-format-specifiers' alist variable contains a mapping
     59 from format specifier identifier to a Lisp function that implements
     60 the substitution.  See the documentation of the individual functions
     61 in that variable for more information on the behavior of each
     62 specifier."
     63   :type '(repeat (list (string :tag "Report Name")
     64                        (string :tag "Command Line")))
     65   :group 'ledger-report)
     66 
     67 (defcustom ledger-report-format-specifiers
     68   '(("ledger-file" . ledger-report-ledger-file-format-specifier)
     69     ("binary" . ledger-report-binary-format-specifier)
     70     ("payee" . ledger-report-payee-format-specifier)
     71     ("account" . ledger-report-account-format-specifier)
     72     ("month" . ledger-report-month-format-specifier)
     73     ("tagname" . ledger-report-tagname-format-specifier)
     74     ("tagvalue" . ledger-report-tagvalue-format-specifier))
     75   "An alist mapping ledger report format specifiers to implementing functions.
     76 
     77 The function is called with no parameters and expected to return
     78 a string, or a list of strings, that should replace the format specifier.
     79 Single strings are quoted with `shell-quote-argument'; lists of strings are
     80 simply concatenated (no quoting)."
     81   :type '(alist :key-type string
     82                 :value-type function)
     83   :group 'ledger-report)
     84 
     85 (defcustom ledger-report-auto-refresh t
     86   "If non-nil, automatically rerun the report when the ledger buffer is saved."
     87   :type 'boolean
     88   :group 'ledger-report)
     89 
     90 (defcustom ledger-report-auto-refresh-sticky-cursor nil
     91   "If non-nil, keep cursor's relative position after auto-refresh."
     92   :type 'boolean
     93   :group 'ledger-report)
     94 
     95 (defcustom ledger-report-links-in-register t
     96   "If non-nil, link entries in \"register\" reports to entries in the ledger buffer."
     97   :type 'boolean
     98   :group 'ledger-report)
     99 
    100 (defcustom ledger-report-use-native-highlighting t
    101   "When non-nil, use ledger's native highlighting in reports."
    102   :type 'boolean
    103   :package-version '(ledger-mode . "4.0.0")
    104   :group 'ledger-report)
    105 
    106 (defcustom ledger-report-native-highlighting-arguments '("--color" "--force-color")
    107   "List of ledger args needed by `ledger-report-use-native-highlighting'.
    108 
    109 If you are using hledger instead of ledger, you might want to set
    110 this variable to `(\"--color=always\")'."
    111   :type '(repeat string)
    112   :group 'ledger-report)
    113 
    114 (defcustom ledger-report-auto-width t
    115   "When non-nil, tell ledger about the width of the report window."
    116   :type 'boolean
    117   :package-version '(ledger-mode . "4.0.0")
    118   :group 'ledger-report)
    119 
    120 (defcustom ledger-report-use-header-line nil
    121   "If non-nil, indicate report name/command in the `header-line'.
    122 The report name/command won't be printed in the buffer.  See
    123 `ledger-report-header-line-fn' for how to customize the
    124 information reported."
    125   :type 'boolean
    126   :package-version '(ledger-mode . "4.0.0")
    127   :group 'ledger-report)
    128 
    129 (defcustom ledger-report-header-line-fn #'ledger-report--header-function
    130   "Evaluate this function in the `header-line' of the report buffer.
    131 `ledger-report-use-header-line' must be non-nil for this to have any effect."
    132   :type 'function
    133   :package-version '(ledger-mode . "4.0.0")
    134   :group 'ledger-report)
    135 
    136 (defcustom ledger-report-resize-window t
    137   "If non-nil, resize the report window.
    138 Calls `shrink-window-if-larger-than-buffer'."
    139   :type 'boolean
    140   :package-version '(ledger-mode . "4.0.0")
    141   :group 'ledger-report)
    142 
    143 (defcustom ledger-report-use-strict nil
    144   "When non-nil, `ledger-mode' will use --strict when running reports?"
    145   :type 'boolean
    146   :package-version '(ledger-mode . "4.0.0")
    147   :group 'ledger-report)
    148 
    149 (defcustom ledger-report-after-report-hook nil
    150   "Hook run after `ledger-report' has created the buffer and report."
    151   :type 'boolean
    152   :package-version '(ledger-mode . "4.0.0")
    153   :group 'ledger-report)
    154 
    155 (defvar ledger-report-buffer-name "*Ledger Report*")
    156 
    157 (defvar-local ledger-report-name nil)
    158 (defvar-local ledger-report-cmd nil)
    159 (defvar-local ledger-report-saved nil)
    160 (defvar-local ledger-report-current-month nil)
    161 (defvar-local ledger-report-is-reversed nil)
    162 (defvar-local ledger-report-cursor-line-number nil)
    163 (defvar-local ledger-report-ledger-buf nil)
    164 (defvar-local ledger-master-file nil
    165   "The master file for the current buffer.
    166 See documentation for the function `ledger-master-file'")
    167 
    168 (defvar ledger-report-name-prompt-history nil)
    169 (defvar ledger-report-cmd-prompt-history nil)
    170 (defvar ledger-minibuffer-history nil)
    171 (defvar ledger-report-mode-abbrev-table)
    172 
    173 (defun ledger-report-reverse-report ()
    174   "Reverse the order of the report."
    175   (interactive)
    176   (ledger-report-reverse-lines)
    177   (setq ledger-report-is-reversed (not ledger-report-is-reversed)))
    178 
    179 (defun ledger-report-reverse-lines ()
    180   "Reverse the lines in the ledger report buffer."
    181   (with-silent-modifications
    182     (goto-char (point-min))
    183     (unless ledger-report-use-header-line
    184       (forward-paragraph)
    185       (forward-line))
    186     (save-excursion
    187       (reverse-region (point) (point-max)))))
    188 
    189 (defun ledger-report-maybe-shrink-window ()
    190   "Shrink window if `ledger-report-resize-window' is non-nil."
    191   (when ledger-report-resize-window
    192     (shrink-window-if-larger-than-buffer)))
    193 
    194 (defvar ledger-report-mode-map
    195   (let ((map (make-sparse-keymap)))
    196     (define-key map (kbd "r") #'ledger-report-redo)
    197     (define-key map (kbd "R") #'ledger-report-reverse-report)
    198     (define-key map (kbd "s") #'ledger-report-save)
    199     (define-key map (kbd "S") #'ledger-report)
    200     (define-key map (kbd "e") #'ledger-report-edit-report)
    201     (define-key map (kbd "E") #'ledger-report-edit-reports)
    202     (define-key map (kbd "q") #'ledger-report-quit)
    203     (define-key map (kbd "C-c C-l C-r") #'ledger-report-redo)
    204     (define-key map (kbd "C-c C-l C-S-s") #'ledger-report-save)
    205     (define-key map (kbd "C-c C-l C-e") #'ledger-report-edit-report)
    206     (define-key map (kbd "C-c C-o C-r") #'ledger-report)
    207     (define-key map (kbd "M-p") #'ledger-report-previous-month)
    208     (define-key map (kbd "M-n") #'ledger-report-next-month)
    209     (define-key map (kbd "$") #'ledger-report-toggle-default-commodity)
    210     map)
    211   "Keymap for `ledger-report-mode'.")
    212 
    213 (easy-menu-define ledger-report-mode-menu ledger-report-mode-map
    214   "Ledger report menu."
    215   '("Reports"
    216     ["Select Report" ledger-report]
    217     ["Save Report" ledger-report-save]
    218     ["Edit Current Report" ledger-report-edit-report]
    219     ["Edit All Reports" ledger-report-edit-reports]
    220     ["Re-run Report" ledger-report-redo]
    221     "---"
    222     ["Reverse report order" ledger-report-reverse-report]
    223     "---"
    224     ["Scroll Up" scroll-up]
    225     ["Visit Source" ledger-report-visit-source]
    226     ["Scroll Down" scroll-down]
    227     "---"
    228     ["Quit" ledger-report-quit]
    229     ))
    230 
    231 (define-derived-mode ledger-report-mode special-mode "Ledger-Report"
    232   "A mode for viewing ledger reports."
    233   (setq-local revert-buffer-function #'ledger-report-redo)
    234   (hack-dir-local-variables-non-file-buffer))
    235 
    236 (defconst ledger-report--extra-args-marker "[[ledger-mode-flags]]")
    237 
    238 (defun ledger-report-binary-format-specifier ()
    239   "Return the path to ledger, plus a marker for extra arguments."
    240   (list (shell-quote-argument ledger-binary-path)
    241         ledger-report--extra-args-marker))
    242 
    243 (defun ledger-report-tagname-format-specifier ()
    244   "Return a valid meta-data tag name."
    245   ;; It is intended completion should be available on existing tag
    246   ;; names, but it remains to be implemented.
    247   (ledger-read-string-with-default "Tag Name" nil))
    248 
    249 (defun ledger-report-tagvalue-format-specifier ()
    250   "Return a valid meta-data tag name."
    251   ;; It is intended completion should be available on existing tag
    252   ;; values, but it remains to be implemented.
    253   (ledger-read-string-with-default "Tag Value" nil))
    254 
    255 (defun ledger-report-read-name ()
    256   "Read the name of a ledger report to use, with completion.
    257 
    258 The empty string and unknown names are allowed."
    259   (completing-read "Report name: "
    260                    ledger-reports nil nil nil
    261                    'ledger-report-name-prompt-history nil))
    262 
    263 (defun ledger-report (report-name edit)
    264   "Run a user-specified report from `ledger-reports'.
    265 
    266 Prompts the user for the REPORT-NAME of the report to run or
    267 EDIT.  If no name is entered, the user will be prompted for a
    268 command line to run.  The command line specified or associated
    269 with the selected report name is run and the output is made
    270 available in another buffer for viewing.  If a prefix argument is
    271 given and the user selects a valid report name, the user is
    272 prompted with the corresponding command line for editing before
    273 the command is run.
    274 
    275 The output buffer will be in `ledger-report-mode', which defines
    276 commands for saving a new named report based on the command line
    277 used to generate the buffer, navigating the buffer, etc."
    278   (interactive
    279    (progn
    280      (when (and (buffer-modified-p)
    281                 (y-or-n-p "Buffer modified, save it? "))
    282        (save-buffer))
    283      (let ((rname (ledger-report-read-name))
    284            (edit (not (null current-prefix-arg))))
    285        (list rname edit))))
    286   (let* ((file (ledger-master-file))
    287          (buf (find-file-noselect file)))
    288     (with-current-buffer
    289         (pop-to-buffer (get-buffer-create ledger-report-buffer-name))
    290       (with-silent-modifications
    291         (erase-buffer)
    292         (ledger-report-mode)
    293         (setq ledger-report-saved nil)
    294         (setq ledger-report-ledger-buf buf)
    295         (setq ledger-report-name report-name)
    296         (setq ledger-report-is-reversed nil)
    297         (setq ledger-report-current-month nil)
    298         (setq ledger-master-file file)
    299         (ledger-do-report (ledger-report-cmd report-name edit)))
    300       (ledger-report-maybe-shrink-window)
    301       (run-hooks 'ledger-report-after-report-hook)
    302       (message (substitute-command-keys (concat "\\[ledger-report-quit] to quit; "
    303                                                 "\\[ledger-report-redo] to redo; "
    304                                                 "\\[ledger-report-edit-report] to edit; "
    305                                                 "\\[ledger-report-save] to save; "
    306                                                 "\\[scroll-up-command] and \\[scroll-down-command] to scroll"))))))
    307 
    308 (defun ledger-report--header-function ()
    309   "Compute the string to be used as the header in the `ledger-report' buffer."
    310   (format "Ledger Report: %s -- Buffer: %s -- Command: %s"
    311           (propertize ledger-report-name 'face 'font-lock-constant-face)
    312           (propertize (buffer-name ledger-report-ledger-buf) 'face 'font-lock-string-face)
    313           (propertize ledger-report-cmd 'face 'font-lock-comment-face)))
    314 
    315 (defun ledger-report-name-exists (name)
    316   "Check to see if the given report NAME exists.
    317 
    318 If exists, returns the object naming the report, otherwise
    319 returns nil."
    320   (unless (string-empty-p name)
    321     (car (assoc name ledger-reports))))
    322 
    323 (defun ledger-reports-add (name cmd)
    324   "Add a new report NAME and CMD to `ledger-reports'."
    325   (setq ledger-reports (cons (list name cmd) ledger-reports)))
    326 
    327 (defun ledger-reports-custom-save ()
    328   "Save the `ledger-reports' variable using the customize framework."
    329   (customize-save-variable 'ledger-reports ledger-reports))
    330 
    331 (defun ledger-report-read-command (report-cmd)
    332   "Read the command line to create a report from REPORT-CMD."
    333   (read-from-minibuffer "Report command line: "
    334                         (if (null report-cmd) "ledger " report-cmd)
    335                         nil nil 'ledger-report-cmd-prompt-history))
    336 
    337 (defun ledger-report-ledger-file-format-specifier ()
    338   "Substitute the full path to master or current ledger file.
    339 
    340 The master file name is determined by the function
    341 `ledger-master-file', which depends on the variable of the same
    342 name.  If it is non-nil, it is used, otherwise the current
    343 buffer's file is used."
    344   (ledger-master-file))
    345 
    346 ;; General helper functions
    347 
    348 (defun ledger-master-file ()
    349   "Return the master file for a ledger file.
    350 
    351 The master file is either the file for the current ledger buffer
    352 or the file specified by the buffer-local variable
    353 `ledger-master-file'.  Typically this variable would be set in a
    354 file local variable comment block at the end of a ledger file
    355 which is included in some other file."
    356   (if ledger-master-file
    357       (expand-file-name ledger-master-file)
    358     (buffer-file-name)))
    359 
    360 (defun ledger-report-payee-format-specifier ()
    361   "Substitute a payee name.
    362 
    363 The user is prompted to enter a payee and that is substituted.
    364 If point is in an xact, the payee for that xact is used as the
    365 default."
    366   (ledger-read-payee-with-prompt "Payee"))
    367 
    368 (defun ledger-report-account-format-specifier ()
    369   "Substitute an account name.
    370 
    371 The user is prompted to enter an account name, which can be any
    372 regular expression identifying an account.  If point is on an
    373 account posting line for an xact, the full account name on that
    374 line is the default."
    375   (ledger-read-account-with-prompt "Account"))
    376 
    377 (defun ledger-report--current-month ()
    378   "Return current month as (YEAR . MONTH-INDEX).
    379 
    380 MONTH-INDEX ranges from 1 (January) to 12 (December) and YEAR is
    381 a number."
    382   (let* ((time-parts (decode-time))
    383          (year (nth 5 time-parts))
    384          (month-index (nth 4 time-parts)))
    385     (cons year month-index)))
    386 
    387 (defun ledger-report--normalize-month (month)
    388   "Return (YEAR . NEW-MONTH) where NEW-MONTH is between 1 and 12.
    389 
    390 MONTH is of the form (YEAR . INDEX) where INDEX is an integer.
    391 The purpose of this method is then to convert any year/month pair
    392 to a meaningful date, e.g., from (2018 . -2) to (2017 . 10)."
    393   (let* ((month-index (cdr month))
    394          (year-shift  (/ (1- month-index) 12)))
    395     (when (<= month-index 0)
    396       (setq year-shift (1- year-shift)))
    397     (cons (+ (car month) year-shift)
    398           (1+ (mod (1- month-index) 12)))))
    399 
    400 (defun ledger-report--shift-month (month shift)
    401   "Return (YEAR . NEW-MONTH) where NEW-MONTH is MONTH+SHIFT.
    402 
    403 MONTH is of the form (YEAR . INDEX) where INDEX ranges from
    404 1 (January) to 12 (December) and YEAR is a number."
    405   (let* ((year (car month))
    406          (new-month (+ (cdr month) shift)))
    407     (ledger-report--normalize-month (cons year new-month))))
    408 
    409 (defun ledger-report-month-format-specifier ()
    410   "Substitute current month."
    411   (with-current-buffer (or ledger-report-buffer-name (current-buffer))
    412     (let* ((month (or ledger-report-current-month (ledger-report--current-month)))
    413            (year (car month))
    414            (month-index (cdr month)))
    415       (format "%s-%s" year month-index))))
    416 
    417 (defun ledger-report-expand-format-specifiers (report-cmd)
    418   "Expand format specifiers in REPORT-CMD.
    419 
    420 Format specifiers are defined in the
    421 `ledger-report-format-specifiers' alist.  The functions are
    422 called in the ledger buffer for which the report is being run."
    423   (let ((ledger-buf ledger-report-ledger-buf))
    424     (with-temp-buffer
    425       (save-excursion (insert report-cmd))
    426       (while (re-search-forward "%(\\([^)]*\\))" nil t)
    427         (when-let ((specifier (match-string 1))
    428                    (f (cdr (assoc specifier ledger-report-format-specifiers))))
    429           (let* ((arg (save-match-data
    430                         (with-current-buffer ledger-buf
    431                           (funcall f))))
    432                  (quoted (save-match-data
    433                            (if (listp arg)
    434                                (string-join arg " ")
    435                              (shell-quote-argument arg)))))
    436             (replace-match quoted 'fixedcase 'literal))))
    437        (buffer-string))))
    438 
    439 (defun ledger-report--cmd-needs-links-p (cmd)
    440   "Check links should be added to the report produced by CMD."
    441   ;; --subtotal reports do not produce identifiable transactions, so
    442   ;; don't prepend location information for them
    443   (and (string-match "\\<reg\\(ister\\)?\\>" cmd)
    444        ledger-report-links-in-register
    445        (not (string-match "--subtotal" cmd))))
    446 
    447 (defun ledger-report--compute-extra-args (report-cmd)
    448   "Compute extra args to add to REPORT-CMD."
    449   `(,@(when (ledger-report--cmd-needs-links-p report-cmd)
    450         '("--prepend-format=%(filename):%(beg_line):"))
    451     ,@(when ledger-report-auto-width
    452         `("--columns" ,(format "%d" (window-max-chars-per-line))))
    453     ,@(when ledger-report-use-native-highlighting
    454         ledger-report-native-highlighting-arguments)
    455     ,@(when ledger-report-use-strict
    456         '("--strict"))))
    457 
    458 (defun ledger-report-cmd (report-name edit)
    459   "Get the command line to run the report name REPORT-NAME.
    460 Optionally EDIT the command."
    461   (let ((report-cmd (car (cdr (assoc report-name ledger-reports)))))
    462     ;; logic for substitution goes here
    463     (when (or (null report-cmd) edit)
    464       (setq report-cmd (ledger-report-read-command report-cmd))
    465       (setq ledger-report-saved nil)) ;; this is a new report, or edited report
    466     (setq report-cmd (ledger-report-expand-format-specifiers report-cmd))
    467     (setq ledger-report-cmd report-cmd)
    468     (or (string-empty-p report-name)
    469         (ledger-report-name-exists report-name)
    470         (progn
    471           (ledger-reports-add report-name report-cmd)
    472           (ledger-reports-custom-save)))
    473     report-cmd))
    474 
    475 (define-button-type 'ledger-report-register-entry
    476   'follow-link t
    477   'face nil ;; Otherwise make-text-button replaces Ledger's native highlighting
    478   'action (lambda (_button) (ledger-report-visit-source)))
    479 
    480 (defun ledger-report--change-month (shift)
    481   "Rebuild report with transactions from current month + SHIFT."
    482   (let* ((current-month (or ledger-report-current-month (ledger-report--current-month)))
    483          (previous-month (ledger-report--shift-month current-month shift)))
    484     (setq ledger-report-current-month previous-month)
    485     (ledger-report-cmd ledger-report-name nil)
    486     (ledger-report-redo)))
    487 
    488 (defun ledger-report--add-links ()
    489   "Replace file and line annotations with buttons."
    490   (while (re-search-forward "^\\(\\(?:/\\|[a-zA-Z]:[\\/]\\)[^:]+\\)?:\\([0-9]+\\)?:" nil t)
    491     (let ((file (match-string 1))
    492           (line (string-to-number (match-string 2))))
    493       (delete-region (match-beginning 0) (match-end 0))
    494       (when (and file line)
    495         (add-text-properties (line-beginning-position) (line-end-position)
    496                              (list 'ledger-source (cons file line)))
    497         (make-text-button
    498          (line-beginning-position) (line-end-position)
    499          'type 'ledger-report-register-entry
    500          'help-echo (format "mouse-2, RET: Visit %s:%d" file line))
    501         ;; Appending the face preserves Ledger's native highlighting
    502         (font-lock-append-text-property (line-beginning-position) (line-end-position)
    503                                         'face 'ledger-font-report-clickable-face)
    504         (end-of-line)))))
    505 
    506 (defun ledger-report--compute-header-line (cmd)
    507   "Call `ledger-report-header-line-fn' with `ledger-report-cmd' bound to CMD."
    508   (let ((ledger-report-cmd cmd))
    509     (funcall ledger-report-header-line-fn)))
    510 
    511 (defun ledger-do-report (cmd)
    512   "Run a report command line CMD.
    513 CMD may contain a (shell-quoted) version of
    514 `ledger-report--extra-args-marker', which will be replaced by
    515 arguments returned by `ledger-report--compute-extra-args'."
    516   (goto-char (point-min))
    517   (let* ((marker ledger-report--extra-args-marker)
    518          (marker-re (concat " *" (regexp-quote marker)))
    519          (args (ledger-report--compute-extra-args cmd))
    520          (args-str (concat " " (mapconcat #'shell-quote-argument args " ")))
    521          (clean-cmd (replace-regexp-in-string marker-re "" cmd t t))
    522          (real-cmd (replace-regexp-in-string marker-re args-str cmd t t)))
    523     (setq header-line-format
    524           (and ledger-report-use-header-line
    525                `(:eval (ledger-report--compute-header-line ,clean-cmd))))
    526     (unless ledger-report-use-header-line
    527       (insert (format "Report: %s\n" ledger-report-name)
    528               (format "Command: %s\n" clean-cmd)
    529               (make-string (- (window-width) 1) ?=)
    530               "\n\n"))
    531     (let* ((report (shell-command-to-string real-cmd)))
    532       (when ledger-report-use-native-highlighting
    533         (setq report (ansi-color-apply report)))
    534       (save-excursion
    535         (insert report))
    536       (when (ledger-report--cmd-needs-links-p cmd)
    537         (save-excursion
    538           (ledger-report--add-links))))))
    539 
    540 (defun ledger-report-visit-source ()
    541   "Visit the transaction under point in the report window."
    542   (interactive)
    543   (let* ((prop (get-text-property (point) 'ledger-source))
    544          (file (car prop))
    545          (line (cdr prop)))
    546     (when (and file line)
    547       (find-file-other-window file)
    548       (widen)
    549       (goto-char (point-min))
    550       (forward-line (1- line))
    551       (ledger-navigate-beginning-of-xact))))
    552 
    553 (defun ledger-report-goto ()
    554   "Goto the ledger report buffer."
    555   (interactive)
    556   (let ((rbuf (get-buffer ledger-report-buffer-name)))
    557     (if (not rbuf)
    558         (error "There is no ledger report buffer"))
    559     (pop-to-buffer rbuf)
    560     (ledger-report-maybe-shrink-window)))
    561 
    562 (defun ledger-report-redo (&optional _ignore-auto _noconfirm)
    563   "Redo the report in the current ledger report buffer.
    564 IGNORE-AUTO and NOCONFIRM are for compatibility with
    565 `revert-buffer-function' and are currently ignored."
    566   (interactive)
    567   (unless (or (derived-mode-p 'ledger-mode)
    568               (derived-mode-p 'ledger-report-mode))
    569     (user-error "Not in a ledger-mode or ledger-report-mode buffer"))
    570   (let ((cur-buf (current-buffer)))
    571     (when (and ledger-report-auto-refresh
    572                (get-buffer ledger-report-buffer-name))
    573       (pop-to-buffer (get-buffer ledger-report-buffer-name))
    574       (ledger-report-maybe-shrink-window)
    575       (setq ledger-report-cursor-line-number (line-number-at-pos))
    576       (with-silent-modifications
    577         (erase-buffer)
    578         (ledger-do-report ledger-report-cmd)
    579         (when ledger-report-is-reversed
    580           (ledger-report-reverse-lines))
    581         (when ledger-report-auto-refresh-sticky-cursor
    582           (forward-line (- ledger-report-cursor-line-number 5))))
    583       (run-hooks 'ledger-report-after-report-hook)
    584       (pop-to-buffer cur-buf))))
    585 
    586 (defun ledger-report-quit ()
    587   "Quit the ledger report buffer and kill its buffer."
    588   (interactive)
    589   (unless (buffer-live-p (get-buffer ledger-report-buffer-name))
    590     (user-error "No ledger report buffer"))
    591   (quit-windows-on ledger-report-buffer-name 'kill))
    592 
    593 (define-obsolete-function-alias 'ledger-report-kill #'ledger-report-quit "2018-03-18")
    594 
    595 (defun ledger-report-edit-reports ()
    596   "Edit the defined ledger reports."
    597   (interactive)
    598   (customize-variable 'ledger-reports))
    599 
    600 (defun ledger-report-edit-report ()
    601   "Edit the current report command in the mini buffer and re-run the report."
    602   (interactive)
    603   (setq ledger-report-cmd (ledger-report-read-command ledger-report-cmd))
    604   (ledger-report-redo))
    605 
    606 (define-obsolete-function-alias 'ledger-report-select-report #'ledger-report "ledger 4.0.0")
    607 
    608 (defun ledger-report-read-new-name ()
    609   "Read the name for a new report from the minibuffer."
    610   (let ((name ""))
    611     (while (string-empty-p name)
    612       (setq name (read-from-minibuffer "Report name: " nil nil nil
    613                                        'ledger-report-name-prompt-history)))
    614     name))
    615 
    616 (defun ledger-report-save ()
    617   "Save the current report command line as a named report."
    618   (interactive)
    619   (ledger-report-goto)
    620   (when (string-empty-p ledger-report-name)
    621     (setq ledger-report-name (ledger-report-read-new-name)))
    622 
    623   (when-let ((existing-name (ledger-report-name-exists ledger-report-name)))
    624     (cond ((y-or-n-p (format "Overwrite existing report named '%s'? "
    625                              ledger-report-name))
    626            (if (string-equal
    627                 ledger-report-cmd
    628                 (car (cdr (assq existing-name ledger-reports))))
    629                (message "Nothing to save. Current command is identical to existing saved one")
    630              (setq ledger-reports
    631                    (assq-delete-all existing-name ledger-reports))
    632              (ledger-reports-add ledger-report-name ledger-report-cmd)
    633              (ledger-reports-custom-save)))
    634           (t
    635            (setq ledger-report-name (ledger-report-read-new-name))
    636            (ledger-reports-add ledger-report-name ledger-report-cmd)
    637            (ledger-reports-custom-save)))))
    638 
    639 (defun ledger-report-previous-month ()
    640   "Rebuild report with transactions from the previous month."
    641   (interactive)
    642   (ledger-report--change-month -1))
    643 
    644 (defun ledger-report-next-month ()
    645   "Rebuild report with transactions from the next month."
    646   (interactive)
    647   (ledger-report--change-month 1))
    648 
    649 (defun ledger-report-toggle-default-commodity ()
    650   "Toggle exchange of reported amounts to `ledger-reconcile-default-commodity'."
    651   (interactive)
    652   (unless (derived-mode-p 'ledger-report-mode)
    653     (user-error "Not a ledger report buffer"))
    654   (save-match-data
    655     (if (string-match
    656          (concat (rx (or "--exchange" "-X") (1+ space))
    657                  (regexp-quote ledger-reconcile-default-commodity))
    658          ledger-report-cmd)
    659         (setq ledger-report-cmd (replace-match "" nil nil ledger-report-cmd))
    660       (setq ledger-report-cmd (concat ledger-report-cmd
    661                                       " --exchange " ledger-reconcile-default-commodity))))
    662   (ledger-report-redo))
    663 
    664 (provide 'ledger-report)
    665 
    666 ;;; ledger-report.el ends here