Patch for beancount.el

82 views
Skip to first unread message

Stefan Monnier

unread,
Dec 17, 2019, 6:05:23 PM12/17/19
to bean...@googlegroups.com
I finally got around to merging the patches I used to use with the old
(minor-mode) version of beancount.el into the new version.

Most of the tweaks I was using aren't needed any more so all that's left
is the patch below:

- Remove the redundant `:group`s (since they default to the last
`defgroup` anyway).

- New var `beancount-electricity` to control behavior of RET.
When non-nil, RET at the end of a leg adds the currency if missing (or
tries to anyway: I think it's done in a too naive way which doesn't
account for the many ways to specify the amount).

- Comment out a `defvar` of `beancount-directive-names` that doesn't do
anything since it's preceded by a `defconst` of the same var.

- Add a M-RET binding that inserts the date part of a new transaction.

- Fix `beancount-collect`: the old code collects all matches and then
its callers removed the entry found at point (because that entry is
the one we're in the process of completing so it's presumably not
(yet) valid) but that misbehaves when the entry at point is actually
already valid, in which case that entry is incorrectly removed from
the possible completions. So instead I changed `beancount-collect` so
it ignores matches around point which usually gives the same result
except when the match around point is *also* found elsewhere (in which
case the new code keeps that completion whereas the old one
incorrectly discarded it).


Stefan


diff --git a/editors/emacs/beancount.el b/editors/emacs/beancount.el
index 37e7eed0..d1a8cf22 100644
--- a/editors/emacs/beancount.el
+++ b/editors/emacs/beancount.el
@@ -40,82 +40,72 @@

