diff options
author | Raúl Benencia <rul@kalgan.cc> | 2019-06-23 11:43:30 -0700 |
---|---|---|
committer | Raúl Benencia <rul@kalgan.cc> | 2019-06-23 13:32:01 -0700 |
commit | 284c8327c95bb0c71b111ebf95723a35a478295c (patch) | |
tree | faea9db4b1bba00d73d33b2065ed102f88b76b2c /.emacs.local.d/elisp/beancount.el |
Add emacs config
Diffstat (limited to '.emacs.local.d/elisp/beancount.el')
-rw-r--r-- | .emacs.local.d/elisp/beancount.el | 653 |
1 files changed, 653 insertions, 0 deletions
diff --git a/.emacs.local.d/elisp/beancount.el b/.emacs.local.d/elisp/beancount.el new file mode 100644 index 0000000..7ca2a02 --- /dev/null +++ b/.emacs.local.d/elisp/beancount.el @@ -0,0 +1,653 @@ +;;; beancount.el --- A minor mode that can be used to edit beancount input files. + +;; Copyright (C) 2013 Martin Blais <blais@furius.ca> +;; Copyright (C) 2015 Free Software Foundation, Inc. + +;; Version: 0 +;; Author: Martin Blais <blais@furius.ca> +;; Author: Stefan Monnier <monnier@iro.umontreal.ca> + +;; 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 'font-lock) + +(defgroup beancount () + "Editing mode for Beancount files." + :group 'beancount) + +(defconst beancount-timestamped-directive-names + '("balance" + "open" + "close" + "pad" + "document" + "note" + ;; The ones below are not followed by an account name. + "event" + "price" + "commodity" + "query" + "txn") + "Directive names that can appear after a date.") + +(defconst beancount-nontimestamped-directive-names + '("pushtag" + "poptag" + "option" + "include" + "plugin") + "Directive names that can appear after a date.") + +(defvar beancount-directive-names + (append beancount-nontimestamped-directive-names + beancount-timestamped-directive-names) + "A list of the directive names.") + +(defconst beancount-tag-chars "[:alnum:]-_/.") + +(defconst beancount-account-categories + '("Assets" "Liabilities" "Equity" "Income" "Expenses")) + +(defconst beancount-account-chars "[:alnum:]-_:") + +(defconst beancount-option-names + ;; This list is kept in sync with the options defined in + ;; beancount/parser/options.py. + ;; Note: We should eventually build a tool that spits out the current list + ;; automatically. + '("title" + "name_assets" + "name_liabilities" + "name_equity" + "name_income" + "name_expenses" + "bookin_algorithm" + "bookin_method" + "account_previous_balances" + "account_previous_earnings" + "account_previous_conversions" + "account_current_earnings" + "account_current_conversions" + "account_rounding" + "conversion_currency" + "inferred_tolerance_default" + "inferred_tolerance_multiplier" + "infer_tolerance_from_cost" + "documents" + "operating_currency" + "render_commas" + "plugin_processing_mode" + "plugin" + "long_string_maxlines" + )) + +(defvar beancount-font-lock-keywords + `(;; Reserved keywords + (,(regexp-opt beancount-directive-names) . font-lock-keyword-face) + + ;; Tags & Links + ("[#\\^][A-Za-z0-9\-_/.]+" . font-lock-type-face) + + ;; Date + ("[0-9][0-9][0-9][0-9][-/][0-9][0-9][-/][0-9][0-9]" . font-lock-constant-face) + + ;; Account + ("\\([A-Z][A-Za-z0-9\-]+:\\)+\\([A-Z][A-Za-z0-9\-]+\\)" . font-lock-builtin-face) + + ;; Txn Flags + ("! " . font-lock-warning-face) + + ;; Number + Currency + ;;; ("\\([\\-+]?[0-9,]+\\(\\.[0-9]+\\)?\\)\\s-+\\([A-Z][A-Z0-9'\.]\\{1,10\\}\\)" . ) + )) + + +(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 (vconcat p [(\')]) #'beancount-insert-account) + (define-key map (vconcat p [(control g)]) #'beancount-transaction-set-flag) + (define-key map (vconcat p [(r)]) #'beancount-init-accounts) + (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) + + ;; FIXME: Binding TAB breaks expected org-mode behavior to fold/unfold. We + ;; need to find a better solution. + ;;(define-key map [?\t] #'beancount-tab) + 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)) + +(defun beancount--goto-bob () (goto-char (point-min))) + +;;;###autoload +(define-minor-mode beancount-mode + "A minor mode to help editing Beancount files. +This can be used within other text modes, in particular, org-mode +is great for sectioning large files with many transactions. + +\\{beancount-mode-map}" + :init-value nil + :lighter " Beancount" + :group 'beancount + + ;; The following is mostly lifted from lisp-mode. + + (set (make-local-variable 'paragraph-ignore-fill-prefix) t) + (set (make-local-variable 'fill-paragraph-function) #'lisp-fill-paragraph) + + (set (make-local-variable 'comment-start) ";; ") + + ;; Look within the line for a ; following an even number of backslashes + ;; after either a non-backslash or the line beginning. + (set (make-local-variable 'comment-start-skip) + "\\(\\(^\\|[^\\\\\n]\\)\\(\\\\\\\\\\)*\\);+ *") + ;; Font lock mode uses this only when it KNOWS a comment is starting. + ;; FIXME: Why bother? + (set (make-local-variable 'font-lock-comment-start-skip) ";+ *") + ;; Default to `;;' in comment-region. + (set (make-local-variable 'comment-add) 1) + + ;; Org-mode sets both of these to `org-comment-or-uncomment-region', + ;; which doesn't know about our ";" comments. + (kill-local-variable 'comment-region-function) + (kill-local-variable 'uncomment-region-function) + + ;; No tabs by default. + (set (make-local-variable 'indent-tabs-mode) nil) + + (add-hook 'completion-at-point-functions + #'beancount-completion-at-point nil t) + (set (make-local-variable 'completion-ignore-case) t) + + ;; Customize font-lock for beancount. + ;; + (set-syntax-table beancount-mode-syntax-table) + (when (fboundp 'syntax-ppss-flush-cache) + (syntax-ppss-flush-cache (point-min)) + (set (make-local-variable 'syntax-begin-function) #'beancount--goto-bob)) + ;; Force font-lock to use the syntax-table to find strings-and-comments, + ;; regardless of what the "host major mode" decided. + (set (make-local-variable 'font-lock-keywords-only) nil) + ;; Important: you have to use 'nil for the mode here because in certain major + ;; modes (e.g. org-mode) the font-lock-keywords is a buffer-local variable. + (if beancount-mode + (font-lock-add-keywords nil beancount-font-lock-keywords) + (font-lock-remove-keywords nil beancount-font-lock-keywords)) + (if (fboundp 'font-lock-flush) + (font-lock-flush) + (with-no-warnings (font-lock-fontify-buffer))) + + (when beancount-mode + (beancount-init-accounts)) + ) + +(defvar beancount-accounts nil + "A list of the accounts available in this buffer. +This is a cache of the value computed by `beancount-get-accounts'.") +(make-variable-buffer-local 'beancount-accounts) + +(defun beancount-init-accounts () + "Initialize or reset the list of accounts." + (interactive) + (setq beancount-accounts (beancount-get-accounts)) + (message "Accounts updated.")) + +(defvar beancount-date-regexp "[0-9][0-9][0-9][0-9][-/][0-9][0-9][-/][0-9][0-9]" + "A regular expression to match dates.") + +(defvar beancount-account-regexp + (concat (regexp-opt beancount-account-categories) + "\\(?::[[:upper:]][" beancount-account-chars "]+\\)") + "A regular expression to match account names.") + +(defvar beancount-number-regexp "[-+]?[0-9,]+\\(?:\\.[0-9]*\\)?" + "A regular expression to match decimal numbers in beancount.") + +(defvar beancount-currency-regexp "[A-Z][A-Z-_'.]*" + "A regular expression to match currencies in beancount.") + +(defun beancount-tab () + "Try to use the right meaning of TAB." + (interactive) + (let ((cdata (beancount-completion-at-point))) + (if cdata + ;; There's beancount-specific completion at point. + (call-interactively #'completion-at-point) + (let* ((beancount-mode nil) + (fallback (key-binding (this-command-keys)))) + (if (commandp fallback) + (command-execute fallback)))))) + +(defun beancount-tags (prefix) + "Return list of all tags starting with PREFIX in current buffer. +Excludes tags appearing on the current line." + (unless (string-match "\\`[#^]" prefix) + (error "Unexpected prefix to search tags: %S" prefix)) + (let ((found ()) + (re (concat prefix "[" beancount-tag-chars "]*"))) + (save-excursion + (forward-line 0) + (while (re-search-backward re nil t) + (push (match-string 0) found))) + ;; Ignore tags on current line. + (save-excursion + (forward-line 1) + (while (re-search-forward re nil t) + (push (match-string 0) found))) + (delete-dups found))) + +(defconst beancount-txn-regexp + ;; For the full definition of a flag, see the rule that emits FLAG in + ;; beancount/parser/lexer.l. For this, let's assume that it's a single char + ;; that's neither a space nor a lower-case letter. This should be updated as + ;; the parser is improved. + "^[0-9-/]+ +\\(?:txn +\\)?[^ [:lower:]]\\($\\| \\)") + +(defun beancount-inside-txn-p () + ;; FIXME: The doc doesn't actually say how the legs of a transaction can be + ;; layed out. We assume that they all start with some space on the line. + (save-excursion + (forward-line 0) + (while (and (looking-at "[ \t]") (not (bobp))) + (forward-line -1)) + (looking-at beancount-txn-regexp))) + +(defun beancount-completion-at-point () + "Return the completion data relevant for the text at point." + (let ((bp (buffer-substring (line-beginning-position) (point)))) + (cond + ((string-match "\\`[a-z]*\\'" bp) + ;; A directive starting at BOL (hence non-timestamped). + (list (line-beginning-position) + (save-excursion (skip-chars-forward "a-z") (point)) + '("pushtag" "poptag"))) + + ((string-match + (concat "\\`option +\\(\"[a-z_]*\\)?\\'") + bp) + (list (- (point) + (if (match-end 1) (- (match-end 1) (match-beginning 1)) 0)) + (save-excursion (skip-chars-forward "a-z_") + (if (looking-at "\"") (forward-char 1)) + (point)) + (mapcar (lambda (s) (concat "\"" s "\"")) beancount-option-names))) + + ((string-match + (concat "\\`poptag +\\(#[" beancount-tag-chars "]*\\)?\\'") + bp) + (list (- (point) + (if (match-end 1) (- (match-end 1) (match-beginning 1)) 0)) + (save-excursion (skip-chars-forward beancount-tag-chars) (point)) + (save-excursion + (let ((opened ())) + (while (re-search-backward + (concat "^pushtag +\\(#[" beancount-tag-chars "]+\\)") + nil t) + (push (match-string 1) opened)) + opened)))) + + ((string-match "\\`[0-9-/]+ +\\([[:alpha:]]*\\'\\)" bp) + ;; A timestamped directive. + (list (- (point) (- (match-end 1) (match-beginning 1))) + (save-excursion (skip-chars-forward "[:alpha:]") (point)) + beancount-timestamped-directive-names)) + + ((and (beancount-inside-txn-p) + (string-match (concat "\\`[ \t]+\\([" + beancount-account-chars "]*\\)\\'") + bp)) + ;; Hopefully, an account name. We don't force the partially-written + ;; account name to start with a capital, so that it's possible to use + ;; substring completion and also so we can rely on completion to put the + ;; right capitalization (thanks to completion-ignore-case). + (list (- (point) (- (match-end 1) (match-beginning 1))) + (save-excursion (skip-chars-forward beancount-account-chars) + (point)) + #'beancount-account-completion-table)) + + ((string-match (concat "\\`[0-9-/]+ +\\(" + (regexp-opt beancount-timestamped-directive-names) + "\\) +\\([" beancount-account-chars "]*\\'\\)") + bp) + (list (- (point) (- (match-end 2) (match-beginning 2))) + (save-excursion (skip-chars-forward beancount-account-chars) + (point)) + (if (equal (match-string 1 bp) "open") + (append + (mapcar (lambda (c) (concat c ":")) beancount-account-categories) + beancount-accounts) + #'beancount-account-completion-table))) + + ((string-match (concat "[#^][" beancount-tag-chars "]*\\'") bp) + (list (- (point) (- (match-end 0) (match-beginning 0))) + (save-excursion (skip-chars-forward beancount-tag-chars) (point)) + (completion-table-dynamic #'beancount-tags)))))) + +(defun beancount-hash-keys (hashtable) + "Extract all the keys of the given hashtable. Return a sorted list." + (let (rlist) + (maphash (lambda (k _v) (push k rlist)) hashtable) + (sort rlist 'string<))) + +(defun beancount-get-accounts () + "Heuristically obtain a list of all the accounts used in all the postings. +We ignore patterns seen the line 'exclude-line'. If ALL is non-nil, look +for account names in postings as well (default is to look at the @defaccount +declarations only." + (let ((accounts (make-hash-table :test 'equal))) + (save-excursion + (goto-char (point-min)) + (while (re-search-forward beancount-account-regexp nil t) + (puthash (match-string-no-properties 0) nil accounts))) + (sort (beancount-hash-keys accounts) 'string<))) + +(defcustom beancount-use-ido t + "If non-nil, use ido-style completion rather than the standard completion." + :type 'boolean) + +(defun beancount-account-completion-table (string pred action) + (if (eq action 'metadata) + '(metadata (category . beancount-account)) + (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))) +(cond + ((boundp 'completion-category-defaults) + (add-to-list 'completion-category-defaults beancount--completion-overrides)) + ((and (boundp 'completion-category-overrides) + (not (assq 'beancount-account completion-category-overrides))) + (push beancount--completion-overrides completion-category-overrides))) + +(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' is too dumb to understand functional + ;; completion tables! + (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)) + + +(defun beancount-transaction-set-flag () + (interactive) + (save-excursion + (backward-paragraph 1) + (forward-line 1) + (while (search-forward "!" (line-end-position) t) + (replace-match "*")))) + + +(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-max-accounts-width (begin end) + "Return the minimum widths of a list of account names on a list +of lines. Initial whitespace is ignored." + (let* (widths) + (beancount-for-line-in-region + begin end + (let* ((line (thing-at-point 'line))) + (when (string-match + (concat "^[ \t]*\\(" beancount-account-regexp "\\)") line) + (push (length (match-string 1 line)) widths)))) + (apply 'max widths))) + + +(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)) + +(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))) + + +(provide 'beancount) +;;; beancount.el ends here |