12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013 |
- ;;; beancount.el --- A major mode to edit Beancount input files. -*- lexical-binding: t -*-
- ;; Copyright (C) 2013 Martin Blais <blais@furius.ca>
- ;; Copyright (C) 2015 Free Software Foundation, Inc.
- ;; Copyright (C) 2019 Daniele Nicolodi <daniele@grinta.net>
- ;; Version: 0
- ;; Author: Martin Blais <blais@furius.ca>
- ;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
- ;; Author: Daniele Nicolodi <daniele@grinta.net>
- ;; This file is not part of GNU Emacs.
- ;; This package is free software: you can redistribute it and/or modify
- ;; it under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation, either version 3 of the License, or
- ;; (at your option) any later version.
- ;; This package is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
- ;; You should have received a copy of the GNU General Public License
- ;; along with this package. If not, see <http://www.gnu.org/licenses/>.
- ;;; Commentary:
- ;; TODO: Add a flymake rule, using bean-check
- ;;; Code:
- (autoload 'ido-completing-read "ido")
- (require 'subr-x)
- (require 'outline)
- (defgroup beancount ()
- "Editing mode for Beancount files."
- :group 'beancount)
- (defcustom beancount-transaction-indent 2
- "Transaction indent."
- :type 'integer)
- (defcustom beancount-number-alignment-column 52
- "Column to which align numbers in postinng definitions. Set to
- 0 to automatically determine the minimum column that will allow
- to align all amounts."
- :type 'integer)
- (defcustom beancount-highlight-transaction-at-point nil
- "If t highlight transaction under point."
- :type 'boolean)
- (defcustom beancount-use-ido t
- "If non-nil, use ido-style completion rather than the standard."
- :type 'boolean)
- (defcustom beancount-electric-currency nil
- "If non-nil, make `newline' try to add missing currency to
- complete the posting at point. The correct currency is determined
- from the open directive for the relevant account."
- :type 'boolean)
- (defgroup beancount-faces nil "Beancount mode highlighting" :group 'beancount)
- (defface beancount-directive
- `((t :inherit font-lock-keyword-face))
- "Face for Beancount directives.")
- (defface beancount-tag
- `((t :inherit font-lock-type-face))
- "Face for Beancount tags.")
- (defface beancount-link
- `((t :inherit font-lock-type-face))
- "Face for Beancount links.")
- (defface beancount-date
- `((t :inherit font-lock-constant-face))
- "Face for Beancount dates.")
- (defface beancount-account
- `((t :inherit font-lock-builtin-face))
- "Face for Beancount account names.")
- (defface beancount-amount
- `((t :inherit font-lock-default-face))
- "Face for Beancount amounts.")
- (defface beancount-narrative
- `((t :inherit font-lock-builtin-face))
- "Face for Beancount transactions narrative.")
- (defface beancount-narrative-cleared
- `((t :inherit font-lock-string-face))
- "Face for Beancount cleared transactions narrative.")
- (defface beancount-narrative-pending
- `((t :inherit font-lock-keyword-face))
- "Face for Beancount pending transactions narrative.")
- (defface beancount-metadata
- `((t :inherit font-lock-type-face))
- "Face for Beancount metadata.")
- (defface beancount-highlight
- `((t :inherit highlight))
- "Face to highlight Beancount transaction at point.")
- (defconst beancount-account-directive-names
- '("balance"
- "close"
- "document"
- "note"
- "open"
- "pad")
- "Directive bames that can appear after a date and are followd by an account.")
- (defconst beancount-no-account-directive-names
- '("commodity"
- "event"
- "price"
- "query"
- "txn")
- "Directive names that can appear after a date and are _not_ followed by an account.")
- (defconst beancount-timestamped-directive-names
- (append beancount-account-directive-names
- beancount-no-account-directive-names)
- "Directive names that can appear after a date.")
- (defconst beancount-directive-names
- '("include"
- "option"
- "plugin"
- "poptag"
- "pushtag")
- "Directive names that can appear at the beginning of a line.")
- (defconst beancount-account-categories
- '("Assets" "Liabilities" "Equity" "Income" "Expenses"))
- (defconst beancount-tag-chars "[:alnum:]-_/.")
- (defconst beancount-account-chars "[:alnum:]-_:")
- (defconst beancount-option-names
- ;; This list is kept in sync with the options defined in
- ;; beancount/parser/options.py.
- '("account_current_conversions"
- "account_current_earnings"
- "account_previous_balances"
- "account_previous_conversions"
- "account_previous_earnings"
- "account_rounding"
- "allow_deprecated_none_for_tags_and_links"
- "allow_pipe_separator"
- "booking_method"
- "conversion_currency"
- "documents"
- "infer_tolerance_from_cost"
- "inferred_tolerance_default"
- "inferred_tolerance_multiplier"
- "insert_pythonpath"
- "long_string_maxlines"
- "name_assets"
- "name_equity"
- "name_expenses"
- "name_income"
- "name_liabilities"
- "operating_currency"
- "plugin_processing_mode"
- "render_commas"
- "title"))
- (defconst beancount-date-regexp "[0-9]\\{4\\}[-/][0-9]\\{2\\}[-/][0-9]\\{2\\}"
- "A regular expression to match dates.")
- (defconst beancount-account-regexp
- (concat (regexp-opt beancount-account-categories)
- "\\(?::[[:upper:]][[:alnum:]-_]+\\)+")
- "A regular expression to match account names.")
- (defconst beancount-number-regexp "[-+]?[0-9]+\\(?:,[0-9]\\{3\\}\\)*\\(?:\\.[0-9]*\\)?"
- "A regular expression to match decimal numbers.")
- (defconst beancount-currency-regexp "[A-Z][A-Z-_'.]*"
- "A regular expression to match currencies.")
- (defconst beancount-flag-regexp
- ;; Single char that is neither a space nor a lower-case letter.
- "[^ a-z]")
- (defconst beancount-transaction-regexp
- (concat "^\\(" beancount-date-regexp "\\) +"
- "\\(?:txn +\\)?"
- "\\(" beancount-flag-regexp "\\) +"
- "\\(\".*\"\\)"))
- (defconst beancount-posting-regexp
- (concat "^\\s-+"
- "\\(" beancount-account-regexp "\\)"
- "\\(?:\\s-+\\(\\(" beancount-number-regexp "\\)"
- "\\s-+\\(" beancount-currency-regexp "\\)\\)\\)?"))
- (defconst beancount-directive-regexp
- (concat "^\\(" (regexp-opt beancount-directive-names) "\\) +"))
- (defconst beancount-timestamped-directive-regexp
- (concat "^\\(" beancount-date-regexp "\\) +"
- "\\(" (regexp-opt beancount-timestamped-directive-names) "\\) +"))
- (defconst beancount-metadata-regexp
- "^\\s-+\\([a-z][A-Za-z0-9_-]+:\\)\\s-+\\(.+\\)")
- (defvar beancount-outline-regexp "\\(;;;+\\|\\*+\\)")
- (defun beancount-outline-level ()
- (let ((len (- (match-end 1) (match-beginning 1))))
- (if (equal (substring (match-string 1) 0 1) ";")
- (- len 2)
- len)))
- (defun beancount-face-by-state (state)
- (cond ((string-equal state "*") 'beancount-narrative-cleared)
- ((string-equal state "!") 'beancount-narrative-pending)
- (t 'beancount-narrative)))
- (defun beancount-outline-face ()
- (if outline-minor-mode
- (cl-case (funcall outline-level)
- (1 'org-level-1)
- (2 'org-level-2)
- (3 'org-level-3)
- (4 'org-level-4)
- (5 'org-level-5)
- (6 'org-level-6)
- (otherwise nil))
- nil))
- (defvar beancount-font-lock-keywords
- `((,beancount-transaction-regexp (1 'beancount-date)
- (2 (beancount-face-by-state (match-string 2)) t)
- (3 (beancount-face-by-state (match-string 2)) t))
- (,beancount-posting-regexp (1 'beancount-account)
- (2 'beancount-amount nil :lax))
- (,beancount-metadata-regexp (1 'beancount-metadata)
- (2 'beancount-metadata t))
- (,beancount-directive-regexp (1 'beancount-directive))
- (,beancount-timestamped-directive-regexp (1 'beancount-date)
- (2 'beancount-directive))
- ;; Fontify section headers when composed with outline-minor-mode.
- (,(concat "^\\(" beancount-outline-regexp "\\).*") . (0 (beancount-outline-face)))
- ;; Tags and links.
- (,(concat "\\#[" beancount-tag-chars "]*") . 'beancount-tag)
- (,(concat "\\^[" beancount-tag-chars "]*") . 'beancount-link)
- ;; Number followed by currency not covered by previous rules.
- (,(concat beancount-number-regexp "\\s-+" beancount-currency-regexp) . 'beancount-amount)
- ;; Accounts not covered by previous rules.
- (,beancount-account-regexp . 'beancount-account)
- ))
- (defun beancount-tab-dwim (&optional arg)
- (interactive "P")
- (if (and outline-minor-mode
- (or arg (outline-on-heading-p)))
- (beancount-outline-cycle arg)
- (indent-for-tab-command)))
- (defvar beancount-mode-map-prefix [(control c)]
- "The prefix key used to bind Beancount commands in Emacs")
- (defvar beancount-mode-map
- (let ((map (make-sparse-keymap))
- (p beancount-mode-map-prefix))
- (define-key map (kbd "TAB") #'beancount-tab-dwim)
- (define-key map (kbd "M-RET") #'beancount-insert-date)
- (define-key map (vconcat p [(\')]) #'beancount-insert-account)
- (define-key map (vconcat p [(control g)]) #'beancount-transaction-clear)
- (define-key map (vconcat p [(l)]) #'beancount-check)
- (define-key map (vconcat p [(q)]) #'beancount-query)
- (define-key map (vconcat p [(x)]) #'beancount-context)
- (define-key map (vconcat p [(k)]) #'beancount-linked)
- (define-key map (vconcat p [(p)]) #'beancount-insert-prices)
- (define-key map (vconcat p [(\;)]) #'beancount-align-to-previous-number)
- (define-key map (vconcat p [(\:)]) #'beancount-align-numbers)
- map))
- (defvar beancount-mode-syntax-table
- (let ((st (make-syntax-table)))
- (modify-syntax-entry ?\" "\"\"" st)
- (modify-syntax-entry ?\; "<" st)
- (modify-syntax-entry ?\n ">" st)
- st))
- ;;;###autoload
- (define-derived-mode beancount-mode fundamental-mode "Beancount"
- "A mode for Beancount files.
- \\{beancount-mode-map}"
- :group 'beancount
- :syntax-table beancount-mode-syntax-table
- (setq-local paragraph-ignore-fill-prefix t)
- (setq-local fill-paragraph-function #'beancount-indent-transaction)
- (setq-local comment-start ";")
- (setq-local comment-start-skip ";+\\s-*")
- (setq-local comment-add 1)
- (setq-local indent-line-function #'beancount-indent-line)
- (setq-local indent-region-function #'beancount-indent-region)
- (setq-local indent-tabs-mode nil)
- (setq-local tab-always-indent 'complete)
- (setq-local completion-ignore-case t)
-
- (add-hook 'completion-at-point-functions #'beancount-completion-at-point nil t)
- (add-hook 'post-command-hook #'beancount-highlight-transaction-at-point nil t)
- (add-hook 'post-self-insert-hook #'beancount--electric-currency nil t)
-
- (setq-local font-lock-defaults '(beancount-font-lock-keywords))
- (setq-local font-lock-syntax-table t)
- (setq-local outline-regexp beancount-outline-regexp)
- (setq-local outline-level #'beancount-outline-level))
- (defun beancount-collect-pushed-tags (begin end)
- "Return list of all pushed (and not popped) tags in the region."
- (goto-char begin)
- (let ((tags (make-hash-table :test 'equal)))
- (while (re-search-forward
- (concat "^\\(push\\|pop\\)tag\\s-+\\(#[" beancount-tag-chars "]+\\)") end t)
- (if (string-equal (match-string 1) "push")
- (puthash (match-string-no-properties 2) nil tags)
- (remhash (match-string-no-properties 2) tags)))
- (hash-table-keys tags)))
- (defun beancount-goto-transaction-begin ()
- "Move the cursor to the first line of the transaction definition."
- (interactive)
- (beginning-of-line)
- ;; everything that is indented with at lest one space or tab is part
- ;; of the transaction definition
- (while (looking-at-p "[ \t]+")
- (forward-line -1))
- (point))
- (defun beancount-goto-transaction-end ()
- "Move the cursor to the line after the transaction definition."
- (interactive)
- (beginning-of-line)
- (if (looking-at-p beancount-transaction-regexp)
- (forward-line))
- ;; everything that is indented with at least one space or tab as part
- ;; of the transaction definition
- (while (looking-at-p "[ \t]+")
- (forward-line))
- (point))
- (defun beancount-goto-next-transaction (&optional arg)
- "Move to the next transaction.
- With an argument move to the next non cleared transaction."
- (interactive "P")
- (beancount-goto-transaction-end)
- (let ((done nil))
- (while (and (not done)
- (re-search-forward beancount-transaction-regexp nil t))
- (if (and arg (string-equal (match-string 2) "*"))
- (goto-char (match-end 0))
- (goto-char (match-beginning 0))
- (setq done t)))
- (if (not done) (goto-char (point-max)))))
- (defun beancount-find-transaction-extents (p)
- (save-excursion
- (goto-char p)
- (list (beancount-goto-transaction-begin)
- (beancount-goto-transaction-end))))
- (defun beancount-inside-transaction-p ()
- (let ((bounds (beancount-find-transaction-extents (point))))
- (> (- (cadr bounds) (car bounds)) 0)))
- (defun beancount-looking-at (regexp n pos)
- (and (looking-at regexp)
- (>= pos (match-beginning n))
- (<= pos (match-end n))))
- (defvar beancount-accounts nil
- "A list of the accounts available in this buffer.")
- (make-variable-buffer-local 'beancount-accounts)
- (defun beancount-completion-at-point ()
- "Return the completion data relevant for the text at point."
- (save-excursion
- (save-match-data
- (let ((pos (point)))
- (beginning-of-line)
- (cond
- ;; non timestamped directive
- ((beancount-looking-at "[a-z]*" 0 pos)
- (list (match-beginning 0) (match-end 0)
- (mapcar (lambda (s) (concat s " ")) beancount-directive-names)))
- ;; poptag
- ((beancount-looking-at
- (concat "poptag\\s-+\\(\\(?:#[" beancount-tag-chars "]*\\)\\)") 1 pos)
- (list (match-beginning 1) (match-end 1)
- (beancount-collect-pushed-tags (point-min) (point))))
- ;; option
- ((beancount-looking-at
- (concat "^option\\s-+\\(\"[a-z_]*\\)") 1 pos)
- (list (match-beginning 1) (match-end 1)
- (mapcar (lambda (s) (concat "\"" s "\" ")) beancount-option-names)))
- ;; timestamped directive
- ((beancount-looking-at
- (concat beancount-date-regexp "\\s-+\\([[:alpha:]]*\\)") 1 pos)
- (list (match-beginning 1) (match-end 1)
- (mapcar (lambda (s) (concat s " ")) beancount-timestamped-directive-names)))
- ;; timestamped directives followed by account
- ((beancount-looking-at
- (concat "^" beancount-date-regexp
- "\\s-+" (regexp-opt beancount-account-directive-names)
- "\\s-+\\([" beancount-account-chars "]*\\)") 1 pos)
- (setq beancount-accounts nil)
- (list (match-beginning 1) (match-end 1) #'beancount-account-completion-table))
- ;; posting
- ((and (beancount-looking-at
- (concat "[ \t]+\\([" beancount-account-chars "]*\\)") 1 pos)
- ;; Do not force the account name to start with a
- ;; capital, so that it is possible to use substring
- ;; completion and we can rely on completion to fix
- ;; capitalization thanks to completion-ignore-case.
- (beancount-inside-transaction-p))
- (setq beancount-accounts nil)
- (list (match-beginning 1) (match-end 1) #'beancount-account-completion-table))
- ;; tags
- ((beancount-looking-at
- (concat "[ \t]+#\\([" beancount-tag-chars "]*\\)") 1 pos)
- (let* ((candidates nil)
- (regexp (concat "\\#\\([" beancount-tag-chars "]+\\)"))
- (completion-table
- (lambda (string pred action)
- (if (null candidates)
- (setq candidates
- (sort (beancount-collect regexp 1) #'string<)))
- (complete-with-action action candidates string pred))))
- (list (match-beginning 1) (match-end 1) completion-table)))
- ;; links
- ((beancount-looking-at
- (concat "[ \t]+\\^\\([" beancount-tag-chars "]*\\)") 1 pos)
- (let* ((candidates nil)
- (regexp (concat "\\^\\([" beancount-tag-chars "]+\\)"))
- (completion-table
- (lambda (string pred action)
- (if (null candidates)
- (setq candidates
- (sort (beancount-collect regexp 1) #'string<)))
- (complete-with-action action candidates string pred))))
- (list (match-beginning 1) (match-end 1) completion-table))))))))
- (defun beancount-collect (regexp n)
- "Return an unique list of REGEXP group N in the current buffer."
- (let ((pos (point)))
- (save-excursion
- (save-match-data
- (let ((hash (make-hash-table :test 'equal)))
- (goto-char (point-min))
- (while (re-search-forward regexp nil t)
- ;; Ignore matches around `pos' (the point position when
- ;; entering this funcyion) since that's presumably what
- ;; we're currently trying to complete.
- (unless (<= (match-beginning 0) pos (match-end 0))
- (puthash (match-string-no-properties n) nil hash)))
- (hash-table-keys hash))))))
- (defun beancount-account-completion-table (string pred action)
- (if (eq action 'metadata) '(metadata (category . beancount-account))
- (if (null beancount-accounts)
- (setq beancount-accounts
- (sort (beancount-collect beancount-account-regexp 0) #'string<)))
- (complete-with-action action beancount-accounts string pred)))
- ;; Default to substring completion for beancount accounts.
- (defconst beancount--completion-overrides
- '(beancount-account (styles basic partial-completion substring)))
- (add-to-list 'completion-category-defaults beancount--completion-overrides)
- (defun beancount-number-alignment-column ()
- "Return the column to which postings amounts should be aligned to.
- Returns `beancount-number-alignment-column' unless it is 0. In
- that case, scan the buffer to determine the minimum column that
- will allow to align all numbers."
- (if (> beancount-number-alignment-column 0)
- beancount-number-alignment-column
- (save-excursion
- (save-match-data
- (let ((account-width 0)
- (number-width 0))
- (goto-char (point-min))
- (while (re-search-forward beancount-posting-regexp nil t)
- (if (match-string 2)
- (let ((accw (- (match-end 1) (line-beginning-position)))
- (numw (- (match-end 3) (match-beginning 3))))
- (setq account-width (max account-width accw)
- number-width (max number-width numw)))))
- (+ account-width 2 number-width))))))
- (defun beancount-compute-indentation ()
- "Return the column to which the current line should be indented."
- (save-excursion
- (beginning-of-line)
- (cond
- ;; Only timestamped directives start with a digit.
- ((looking-at-p "[0-9]") 0)
- ;; Otherwise look at the previous line.
- ((and (= (forward-line -1) 0)
- (or (looking-at-p "[ \t].+")
- (looking-at-p beancount-timestamped-directive-regexp)
- (looking-at-p beancount-transaction-regexp)))
- beancount-transaction-indent)
- ;; Default.
- (t 0))))
- (defun beancount-align-number (target-column)
- (save-excursion
- (beginning-of-line)
- ;; Check if the current line is a posting with a number to align.
- (when (and (looking-at beancount-posting-regexp)
- (match-string 2))
- (let* ((account-end-column (- (match-end 1) (line-beginning-position)))
- (number-width (- (match-end 3) (match-beginning 3)))
- (account-end (match-end 1))
- (number-beginning (match-beginning 3))
- (spaces (max 2 (- target-column account-end-column number-width))))
- (unless (eq spaces (- number-beginning account-end))
- (goto-char account-end)
- (delete-region account-end number-beginning)
- (insert (make-string spaces ? )))))))
- (defun beancount-indent-line ()
- (let ((indent (beancount-compute-indentation))
- (savep (> (current-column) (current-indentation))))
- (unless (eq indent (current-indentation))
- (if savep (save-excursion (indent-line-to indent))
- (indent-line-to indent)))
- (unless (eq this-command 'beancount-tab-dwim)
- (beancount-align-number (beancount-number-alignment-column)))))
- (defun beancount-indent-region (start end)
- "Indent a region automagically. START and END specify the region to indent."
- (let ((deactivate-mark nil)
- (beancount-number-alignment-column (beancount-number-alignment-column)))
- (save-excursion
- (setq end (copy-marker end))
- (goto-char start)
- (or (bolp) (forward-line 1))
- (while (< (point) end)
- (unless (looking-at-p "\\s-*$")
- (beancount-indent-line))
- (forward-line 1))
- (move-marker end nil))))
- (defun beancount-indent-transaction (&optional _justify _region)
- "Indent Beancount transaction at point."
- (interactive)
- (save-excursion
- (let ((bounds (beancount-find-transaction-extents (point))))
- (beancount-indent-region (car bounds) (cadr bounds)))))
- (defun beancount-transaction-clear (&optional arg)
- "Clear transaction at point. With a prefix argument set the
- transaction as pending."
- (interactive "P")
- (save-excursion
- (save-match-data
- (let ((flag (if arg "!" "*")))
- (beancount-goto-transaction-begin)
- (if (looking-at beancount-transaction-regexp)
- (replace-match flag t t nil 2))))))
- (defun beancount-insert-account (account-name)
- "Insert one of the valid account names in this file.
- Uses ido niceness according to `beancount-use-ido'."
- (interactive
- (list
- (if beancount-use-ido
- ;; `ido-completing-read' does not understand functional
- ;; completion tables thus directly build a list of the
- ;; accounts in the buffer
- (let ((beancount-accounts
- (sort (beancount-collect beancount-account-regexp 0) #'string<)))
- (ido-completing-read "Account: " beancount-accounts
- nil nil (thing-at-point 'word)))
- (completing-read "Account: " #'beancount-account-completion-table
- nil t (thing-at-point 'word)))))
- (let ((bounds (bounds-of-thing-at-point 'word)))
- (when bounds
- (delete-region (car bounds) (cdr bounds))))
- (insert account-name))
- (defmacro beancount-for-line-in-region (begin end &rest exprs)
- "Iterate over each line in region until an empty line is encountered."
- `(save-excursion
- (let ((end-marker (copy-marker ,end)))
- (goto-char ,begin)
- (beginning-of-line)
- (while (and (not (eobp)) (< (point) end-marker))
- (beginning-of-line)
- (progn ,@exprs)
- (forward-line 1)
- ))))
- (defun beancount-align-numbers (begin end &optional requested-currency-column)
- "Align all numbers in the given region. CURRENCY-COLUMN is the character
- at which to align the beginning of the amount's currency. If not specified, use
- the smallest columns that will align all the numbers. With a prefix argument,
- align with the fill-column."
- (interactive "r")
- ;; With a prefix argument, align with the fill-column.
- (when current-prefix-arg
- (setq requested-currency-column fill-column))
- ;; Loop once in the region to find the length of the longest string before the
- ;; number.
- (let (prefix-widths
- number-widths
- (number-padding " "))
- (beancount-for-line-in-region
- begin end
- (let ((line (thing-at-point 'line)))
- (when (string-match (concat "\\(.*?\\)"
- "[ \t]+"
- "\\(" beancount-number-regexp "\\)"
- "[ \t]+"
- beancount-currency-regexp)
- line)
- (push (length (match-string 1 line)) prefix-widths)
- (push (length (match-string 2 line)) number-widths)
- )))
- (when prefix-widths
- ;; Loop again to make the adjustments to the numbers.
- (let* ((number-width (apply 'max number-widths))
- (number-format (format "%%%ss" number-width))
- ;; Compute rightmost column of prefix.
- (max-prefix-width (apply 'max prefix-widths))
- (max-prefix-width
- (if requested-currency-column
- (max (- requested-currency-column (length number-padding) number-width 1)
- max-prefix-width)
- max-prefix-width))
- (prefix-format (format "%%-%ss" max-prefix-width))
- )
- (beancount-for-line-in-region
- begin end
- (let ((line (thing-at-point 'line)))
- (when (string-match (concat "^\\([^\"]*?\\)"
- "[ \t]+"
- "\\(" beancount-number-regexp "\\)"
- "[ \t]+"
- "\\(.*\\)$")
- line)
- (delete-region (line-beginning-position) (line-end-position))
- (let* ((prefix (match-string 1 line))
- (number (match-string 2 line))
- (rest (match-string 3 line)) )
- (insert (format prefix-format prefix))
- (insert number-padding)
- (insert (format number-format number))
- (insert " ")
- (insert rest)))))))))
- (defun beancount-align-to-previous-number ()
- "Align postings under the point's paragraph.
- This function looks for a posting in the previous transaction to
- determine the column at which to align the transaction, or otherwise
- the fill column, and align all the postings of this transaction to
- this column."
- (interactive)
- (let* ((begin (save-excursion
- (beancount-beginning-of-directive)
- (point)))
- (end (save-excursion
- (goto-char begin)
- (forward-paragraph 1)
- (point)))
- (currency-column (or (beancount-find-previous-alignment-column)
- fill-column)))
- (beancount-align-numbers begin end currency-column)))
- (defun beancount-beginning-of-directive ()
- "Move point to the beginning of the enclosed or preceding directive."
- (beginning-of-line)
- (while (and (> (point) (point-min))
- (not (looking-at
- "[0-9][0-9][0-9][0-9][\-/][0-9][0-9][\-/][0-9][0-9]")))
- (forward-line -1)))
- (defun beancount-find-previous-alignment-column ()
- "Find the preceding column to align amounts with.
- This is used to align transactions at the same column as that of
- the previous transaction in the file. This function merely finds
- what that column is and returns it (an integer)."
- ;; Go hunting for the last column with a suitable posting.
- (let (column)
- (save-excursion
- ;; Go to the beginning of the enclosing directive.
- (beancount-beginning-of-directive)
- (forward-line -1)
- ;; Find the last posting with an amount and a currency on it.
- (let ((posting-regexp (concat
- "\\s-+"
- beancount-account-regexp "\\s-+"
- beancount-number-regexp "\\s-+"
- "\\(" beancount-currency-regexp "\\)"))
- (balance-regexp (concat
- beancount-date-regexp "\\s-+"
- "balance" "\\s-+"
- beancount-account-regexp "\\s-+"
- beancount-number-regexp "\\s-+"
- "\\(" beancount-currency-regexp "\\)")))
- (while (and (> (point) (point-min))
- (not (or (looking-at posting-regexp)
- (looking-at balance-regexp))))
- (forward-line -1))
- (when (or (looking-at posting-regexp)
- (looking-at balance-regexp))
- (setq column (- (match-beginning 1) (point))))
- ))
- column))
- (defun beancount--account-currency (account)
- ;; Build a regexp that matches an open directive that specifies a
- ;; single account currencydaaee. The currency is match group 1.
- (let ((re (concat "^" beancount-date-regexp " +open"
- "\\s-+" (regexp-quote account)
- "\\s-+\\(" beancount-currency-regexp "\\)\\s-+")))
- (save-excursion
- (goto-char (point-min))
- (when (re-search-forward re nil t)
- ;; The account has declared a single currency, so we can fill it in.
- (match-string-no-properties 1)))))
- (defun beancount--electric-currency ()
- (when (and beancount-electric-currency (eq last-command-event ?\n))
- (save-excursion
- (forward-line -1)
- (when (and (beancount-inside-transaction-p)
- (looking-at (concat "\\s-+\\(" beancount-account-regexp "\\)"
- "\\s-+\\(" beancount-number-regexp "\\)\\s-*$")))
- ;; Last line is a posting without currency.
- (let* ((account (match-string 1))
- (pos (match-end 0))
- (currency (beancount--account-currency account)))
- (when currency
- (save-excursion
- (goto-char pos)
- (insert " " currency))))))))
- (defun beancount-insert-date ()
- "Start a new timestamped directive."
- (interactive)
- (unless (bolp) (newline))
- (insert (format-time-string "%Y-%m-%d") " "))
- (defvar beancount-install-dir nil
- "Directory in which Beancount's source is located.
- Only useful if you have not installed Beancount properly in your PATH.")
- (defvar beancount-check-program "bean-check"
- "Program to run to run just the parser and validator on an
- input file.")
- (defvar compilation-read-command)
- (defun beancount--run (prog &rest args)
- (let ((process-environment
- (if beancount-install-dir
- `(,(concat "PYTHONPATH=" beancount-install-dir)
- ,(concat "PATH="
- (expand-file-name "bin" beancount-install-dir)
- ":"
- (getenv "PATH"))
- ,@process-environment)
- process-environment))
- (compile-command (mapconcat (lambda (arg)
- (if (stringp arg)
- (shell-quote-argument arg) ""))
- (cons prog args)
- " ")))
- (call-interactively 'compile)))
- (defun beancount-check ()
- "Run `beancount-check-program'."
- (interactive)
- (let ((compilation-read-command nil))
- (beancount--run beancount-check-program
- (file-relative-name buffer-file-name))))
- (defvar beancount-query-program "bean-query"
- "Program to run to run just the parser and validator on an
- input file.")
- (defun beancount-query ()
- "Run bean-query."
- (interactive)
- ;; Don't let-bind compilation-read-command this time, since the default
- ;; command is incomplete.
- (beancount--run beancount-query-program
- (file-relative-name buffer-file-name) t))
- (defvar beancount-doctor-program "bean-doctor"
- "Program to run the doctor commands.")
- (defun beancount-context ()
- "Get the \"context\" from `beancount-doctor-program'."
- (interactive)
- (let ((compilation-read-command nil))
- (beancount--run beancount-doctor-program "context"
- (file-relative-name buffer-file-name)
- (number-to-string (line-number-at-pos)))))
- (defun beancount-linked ()
- "Get the \"linked\" info from `beancount-doctor-program'."
- (interactive)
- (let ((compilation-read-command nil))
- (beancount--run beancount-doctor-program "linked"
- (file-relative-name buffer-file-name)
- (number-to-string (line-number-at-pos)))))
- (defvar beancount-price-program "bean-price"
- "Program to run the price fetching commands.")
- (defun beancount-insert-prices ()
- "Run bean-price on the current file and insert the output inline."
- (interactive)
- (call-process beancount-price-program nil t nil
- (file-relative-name buffer-file-name)))
- ;;; Transaction highligh
- (defvar beancount-highlight-overlay (list))
- (make-variable-buffer-local 'beancount-highlight-overlay)
- (defun beancount-highlight-overlay-make ()
- (let ((overlay (make-overlay 1 1)))
- (overlay-put overlay 'face 'beancount-highlight)
- (overlay-put overlay 'priority '(nil . 99))
- overlay))
- (defun beancount-highlight-transaction-at-point ()
- "Move the highlight overlay to the current transaction."
- (when beancount-highlight-transaction-at-point
- (unless beancount-highlight-overlay
- (setq beancount-highlight-overlay (beancount-highlight-overlay-make)))
- (let* ((bounds (beancount-find-transaction-extents (point)))
- (begin (car bounds))
- (end (cadr bounds)))
- (if (> (- end begin) 0)
- (move-overlay beancount-highlight-overlay begin end)
- (move-overlay beancount-highlight-overlay 1 1)))))
- ;;; Outline minor mode support.
- (defun beancount-outline-cycle (&optional arg)
- "Implement visibility cycling a la `org-mode'.
- The behavior of this command is determined by the first matching
- condition among the following:
- 1. When point is at the beginning of the buffer, or when called
- with a `\\[universal-argument]' universal argument, rotate the entire buffer
- through 3 states:
- - OVERVIEW: Show only top-level headlines.
- - CONTENTS: Show all headlines of all levels, but no body text.
- - SHOW ALL: Show everything.
- 2. When point is at the beginning of a headline, rotate the
- subtree starting at this line through 3 different states:
- - FOLDED: Only the main headline is shown.
- - CHILDREN: The main headline and its direct children are shown.
- From this state, you can move to one of the children
- and zoom in further.
- - SUBTREE: Show the entire subtree, including body text."
- (interactive "P")
- (setq deactivate-mark t)
- (cond
- ;; Beginning of buffer or called with C-u: Global cycling
- ((or (equal arg '(4))
- (and (bobp)
- ;; org-mode style behaviour - only cycle if not on a heading
- (not (outline-on-heading-p))))
- (beancount-cycle-buffer))
- ;; At a heading: rotate between three different views
- ((save-excursion (beginning-of-line 1) (looking-at outline-regexp))
- (outline-back-to-heading)
- (let ((goal-column 0) eoh eol eos)
- ;; First, some boundaries
- (save-excursion
- (save-excursion (beancount-next-line) (setq eol (point)))
- (outline-end-of-heading) (setq eoh (point))
- (outline-end-of-subtree) (setq eos (point)))
- ;; Find out what to do next and set `this-command'
- (cond
- ((= eos eoh)
- ;; Nothing is hidden behind this heading
- (beancount-message "EMPTY ENTRY"))
- ((>= eol eos)
- ;; Entire subtree is hidden in one line: open it
- (outline-show-entry)
- (outline-show-children)
- (beancount-message "CHILDREN")
- (setq
- this-command 'beancount-cycle-children))
- ((eq last-command 'beancount-cycle-children)
- ;; We just showed the children, now show everything.
- (outline-show-subtree)
- (beancount-message "SUBTREE"))
- (t
- ;; Default action: hide the subtree.
- (outline-hide-subtree)
- (beancount-message "FOLDED")))))))
- (defvar beancount-current-buffer-visibility-state nil
- "Current visibility state of buffer.")
- (make-variable-buffer-local 'beancount-current-buffer-visibility-state)
- (defvar beancount-current-buffer-visibility-state)
- (defun beancount-cycle-buffer (&optional arg)
- "Rotate the visibility state of the buffer through 3 states:
- - OVERVIEW: Show only top-level headlines.
- - CONTENTS: Show all headlines of all levels, but no body text.
- - SHOW ALL: Show everything.
- With a numeric prefix ARG, show all headlines up to that level."
- (interactive "P")
- (save-excursion
- (cond
- ((integerp arg)
- (outline-show-all)
- (outline-hide-sublevels arg))
- ((eq last-command 'beancount-cycle-overview)
- ;; We just created the overview - now do table of contents
- ;; This can be slow in very large buffers, so indicate action
- ;; Visit all headings and show their offspring
- (goto-char (point-max))
- (while (not (bobp))
- (condition-case nil
- (progn
- (outline-previous-visible-heading 1)
- (outline-show-branches))
- (error (goto-char (point-min)))))
- (beancount-message "CONTENTS")
- (setq this-command 'beancount-cycle-toc
- beancount-current-buffer-visibility-state 'contents))
- ((eq last-command 'beancount-cycle-toc)
- ;; We just showed the table of contents - now show everything
- (outline-show-all)
- (beancount-message "SHOW ALL")
- (setq this-command 'beancount-cycle-showall
- beancount-current-buffer-visibility-state 'all))
- (t
- ;; Default action: go to overview
- (let ((toplevel
- (cond
- (current-prefix-arg
- (prefix-numeric-value current-prefix-arg))
- ((save-excursion
- (beginning-of-line)
- (looking-at outline-regexp))
- (max 1 (funcall outline-level)))
- (t 1))))
- (outline-hide-sublevels toplevel))
- (beancount-message "OVERVIEW")
- (setq this-command 'beancount-cycle-overview
- beancount-current-buffer-visibility-state 'overview)))))
- (defun beancount-message (msg)
- "Display MSG, but avoid logging it in the *Messages* buffer."
- (let ((message-log-max nil))
- (message msg)))
- (defun beancount-next-line ()
- "Forward line, but mover over invisible line ends.
- Essentially a much simplified version of `next-line'."
- (interactive)
- (beginning-of-line 2)
- (while (and (not (eobp))
- (get-char-property (1- (point)) 'invisible))
- (beginning-of-line 2)))
- (provide 'beancount)
- ;;; beancount.el ends here
|