(defcustom beancount-transaction-indent 2
"Transaction indent."
- :type 'integer
- :group 'beancount)
+ :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
- :group 'beancount)
+ :type 'integer)

(defcustom beancount-highlight-transaction-at-point nil
"If t highlight transaction under point."
- :type 'boolean
- :group 'beancount)
+ :type 'boolean)

(defcustom beancount-use-ido t
"If non-nil, use ido-style completion rather than the standard."
- :type 'boolean
- :group 'beancount)
+ :type 'boolean)
+
+(defcustom beancount-electricity t
+ "If non-nil, make some self-inserting keys electric.
+Currently, only `newline' is electric, to add missing currency."
+ :type 'boolean)

(defgroup beancount-faces nil "Beancount mode highlighting" :group 'beancount)

(defface beancount-directive
`((t :inherit font-lock-keyword-face))
- "Face for Beancount directives."
- :group 'beancount-faces)
+ "Face for Beancount directives.")

(defface beancount-tag
`((t :inherit font-lock-type-face))
- "Face for Beancount tags."
- :group 'beancount-faces)
+ "Face for Beancount tags.")

(defface beancount-link
`((t :inherit font-lock-type-face))
- "Face for Beancount links."
- :group 'beancount-faces)
+ "Face for Beancount links.")

(defface beancount-date
`((t :inherit font-lock-constant-face))
- "Face for Beancount dates."
- :group 'beancount-faces)
+ "Face for Beancount dates.")

(defface beancount-account
`((t :inherit font-lock-builtin-face))
- "Face for Beancount account names."
- :group 'beancount-faces)
+ "Face for Beancount account names.")

(defface beancount-amount
`((t :inherit font-lock-default-face))
- "Face for Beancount amounts."
- :group 'beancount-faces)
+ "Face for Beancount amounts.")

(defface beancount-narrative
`((t :inherit font-lock-builtin-face))
- "Face for Beancount transactions narrative."
- :group 'beancount-faces)
+ "Face for Beancount transactions narrative.")

(defface beancount-narrative-cleared
`((t :inherit font-lock-string-face))
- "Face for Beancount cleared transactions narrative."
- :group 'beancount-faces)
+ "Face for Beancount cleared transactions narrative.")

(defface beancount-narrative-pending
`((t :inherit font-lock-keyword-face))
- "Face for Beancount pending transactions narrative."
- :group 'beancount-faces)
+ "Face for Beancount pending transactions narrative.")

(defface beancount-metadata
`((t :inherit font-lock-type-face))
- "Face for Beancount metadata."
- :group 'beancount-faces)
+ "Face for Beancount metadata.")

(defface beancount-highlight
`((t :inherit highlight))
- "Face to highlight Beancount transaction at point."
- :group 'beancount-faces)
+ "Face to highlight Beancount transaction at point.")

(defconst beancount-account-directive-names
'("balance"
@@ -147,10 +137,10 @@ to align all amounts."
"pushtag")
"Directive names that can appear at the beginning of a line.")

-(defvar beancount-directive-names
- (append beancount-directive-names
- beancount-timestamped-directive-names)
- "A list of the directive names.")
+;; (defvar beancount-directive-names
+;; (append beancount-directive-names
+;; beancount-timestamped-directive-names)
+;; "A list of the directive names.")

(defconst beancount-account-categories
'("Assets" "Liabilities" "Equity" "Income" "Expenses"))
@@ -289,6 +279,7 @@ to align all amounts."
(let ((map (make-sparse-keymap))
(p beancount-mode-map-prefix))
(define-key map (kbd "TAB") #'beancount-tab-dwim)
+ (define-key map [?\M-\C-m] #'beancount-insert-entry)
(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)
@@ -330,8 +321,10 @@ to align all amounts."
(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 nil t)
+
(setq-local font-lock-defaults '(beancount-font-lock-keywords))
(setq-local font-lock-syntax-table t)

@@ -438,7 +432,8 @@ With an argument move to the next non cleared transaction."
((beancount-looking-at
(concat "^" beancount-date-regexp
"\\s-+" (regexp-opt beancount-account-directive-names)
- "\\s-+\\([" beancount-account-chars "]*\\)") 1 pos)
+ "\\s-+\\([" beancount-account-chars "]*\\)")
+ 1 pos)
(setq beancount-accounts nil)
(list (match-beginning 1) (match-end 1) #'beancount-account-completion-table))

@@ -462,7 +457,7 @@ With an argument move to the next non cleared transaction."
(lambda (string pred action)
(if (null candidates)
(setq candidates
- (sort (delete string (beancount-collect regexp 1)) #'string<)))
+ (sort (beancount-collect regexp 1) #'string<)))
(complete-with-action action candidates string pred))))
(list (match-beginning 1) (match-end 1) completion-table)))

@@ -475,25 +470,29 @@ With an argument move to the next non cleared transaction."
(lambda (string pred action)
(if (null candidates)
(setq candidates
- (sort (delete string (beancount-collect regexp 1)) #'string<)))
+ (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."
- (save-excursion
- (save-match-data
- (let ((hash (make-hash-table :test 'equal)))
- (goto-char (point-min))
- (while (re-search-forward regexp nil t)
- (puthash (match-string-no-properties n) nil hash))
- (hash-table-keys hash)))))
+ (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' 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 (delete string (beancount-collect beancount-account-regexp 0)) #'string<)))
+ (sort (beancount-collect beancount-account-regexp 0) #'string<)))
(complete-with-action action beancount-accounts string pred)))

;; Default to substring completion for beancount accounts.
@@ -750,6 +749,46 @@ what that column is and returns it (an integer)."
))
column))

+(defun beancount--account-currency (account)
+ (save-excursion
+ (goto-char (point-min))
+ (when (re-search-forward (concat "^[0-9-]+[ \t]+open[ \t]+"
+ (regexp-quote account)
+ "[ \t]+\\([[:upper:]]+\\)[ \t]*"
+ "\\(?:$\\|;\\)")
+ nil t)
+ ;; The account has declared a single currency, so we can fill it in.
+ (match-string 1))))
+
+(defun beancount--electric ()
+ ;; TODO: When hitting RET after the first leg of a txn, look back
+ ;; for similar transactions and insert the matching account!
+ (when (and beancount-electricity (eq last-command-event ?\n))
+ (cond
+ ;; ((save-excursion (forward-line -1) (looking-at beancount--txn-re))
+ ;; ;; FIXME: Provide an indent-line-function instead!
+ ;; ;; TODO: Auto-align the amount (probably use a beancount-amount-column).
+ ;; (indent-line-to 2))
+ ((save-excursion (forward-line -1)
+ (and (beancount-inside-transaction-p)
+ (looking-at (concat "[ \t]+\\(["
+ beancount-account-chars
+ "]+\\)[ \t]+-?[0-9.]+[ \t]*$"))))
+ ;; Last line is a leg 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-entry ()
+ "Start a new entry."
+ (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.")

Daniele Nicolodi

unread,
Dec 18, 2019, 12:39:17 PM12/18/19
to bean...@googlegroups.com
Hello Stefan,

On 17-12-2019 16:05, Stefan Monnier wrote:
> I finally got around to merging the patches I used to use with the old
> (minor-mode) version of beancount.el into the new version.

Thank you very much for the patch. Do you mind if I split it in a few
commits and I submit it with a few tweaks and your attribution as a
merge request on bitbucket, so that Martin can easily merge it?

> - Remove the redundant `:group`s (since they default to the last
> `defgroup` anyway).

I didn't know about this feature.

> - New var `beancount-electricity` to control behavior of RET.
> When non-nil, RET at the end of a leg adds the currency if missing (or
> tries to anyway: I think it's done in a too naive way which doesn't
> account for the many ways to specify the amount).

I think this should be called `beancount-electric-currency` instead. Can
you be more specific about how this may not work for more complex ways
of specifying the amount?

> - Comment out a `defvar` of `beancount-directive-names` that doesn't do
> anything since it's preceded by a `defconst` of the same var.

I'll remove it all together.

> - Add a M-RET binding that inserts the date part of a new transaction.

I don't enter many transactions manually, but this addition seems
reasonable. What about a M-S-RET binding that asks for the transaction
date with org-read-date (using the calendar to choose the date)?

> - Fix `beancount-collect`: the old code collects all matches and then
> its callers removed the entry found at point (because that entry is
> the one we're in the process of completing so it's presumably not
> (yet) valid) but that misbehaves when the entry at point is actually
> already valid, in which case that entry is incorrectly removed from
> the possible completions. So instead I changed `beancount-collect` so
> it ignores matches around point which usually gives the same result
> except when the match around point is *also* found elsewhere (in which
> case the new code keeps that completion whereas the old one
> incorrectly discarded it).

Good catch!

Thank you.

Cheers,
Dan

Stefano Zacchiroli

unread,
Dec 18, 2019, 1:40:12 PM12/18/19
to bean...@googlegroups.com
On Wed, Dec 18, 2019 at 10:39:13AM -0700, Daniele Nicolodi wrote:
> > - Add a M-RET binding that inserts the date part of a new transaction.
>
> I don't enter many transactions manually, but this addition seems
> reasonable. What about a M-S-RET binding that asks for the transaction
> date with org-read-date (using the calendar to choose the date)?

I'm very much looking forward for this functionality, thanks for it
Stefan.

In terms of which date to enter upon M-S-RET, I think a reasonable
default would be the date in the beancount entry that precedes the place
where the date is being inserted. That might be the default date shown
in the calendar, as you propose, or just the date directly inserted in
the file.

My 0.02 EUR,
Cheers
--
Stefano Zacchiroli . za...@upsilon.cc . upsilon.cc/zack . . o . . . o . o
Computer Science Professor . CTO Software Heritage . . . . . o . . . o o
Former Debian Project Leader & OSI Board Director . . . o o o . . . o .
« the first rule of tautology club is the first rule of tautology club »

Stefan Monnier

unread,
Dec 18, 2019, 10:34:24 PM12/18/19
to bean...@googlegroups.com
>> I finally got around to merging the patches I used to use with the old
>> (minor-mode) version of beancount.el into the new version.
> Thank you very much for the patch. Do you mind if I split it in a few
> commits and I submit it with a few tweaks and your attribution as a
> merge request on bitbucket, so that Martin can easily merge it?

That would be very sweet.

>> - New var `beancount-electricity` to control behavior of RET.
>> When non-nil, RET at the end of a leg adds the currency if missing (or
>> tries to anyway: I think it's done in a too naive way which doesn't
>> account for the many ways to specify the amount).
> I think this should be called `beancount-electric-currency` instead.

When I introduced it I intended for it to do more:
- do a bit of auto-indentation (that was when beancount-mode didn't
have a line-indent-function)
- re-align the amount (again, it's now done by electric-indent-mode)
- auto-add the next account if it's always been the same so far (never
implemented)

But, yes, nowadays it's only the currency. I don't have any strong
opinion on the name of the config var.

> Can you be more specific about how this may not work for more complex
> ways of specifying the amount?

Not really: I only ever use simple amounts (no @ things and whatnot) so
I not only haven't tested with anything more complex but I can't
remember what those more complex cases can look like anyway.

Oh I can give you one concrete case I bumped into and never fixed:

Asset:Account (0.8 * 100) RET

will not insert the currency because the code doesn't recognize "(0.8 *
100)" as an amount.

>> - Add a M-RET binding that inserts the date part of a new transaction.
> I don't enter many transactions manually, but this addition seems
> reasonable.

Yeah, it's not super useful, admittedly.
Stefano's suggestion to use the date of the previous transaction might
be a good alternative. I don't have strong feelings about it
should do. I find the key-binding intuitive and the idea of the
functionality appealing, although the practice of it is
rather underwhelming.

Thank you for your rewrite into a major mode: it fixed most of the
annoyances I encountered in the old mode.


Stefan

Martin Blais

unread,
Dec 21, 2019, 5:43:17 PM12/21/19
to Beancount
On Wed, Dec 18, 2019 at 10:34 PM Stefan Monnier
<mon...@iro.umontreal.ca> wrote:
>
> >> I finally got around to merging the patches I used to use with the old
> >> (minor-mode) version of beancount.el into the new version.
> > Thank you very much for the patch. Do you mind if I split it in a few
> > commits and I submit it with a few tweaks and your attribution as a
> > merge request on bitbucket, so that Martin can easily merge it?
>
> That would be very sweet.

Feel free to send me the newest version of the file as well, I'm off
for a few days.


> >> - New var `beancount-electricity` to control behavior of RET.
> >> When non-nil, RET at the end of a leg adds the currency if missing (or
> >> tries to anyway: I think it's done in a too naive way which doesn't
> >> account for the many ways to specify the amount).
> > I think this should be called `beancount-electric-currency` instead.
>
> When I introduced it I intended for it to do more:
> - do a bit of auto-indentation (that was when beancount-mode didn't
> have a line-indent-function)
> - re-align the amount (again, it's now done by electric-indent-mode)

That's great.
My amounts are all misaligned and I find it annoying, haven't been
able to find time to fix it properly.
Thanks for doing this.
> --
> You received this message because you are subscribed to the Google Groups "Beancount" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to beancount+...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/beancount/jwvimmddmw6.fsf-monnier%2Bgmane.comp.finance.beancount%40gnu.org.

Daniele Nicolodi

unread,
Dec 23, 2019, 11:54:10 PM12/23/19
to bean...@googlegroups.com
On 18/12/2019 20:34, Stefan Monnier wrote:
>>> I finally got around to merging the patches I used to use with the old
>>> (minor-mode) version of beancount.el into the new version.
>> Thank you very much for the patch. Do you mind if I split it in a few
>> commits and I submit it with a few tweaks and your attribution as a
>> merge request on bitbucket, so that Martin can easily merge it?
>
> That would be very sweet.

Here is the pull request

https://bitbucket.org/blais/beancount/pull-requests/140

I hope I haven't broken anything. Please have a look.

Cheers,
Dan
Reply all
Reply to author
Forward
0 new messages