beancount.el 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013
  1. ;;; beancount.el --- A major mode to edit Beancount input files. -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2013 Martin Blais <blais@furius.ca>
  3. ;; Copyright (C) 2015 Free Software Foundation, Inc.
  4. ;; Copyright (C) 2019 Daniele Nicolodi <daniele@grinta.net>
  5. ;; Version: 0
  6. ;; Author: Martin Blais <blais@furius.ca>
  7. ;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
  8. ;; Author: Daniele Nicolodi <daniele@grinta.net>
  9. ;; This file is not part of GNU Emacs.
  10. ;; This package is free software: you can redistribute it and/or modify
  11. ;; it under the terms of the GNU General Public License as published by
  12. ;; the Free Software Foundation, either version 3 of the License, or
  13. ;; (at your option) any later version.
  14. ;; This package is distributed in the hope that it will be useful,
  15. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ;; GNU General Public License for more details.
  18. ;; You should have received a copy of the GNU General Public License
  19. ;; along with this package. If not, see <http://www.gnu.org/licenses/>.
  20. ;;; Commentary:
  21. ;; TODO: Add a flymake rule, using bean-check
  22. ;;; Code:
  23. (autoload 'ido-completing-read "ido")
  24. (require 'subr-x)
  25. (require 'outline)
  26. (defgroup beancount ()
  27. "Editing mode for Beancount files."
  28. :group 'beancount)
  29. (defcustom beancount-transaction-indent 2
  30. "Transaction indent."
  31. :type 'integer)
  32. (defcustom beancount-number-alignment-column 52
  33. "Column to which align numbers in postinng definitions. Set to
  34. 0 to automatically determine the minimum column that will allow
  35. to align all amounts."
  36. :type 'integer)
  37. (defcustom beancount-highlight-transaction-at-point nil
  38. "If t highlight transaction under point."
  39. :type 'boolean)
  40. (defcustom beancount-use-ido t
  41. "If non-nil, use ido-style completion rather than the standard."
  42. :type 'boolean)
  43. (defcustom beancount-electric-currency nil
  44. "If non-nil, make `newline' try to add missing currency to
  45. complete the posting at point. The correct currency is determined
  46. from the open directive for the relevant account."
  47. :type 'boolean)
  48. (defgroup beancount-faces nil "Beancount mode highlighting" :group 'beancount)
  49. (defface beancount-directive
  50. `((t :inherit font-lock-keyword-face))
  51. "Face for Beancount directives.")
  52. (defface beancount-tag
  53. `((t :inherit font-lock-type-face))
  54. "Face for Beancount tags.")
  55. (defface beancount-link
  56. `((t :inherit font-lock-type-face))
  57. "Face for Beancount links.")
  58. (defface beancount-date
  59. `((t :inherit font-lock-constant-face))
  60. "Face for Beancount dates.")
  61. (defface beancount-account
  62. `((t :inherit font-lock-builtin-face))
  63. "Face for Beancount account names.")
  64. (defface beancount-amount
  65. `((t :inherit font-lock-default-face))
  66. "Face for Beancount amounts.")
  67. (defface beancount-narrative
  68. `((t :inherit font-lock-builtin-face))
  69. "Face for Beancount transactions narrative.")
  70. (defface beancount-narrative-cleared
  71. `((t :inherit font-lock-string-face))
  72. "Face for Beancount cleared transactions narrative.")
  73. (defface beancount-narrative-pending
  74. `((t :inherit font-lock-keyword-face))
  75. "Face for Beancount pending transactions narrative.")
  76. (defface beancount-metadata
  77. `((t :inherit font-lock-type-face))
  78. "Face for Beancount metadata.")
  79. (defface beancount-highlight
  80. `((t :inherit highlight))
  81. "Face to highlight Beancount transaction at point.")
  82. (defconst beancount-account-directive-names
  83. '("balance"
  84. "close"
  85. "document"
  86. "note"
  87. "open"
  88. "pad")
  89. "Directive bames that can appear after a date and are followd by an account.")
  90. (defconst beancount-no-account-directive-names
  91. '("commodity"
  92. "event"
  93. "price"
  94. "query"
  95. "txn")
  96. "Directive names that can appear after a date and are _not_ followed by an account.")
  97. (defconst beancount-timestamped-directive-names
  98. (append beancount-account-directive-names
  99. beancount-no-account-directive-names)
  100. "Directive names that can appear after a date.")
  101. (defconst beancount-directive-names
  102. '("include"
  103. "option"
  104. "plugin"
  105. "poptag"
  106. "pushtag")
  107. "Directive names that can appear at the beginning of a line.")
  108. (defconst beancount-account-categories
  109. '("Assets" "Liabilities" "Equity" "Income" "Expenses"))
  110. (defconst beancount-tag-chars "[:alnum:]-_/.")
  111. (defconst beancount-account-chars "[:alnum:]-_:")
  112. (defconst beancount-option-names
  113. ;; This list is kept in sync with the options defined in
  114. ;; beancount/parser/options.py.
  115. '("account_current_conversions"
  116. "account_current_earnings"
  117. "account_previous_balances"
  118. "account_previous_conversions"
  119. "account_previous_earnings"
  120. "account_rounding"
  121. "allow_deprecated_none_for_tags_and_links"
  122. "allow_pipe_separator"
  123. "booking_method"
  124. "conversion_currency"
  125. "documents"
  126. "infer_tolerance_from_cost"
  127. "inferred_tolerance_default"
  128. "inferred_tolerance_multiplier"
  129. "insert_pythonpath"
  130. "long_string_maxlines"
  131. "name_assets"
  132. "name_equity"
  133. "name_expenses"
  134. "name_income"
  135. "name_liabilities"
  136. "operating_currency"
  137. "plugin_processing_mode"
  138. "render_commas"
  139. "title"))
  140. (defconst beancount-date-regexp "[0-9]\\{4\\}[-/][0-9]\\{2\\}[-/][0-9]\\{2\\}"
  141. "A regular expression to match dates.")
  142. (defconst beancount-account-regexp
  143. (concat (regexp-opt beancount-account-categories)
  144. "\\(?::[[:upper:]][[:alnum:]-_]+\\)+")
  145. "A regular expression to match account names.")
  146. (defconst beancount-number-regexp "[-+]?[0-9]+\\(?:,[0-9]\\{3\\}\\)*\\(?:\\.[0-9]*\\)?"
  147. "A regular expression to match decimal numbers.")
  148. (defconst beancount-currency-regexp "[A-Z][A-Z-_'.]*"
  149. "A regular expression to match currencies.")
  150. (defconst beancount-flag-regexp
  151. ;; Single char that is neither a space nor a lower-case letter.
  152. "[^ a-z]")
  153. (defconst beancount-transaction-regexp
  154. (concat "^\\(" beancount-date-regexp "\\) +"
  155. "\\(?:txn +\\)?"
  156. "\\(" beancount-flag-regexp "\\) +"
  157. "\\(\".*\"\\)"))
  158. (defconst beancount-posting-regexp
  159. (concat "^\\s-+"
  160. "\\(" beancount-account-regexp "\\)"
  161. "\\(?:\\s-+\\(\\(" beancount-number-regexp "\\)"
  162. "\\s-+\\(" beancount-currency-regexp "\\)\\)\\)?"))
  163. (defconst beancount-directive-regexp
  164. (concat "^\\(" (regexp-opt beancount-directive-names) "\\) +"))
  165. (defconst beancount-timestamped-directive-regexp
  166. (concat "^\\(" beancount-date-regexp "\\) +"
  167. "\\(" (regexp-opt beancount-timestamped-directive-names) "\\) +"))
  168. (defconst beancount-metadata-regexp
  169. "^\\s-+\\([a-z][A-Za-z0-9_-]+:\\)\\s-+\\(.+\\)")
  170. (defvar beancount-outline-regexp "\\(;;;+\\|\\*+\\)")
  171. (defun beancount-outline-level ()
  172. (let ((len (- (match-end 1) (match-beginning 1))))
  173. (if (equal (substring (match-string 1) 0 1) ";")
  174. (- len 2)
  175. len)))
  176. (defun beancount-face-by-state (state)
  177. (cond ((string-equal state "*") 'beancount-narrative-cleared)
  178. ((string-equal state "!") 'beancount-narrative-pending)
  179. (t 'beancount-narrative)))
  180. (defun beancount-outline-face ()
  181. (if outline-minor-mode
  182. (cl-case (funcall outline-level)
  183. (1 'org-level-1)
  184. (2 'org-level-2)
  185. (3 'org-level-3)
  186. (4 'org-level-4)
  187. (5 'org-level-5)
  188. (6 'org-level-6)
  189. (otherwise nil))
  190. nil))
  191. (defvar beancount-font-lock-keywords
  192. `((,beancount-transaction-regexp (1 'beancount-date)
  193. (2 (beancount-face-by-state (match-string 2)) t)
  194. (3 (beancount-face-by-state (match-string 2)) t))
  195. (,beancount-posting-regexp (1 'beancount-account)
  196. (2 'beancount-amount nil :lax))
  197. (,beancount-metadata-regexp (1 'beancount-metadata)
  198. (2 'beancount-metadata t))
  199. (,beancount-directive-regexp (1 'beancount-directive))
  200. (,beancount-timestamped-directive-regexp (1 'beancount-date)
  201. (2 'beancount-directive))
  202. ;; Fontify section headers when composed with outline-minor-mode.
  203. (,(concat "^\\(" beancount-outline-regexp "\\).*") . (0 (beancount-outline-face)))
  204. ;; Tags and links.
  205. (,(concat "\\#[" beancount-tag-chars "]*") . 'beancount-tag)
  206. (,(concat "\\^[" beancount-tag-chars "]*") . 'beancount-link)
  207. ;; Number followed by currency not covered by previous rules.
  208. (,(concat beancount-number-regexp "\\s-+" beancount-currency-regexp) . 'beancount-amount)
  209. ;; Accounts not covered by previous rules.
  210. (,beancount-account-regexp . 'beancount-account)
  211. ))
  212. (defun beancount-tab-dwim (&optional arg)
  213. (interactive "P")
  214. (if (and outline-minor-mode
  215. (or arg (outline-on-heading-p)))
  216. (beancount-outline-cycle arg)
  217. (indent-for-tab-command)))
  218. (defvar beancount-mode-map-prefix [(control c)]
  219. "The prefix key used to bind Beancount commands in Emacs")
  220. (defvar beancount-mode-map
  221. (let ((map (make-sparse-keymap))
  222. (p beancount-mode-map-prefix))
  223. (define-key map (kbd "TAB") #'beancount-tab-dwim)
  224. (define-key map (kbd "M-RET") #'beancount-insert-date)
  225. (define-key map (vconcat p [(\')]) #'beancount-insert-account)
  226. (define-key map (vconcat p [(control g)]) #'beancount-transaction-clear)
  227. (define-key map (vconcat p [(l)]) #'beancount-check)
  228. (define-key map (vconcat p [(q)]) #'beancount-query)
  229. (define-key map (vconcat p [(x)]) #'beancount-context)
  230. (define-key map (vconcat p [(k)]) #'beancount-linked)
  231. (define-key map (vconcat p [(p)]) #'beancount-insert-prices)
  232. (define-key map (vconcat p [(\;)]) #'beancount-align-to-previous-number)
  233. (define-key map (vconcat p [(\:)]) #'beancount-align-numbers)
  234. map))
  235. (defvar beancount-mode-syntax-table
  236. (let ((st (make-syntax-table)))
  237. (modify-syntax-entry ?\" "\"\"" st)
  238. (modify-syntax-entry ?\; "<" st)
  239. (modify-syntax-entry ?\n ">" st)
  240. st))
  241. ;;;###autoload
  242. (define-derived-mode beancount-mode fundamental-mode "Beancount"
  243. "A mode for Beancount files.
  244. \\{beancount-mode-map}"
  245. :group 'beancount
  246. :syntax-table beancount-mode-syntax-table
  247. (setq-local paragraph-ignore-fill-prefix t)
  248. (setq-local fill-paragraph-function #'beancount-indent-transaction)
  249. (setq-local comment-start ";")
  250. (setq-local comment-start-skip ";+\\s-*")
  251. (setq-local comment-add 1)
  252. (setq-local indent-line-function #'beancount-indent-line)
  253. (setq-local indent-region-function #'beancount-indent-region)
  254. (setq-local indent-tabs-mode nil)
  255. (setq-local tab-always-indent 'complete)
  256. (setq-local completion-ignore-case t)
  257. (add-hook 'completion-at-point-functions #'beancount-completion-at-point nil t)
  258. (add-hook 'post-command-hook #'beancount-highlight-transaction-at-point nil t)
  259. (add-hook 'post-self-insert-hook #'beancount--electric-currency nil t)
  260. (setq-local font-lock-defaults '(beancount-font-lock-keywords))
  261. (setq-local font-lock-syntax-table t)
  262. (setq-local outline-regexp beancount-outline-regexp)
  263. (setq-local outline-level #'beancount-outline-level))
  264. (defun beancount-collect-pushed-tags (begin end)
  265. "Return list of all pushed (and not popped) tags in the region."
  266. (goto-char begin)
  267. (let ((tags (make-hash-table :test 'equal)))
  268. (while (re-search-forward
  269. (concat "^\\(push\\|pop\\)tag\\s-+\\(#[" beancount-tag-chars "]+\\)") end t)
  270. (if (string-equal (match-string 1) "push")
  271. (puthash (match-string-no-properties 2) nil tags)
  272. (remhash (match-string-no-properties 2) tags)))
  273. (hash-table-keys tags)))
  274. (defun beancount-goto-transaction-begin ()
  275. "Move the cursor to the first line of the transaction definition."
  276. (interactive)
  277. (beginning-of-line)
  278. ;; everything that is indented with at lest one space or tab is part
  279. ;; of the transaction definition
  280. (while (looking-at-p "[ \t]+")
  281. (forward-line -1))
  282. (point))
  283. (defun beancount-goto-transaction-end ()
  284. "Move the cursor to the line after the transaction definition."
  285. (interactive)
  286. (beginning-of-line)
  287. (if (looking-at-p beancount-transaction-regexp)
  288. (forward-line))
  289. ;; everything that is indented with at least one space or tab as part
  290. ;; of the transaction definition
  291. (while (looking-at-p "[ \t]+")
  292. (forward-line))
  293. (point))
  294. (defun beancount-goto-next-transaction (&optional arg)
  295. "Move to the next transaction.
  296. With an argument move to the next non cleared transaction."
  297. (interactive "P")
  298. (beancount-goto-transaction-end)
  299. (let ((done nil))
  300. (while (and (not done)
  301. (re-search-forward beancount-transaction-regexp nil t))
  302. (if (and arg (string-equal (match-string 2) "*"))
  303. (goto-char (match-end 0))
  304. (goto-char (match-beginning 0))
  305. (setq done t)))
  306. (if (not done) (goto-char (point-max)))))
  307. (defun beancount-find-transaction-extents (p)
  308. (save-excursion
  309. (goto-char p)
  310. (list (beancount-goto-transaction-begin)
  311. (beancount-goto-transaction-end))))
  312. (defun beancount-inside-transaction-p ()
  313. (let ((bounds (beancount-find-transaction-extents (point))))
  314. (> (- (cadr bounds) (car bounds)) 0)))
  315. (defun beancount-looking-at (regexp n pos)
  316. (and (looking-at regexp)
  317. (>= pos (match-beginning n))
  318. (<= pos (match-end n))))
  319. (defvar beancount-accounts nil
  320. "A list of the accounts available in this buffer.")
  321. (make-variable-buffer-local 'beancount-accounts)
  322. (defun beancount-completion-at-point ()
  323. "Return the completion data relevant for the text at point."
  324. (save-excursion
  325. (save-match-data
  326. (let ((pos (point)))
  327. (beginning-of-line)
  328. (cond
  329. ;; non timestamped directive
  330. ((beancount-looking-at "[a-z]*" 0 pos)
  331. (list (match-beginning 0) (match-end 0)
  332. (mapcar (lambda (s) (concat s " ")) beancount-directive-names)))
  333. ;; poptag
  334. ((beancount-looking-at
  335. (concat "poptag\\s-+\\(\\(?:#[" beancount-tag-chars "]*\\)\\)") 1 pos)
  336. (list (match-beginning 1) (match-end 1)
  337. (beancount-collect-pushed-tags (point-min) (point))))
  338. ;; option
  339. ((beancount-looking-at
  340. (concat "^option\\s-+\\(\"[a-z_]*\\)") 1 pos)
  341. (list (match-beginning 1) (match-end 1)
  342. (mapcar (lambda (s) (concat "\"" s "\" ")) beancount-option-names)))
  343. ;; timestamped directive
  344. ((beancount-looking-at
  345. (concat beancount-date-regexp "\\s-+\\([[:alpha:]]*\\)") 1 pos)
  346. (list (match-beginning 1) (match-end 1)
  347. (mapcar (lambda (s) (concat s " ")) beancount-timestamped-directive-names)))
  348. ;; timestamped directives followed by account
  349. ((beancount-looking-at
  350. (concat "^" beancount-date-regexp
  351. "\\s-+" (regexp-opt beancount-account-directive-names)
  352. "\\s-+\\([" beancount-account-chars "]*\\)") 1 pos)
  353. (setq beancount-accounts nil)
  354. (list (match-beginning 1) (match-end 1) #'beancount-account-completion-table))
  355. ;; posting
  356. ((and (beancount-looking-at
  357. (concat "[ \t]+\\([" beancount-account-chars "]*\\)") 1 pos)
  358. ;; Do not force the account name to start with a
  359. ;; capital, so that it is possible to use substring
  360. ;; completion and we can rely on completion to fix
  361. ;; capitalization thanks to completion-ignore-case.
  362. (beancount-inside-transaction-p))
  363. (setq beancount-accounts nil)
  364. (list (match-beginning 1) (match-end 1) #'beancount-account-completion-table))
  365. ;; tags
  366. ((beancount-looking-at
  367. (concat "[ \t]+#\\([" beancount-tag-chars "]*\\)") 1 pos)
  368. (let* ((candidates nil)
  369. (regexp (concat "\\#\\([" beancount-tag-chars "]+\\)"))
  370. (completion-table
  371. (lambda (string pred action)
  372. (if (null candidates)
  373. (setq candidates
  374. (sort (beancount-collect regexp 1) #'string<)))
  375. (complete-with-action action candidates string pred))))
  376. (list (match-beginning 1) (match-end 1) completion-table)))
  377. ;; links
  378. ((beancount-looking-at
  379. (concat "[ \t]+\\^\\([" beancount-tag-chars "]*\\)") 1 pos)
  380. (let* ((candidates nil)
  381. (regexp (concat "\\^\\([" beancount-tag-chars "]+\\)"))
  382. (completion-table
  383. (lambda (string pred action)
  384. (if (null candidates)
  385. (setq candidates
  386. (sort (beancount-collect regexp 1) #'string<)))
  387. (complete-with-action action candidates string pred))))
  388. (list (match-beginning 1) (match-end 1) completion-table))))))))
  389. (defun beancount-collect (regexp n)
  390. "Return an unique list of REGEXP group N in the current buffer."
  391. (let ((pos (point)))
  392. (save-excursion
  393. (save-match-data
  394. (let ((hash (make-hash-table :test 'equal)))
  395. (goto-char (point-min))
  396. (while (re-search-forward regexp nil t)
  397. ;; Ignore matches around `pos' (the point position when
  398. ;; entering this funcyion) since that's presumably what
  399. ;; we're currently trying to complete.
  400. (unless (<= (match-beginning 0) pos (match-end 0))
  401. (puthash (match-string-no-properties n) nil hash)))
  402. (hash-table-keys hash))))))
  403. (defun beancount-account-completion-table (string pred action)
  404. (if (eq action 'metadata) '(metadata (category . beancount-account))
  405. (if (null beancount-accounts)
  406. (setq beancount-accounts
  407. (sort (beancount-collect beancount-account-regexp 0) #'string<)))
  408. (complete-with-action action beancount-accounts string pred)))
  409. ;; Default to substring completion for beancount accounts.
  410. (defconst beancount--completion-overrides
  411. '(beancount-account (styles basic partial-completion substring)))
  412. (add-to-list 'completion-category-defaults beancount--completion-overrides)
  413. (defun beancount-number-alignment-column ()
  414. "Return the column to which postings amounts should be aligned to.
  415. Returns `beancount-number-alignment-column' unless it is 0. In
  416. that case, scan the buffer to determine the minimum column that
  417. will allow to align all numbers."
  418. (if (> beancount-number-alignment-column 0)
  419. beancount-number-alignment-column
  420. (save-excursion
  421. (save-match-data
  422. (let ((account-width 0)
  423. (number-width 0))
  424. (goto-char (point-min))
  425. (while (re-search-forward beancount-posting-regexp nil t)
  426. (if (match-string 2)
  427. (let ((accw (- (match-end 1) (line-beginning-position)))
  428. (numw (- (match-end 3) (match-beginning 3))))
  429. (setq account-width (max account-width accw)
  430. number-width (max number-width numw)))))
  431. (+ account-width 2 number-width))))))
  432. (defun beancount-compute-indentation ()
  433. "Return the column to which the current line should be indented."
  434. (save-excursion
  435. (beginning-of-line)
  436. (cond
  437. ;; Only timestamped directives start with a digit.
  438. ((looking-at-p "[0-9]") 0)
  439. ;; Otherwise look at the previous line.
  440. ((and (= (forward-line -1) 0)
  441. (or (looking-at-p "[ \t].+")
  442. (looking-at-p beancount-timestamped-directive-regexp)
  443. (looking-at-p beancount-transaction-regexp)))
  444. beancount-transaction-indent)
  445. ;; Default.
  446. (t 0))))
  447. (defun beancount-align-number (target-column)
  448. (save-excursion
  449. (beginning-of-line)
  450. ;; Check if the current line is a posting with a number to align.
  451. (when (and (looking-at beancount-posting-regexp)
  452. (match-string 2))
  453. (let* ((account-end-column (- (match-end 1) (line-beginning-position)))
  454. (number-width (- (match-end 3) (match-beginning 3)))
  455. (account-end (match-end 1))
  456. (number-beginning (match-beginning 3))
  457. (spaces (max 2 (- target-column account-end-column number-width))))
  458. (unless (eq spaces (- number-beginning account-end))
  459. (goto-char account-end)
  460. (delete-region account-end number-beginning)
  461. (insert (make-string spaces ? )))))))
  462. (defun beancount-indent-line ()
  463. (let ((indent (beancount-compute-indentation))
  464. (savep (> (current-column) (current-indentation))))
  465. (unless (eq indent (current-indentation))
  466. (if savep (save-excursion (indent-line-to indent))
  467. (indent-line-to indent)))
  468. (unless (eq this-command 'beancount-tab-dwim)
  469. (beancount-align-number (beancount-number-alignment-column)))))
  470. (defun beancount-indent-region (start end)
  471. "Indent a region automagically. START and END specify the region to indent."
  472. (let ((deactivate-mark nil)
  473. (beancount-number-alignment-column (beancount-number-alignment-column)))
  474. (save-excursion
  475. (setq end (copy-marker end))
  476. (goto-char start)
  477. (or (bolp) (forward-line 1))
  478. (while (< (point) end)
  479. (unless (looking-at-p "\\s-*$")
  480. (beancount-indent-line))
  481. (forward-line 1))
  482. (move-marker end nil))))
  483. (defun beancount-indent-transaction (&optional _justify _region)
  484. "Indent Beancount transaction at point."
  485. (interactive)
  486. (save-excursion
  487. (let ((bounds (beancount-find-transaction-extents (point))))
  488. (beancount-indent-region (car bounds) (cadr bounds)))))
  489. (defun beancount-transaction-clear (&optional arg)
  490. "Clear transaction at point. With a prefix argument set the
  491. transaction as pending."
  492. (interactive "P")
  493. (save-excursion
  494. (save-match-data
  495. (let ((flag (if arg "!" "*")))
  496. (beancount-goto-transaction-begin)
  497. (if (looking-at beancount-transaction-regexp)
  498. (replace-match flag t t nil 2))))))
  499. (defun beancount-insert-account (account-name)
  500. "Insert one of the valid account names in this file.
  501. Uses ido niceness according to `beancount-use-ido'."
  502. (interactive
  503. (list
  504. (if beancount-use-ido
  505. ;; `ido-completing-read' does not understand functional
  506. ;; completion tables thus directly build a list of the
  507. ;; accounts in the buffer
  508. (let ((beancount-accounts
  509. (sort (beancount-collect beancount-account-regexp 0) #'string<)))
  510. (ido-completing-read "Account: " beancount-accounts
  511. nil nil (thing-at-point 'word)))
  512. (completing-read "Account: " #'beancount-account-completion-table
  513. nil t (thing-at-point 'word)))))
  514. (let ((bounds (bounds-of-thing-at-point 'word)))
  515. (when bounds
  516. (delete-region (car bounds) (cdr bounds))))
  517. (insert account-name))
  518. (defmacro beancount-for-line-in-region (begin end &rest exprs)
  519. "Iterate over each line in region until an empty line is encountered."
  520. `(save-excursion
  521. (let ((end-marker (copy-marker ,end)))
  522. (goto-char ,begin)
  523. (beginning-of-line)
  524. (while (and (not (eobp)) (< (point) end-marker))
  525. (beginning-of-line)
  526. (progn ,@exprs)
  527. (forward-line 1)
  528. ))))
  529. (defun beancount-align-numbers (begin end &optional requested-currency-column)
  530. "Align all numbers in the given region. CURRENCY-COLUMN is the character
  531. at which to align the beginning of the amount's currency. If not specified, use
  532. the smallest columns that will align all the numbers. With a prefix argument,
  533. align with the fill-column."
  534. (interactive "r")
  535. ;; With a prefix argument, align with the fill-column.
  536. (when current-prefix-arg
  537. (setq requested-currency-column fill-column))
  538. ;; Loop once in the region to find the length of the longest string before the
  539. ;; number.
  540. (let (prefix-widths
  541. number-widths
  542. (number-padding " "))
  543. (beancount-for-line-in-region
  544. begin end
  545. (let ((line (thing-at-point 'line)))
  546. (when (string-match (concat "\\(.*?\\)"
  547. "[ \t]+"
  548. "\\(" beancount-number-regexp "\\)"
  549. "[ \t]+"
  550. beancount-currency-regexp)
  551. line)
  552. (push (length (match-string 1 line)) prefix-widths)
  553. (push (length (match-string 2 line)) number-widths)
  554. )))
  555. (when prefix-widths
  556. ;; Loop again to make the adjustments to the numbers.
  557. (let* ((number-width (apply 'max number-widths))
  558. (number-format (format "%%%ss" number-width))
  559. ;; Compute rightmost column of prefix.
  560. (max-prefix-width (apply 'max prefix-widths))
  561. (max-prefix-width
  562. (if requested-currency-column
  563. (max (- requested-currency-column (length number-padding) number-width 1)
  564. max-prefix-width)
  565. max-prefix-width))
  566. (prefix-format (format "%%-%ss" max-prefix-width))
  567. )
  568. (beancount-for-line-in-region
  569. begin end
  570. (let ((line (thing-at-point 'line)))
  571. (when (string-match (concat "^\\([^\"]*?\\)"
  572. "[ \t]+"
  573. "\\(" beancount-number-regexp "\\)"
  574. "[ \t]+"
  575. "\\(.*\\)$")
  576. line)
  577. (delete-region (line-beginning-position) (line-end-position))
  578. (let* ((prefix (match-string 1 line))
  579. (number (match-string 2 line))
  580. (rest (match-string 3 line)) )
  581. (insert (format prefix-format prefix))
  582. (insert number-padding)
  583. (insert (format number-format number))
  584. (insert " ")
  585. (insert rest)))))))))
  586. (defun beancount-align-to-previous-number ()
  587. "Align postings under the point's paragraph.
  588. This function looks for a posting in the previous transaction to
  589. determine the column at which to align the transaction, or otherwise
  590. the fill column, and align all the postings of this transaction to
  591. this column."
  592. (interactive)
  593. (let* ((begin (save-excursion
  594. (beancount-beginning-of-directive)
  595. (point)))
  596. (end (save-excursion
  597. (goto-char begin)
  598. (forward-paragraph 1)
  599. (point)))
  600. (currency-column (or (beancount-find-previous-alignment-column)
  601. fill-column)))
  602. (beancount-align-numbers begin end currency-column)))
  603. (defun beancount-beginning-of-directive ()
  604. "Move point to the beginning of the enclosed or preceding directive."
  605. (beginning-of-line)
  606. (while (and (> (point) (point-min))
  607. (not (looking-at
  608. "[0-9][0-9][0-9][0-9][\-/][0-9][0-9][\-/][0-9][0-9]")))
  609. (forward-line -1)))
  610. (defun beancount-find-previous-alignment-column ()
  611. "Find the preceding column to align amounts with.
  612. This is used to align transactions at the same column as that of
  613. the previous transaction in the file. This function merely finds
  614. what that column is and returns it (an integer)."
  615. ;; Go hunting for the last column with a suitable posting.
  616. (let (column)
  617. (save-excursion
  618. ;; Go to the beginning of the enclosing directive.
  619. (beancount-beginning-of-directive)
  620. (forward-line -1)
  621. ;; Find the last posting with an amount and a currency on it.
  622. (let ((posting-regexp (concat
  623. "\\s-+"
  624. beancount-account-regexp "\\s-+"
  625. beancount-number-regexp "\\s-+"
  626. "\\(" beancount-currency-regexp "\\)"))
  627. (balance-regexp (concat
  628. beancount-date-regexp "\\s-+"
  629. "balance" "\\s-+"
  630. beancount-account-regexp "\\s-+"
  631. beancount-number-regexp "\\s-+"
  632. "\\(" beancount-currency-regexp "\\)")))
  633. (while (and (> (point) (point-min))
  634. (not (or (looking-at posting-regexp)
  635. (looking-at balance-regexp))))
  636. (forward-line -1))
  637. (when (or (looking-at posting-regexp)
  638. (looking-at balance-regexp))
  639. (setq column (- (match-beginning 1) (point))))
  640. ))
  641. column))
  642. (defun beancount--account-currency (account)
  643. ;; Build a regexp that matches an open directive that specifies a
  644. ;; single account currencydaaee. The currency is match group 1.
  645. (let ((re (concat "^" beancount-date-regexp " +open"
  646. "\\s-+" (regexp-quote account)
  647. "\\s-+\\(" beancount-currency-regexp "\\)\\s-+")))
  648. (save-excursion
  649. (goto-char (point-min))
  650. (when (re-search-forward re nil t)
  651. ;; The account has declared a single currency, so we can fill it in.
  652. (match-string-no-properties 1)))))
  653. (defun beancount--electric-currency ()
  654. (when (and beancount-electric-currency (eq last-command-event ?\n))
  655. (save-excursion
  656. (forward-line -1)
  657. (when (and (beancount-inside-transaction-p)
  658. (looking-at (concat "\\s-+\\(" beancount-account-regexp "\\)"
  659. "\\s-+\\(" beancount-number-regexp "\\)\\s-*$")))
  660. ;; Last line is a posting without currency.
  661. (let* ((account (match-string 1))
  662. (pos (match-end 0))
  663. (currency (beancount--account-currency account)))
  664. (when currency
  665. (save-excursion
  666. (goto-char pos)
  667. (insert " " currency))))))))
  668. (defun beancount-insert-date ()
  669. "Start a new timestamped directive."
  670. (interactive)
  671. (unless (bolp) (newline))
  672. (insert (format-time-string "%Y-%m-%d") " "))
  673. (defvar beancount-install-dir nil
  674. "Directory in which Beancount's source is located.
  675. Only useful if you have not installed Beancount properly in your PATH.")
  676. (defvar beancount-check-program "bean-check"
  677. "Program to run to run just the parser and validator on an
  678. input file.")
  679. (defvar compilation-read-command)
  680. (defun beancount--run (prog &rest args)
  681. (let ((process-environment
  682. (if beancount-install-dir
  683. `(,(concat "PYTHONPATH=" beancount-install-dir)
  684. ,(concat "PATH="
  685. (expand-file-name "bin" beancount-install-dir)
  686. ":"
  687. (getenv "PATH"))
  688. ,@process-environment)
  689. process-environment))
  690. (compile-command (mapconcat (lambda (arg)
  691. (if (stringp arg)
  692. (shell-quote-argument arg) ""))
  693. (cons prog args)
  694. " ")))
  695. (call-interactively 'compile)))
  696. (defun beancount-check ()
  697. "Run `beancount-check-program'."
  698. (interactive)
  699. (let ((compilation-read-command nil))
  700. (beancount--run beancount-check-program
  701. (file-relative-name buffer-file-name))))
  702. (defvar beancount-query-program "bean-query"
  703. "Program to run to run just the parser and validator on an
  704. input file.")
  705. (defun beancount-query ()
  706. "Run bean-query."
  707. (interactive)
  708. ;; Don't let-bind compilation-read-command this time, since the default
  709. ;; command is incomplete.
  710. (beancount--run beancount-query-program
  711. (file-relative-name buffer-file-name) t))
  712. (defvar beancount-doctor-program "bean-doctor"
  713. "Program to run the doctor commands.")
  714. (defun beancount-context ()
  715. "Get the \"context\" from `beancount-doctor-program'."
  716. (interactive)
  717. (let ((compilation-read-command nil))
  718. (beancount--run beancount-doctor-program "context"
  719. (file-relative-name buffer-file-name)
  720. (number-to-string (line-number-at-pos)))))
  721. (defun beancount-linked ()
  722. "Get the \"linked\" info from `beancount-doctor-program'."
  723. (interactive)
  724. (let ((compilation-read-command nil))
  725. (beancount--run beancount-doctor-program "linked"
  726. (file-relative-name buffer-file-name)
  727. (number-to-string (line-number-at-pos)))))
  728. (defvar beancount-price-program "bean-price"
  729. "Program to run the price fetching commands.")
  730. (defun beancount-insert-prices ()
  731. "Run bean-price on the current file and insert the output inline."
  732. (interactive)
  733. (call-process beancount-price-program nil t nil
  734. (file-relative-name buffer-file-name)))
  735. ;;; Transaction highligh
  736. (defvar beancount-highlight-overlay (list))
  737. (make-variable-buffer-local 'beancount-highlight-overlay)
  738. (defun beancount-highlight-overlay-make ()
  739. (let ((overlay (make-overlay 1 1)))
  740. (overlay-put overlay 'face 'beancount-highlight)
  741. (overlay-put overlay 'priority '(nil . 99))
  742. overlay))
  743. (defun beancount-highlight-transaction-at-point ()
  744. "Move the highlight overlay to the current transaction."
  745. (when beancount-highlight-transaction-at-point
  746. (unless beancount-highlight-overlay
  747. (setq beancount-highlight-overlay (beancount-highlight-overlay-make)))
  748. (let* ((bounds (beancount-find-transaction-extents (point)))
  749. (begin (car bounds))
  750. (end (cadr bounds)))
  751. (if (> (- end begin) 0)
  752. (move-overlay beancount-highlight-overlay begin end)
  753. (move-overlay beancount-highlight-overlay 1 1)))))
  754. ;;; Outline minor mode support.
  755. (defun beancount-outline-cycle (&optional arg)
  756. "Implement visibility cycling a la `org-mode'.
  757. The behavior of this command is determined by the first matching
  758. condition among the following:
  759. 1. When point is at the beginning of the buffer, or when called
  760. with a `\\[universal-argument]' universal argument, rotate the entire buffer
  761. through 3 states:
  762. - OVERVIEW: Show only top-level headlines.
  763. - CONTENTS: Show all headlines of all levels, but no body text.
  764. - SHOW ALL: Show everything.
  765. 2. When point is at the beginning of a headline, rotate the
  766. subtree starting at this line through 3 different states:
  767. - FOLDED: Only the main headline is shown.
  768. - CHILDREN: The main headline and its direct children are shown.
  769. From this state, you can move to one of the children
  770. and zoom in further.
  771. - SUBTREE: Show the entire subtree, including body text."
  772. (interactive "P")
  773. (setq deactivate-mark t)
  774. (cond
  775. ;; Beginning of buffer or called with C-u: Global cycling
  776. ((or (equal arg '(4))
  777. (and (bobp)
  778. ;; org-mode style behaviour - only cycle if not on a heading
  779. (not (outline-on-heading-p))))
  780. (beancount-cycle-buffer))
  781. ;; At a heading: rotate between three different views
  782. ((save-excursion (beginning-of-line 1) (looking-at outline-regexp))
  783. (outline-back-to-heading)
  784. (let ((goal-column 0) eoh eol eos)
  785. ;; First, some boundaries
  786. (save-excursion
  787. (save-excursion (beancount-next-line) (setq eol (point)))
  788. (outline-end-of-heading) (setq eoh (point))
  789. (outline-end-of-subtree) (setq eos (point)))
  790. ;; Find out what to do next and set `this-command'
  791. (cond
  792. ((= eos eoh)
  793. ;; Nothing is hidden behind this heading
  794. (beancount-message "EMPTY ENTRY"))
  795. ((>= eol eos)
  796. ;; Entire subtree is hidden in one line: open it
  797. (outline-show-entry)
  798. (outline-show-children)
  799. (beancount-message "CHILDREN")
  800. (setq
  801. this-command 'beancount-cycle-children))
  802. ((eq last-command 'beancount-cycle-children)
  803. ;; We just showed the children, now show everything.
  804. (outline-show-subtree)
  805. (beancount-message "SUBTREE"))
  806. (t
  807. ;; Default action: hide the subtree.
  808. (outline-hide-subtree)
  809. (beancount-message "FOLDED")))))))
  810. (defvar beancount-current-buffer-visibility-state nil
  811. "Current visibility state of buffer.")
  812. (make-variable-buffer-local 'beancount-current-buffer-visibility-state)
  813. (defvar beancount-current-buffer-visibility-state)
  814. (defun beancount-cycle-buffer (&optional arg)
  815. "Rotate the visibility state of the buffer through 3 states:
  816. - OVERVIEW: Show only top-level headlines.
  817. - CONTENTS: Show all headlines of all levels, but no body text.
  818. - SHOW ALL: Show everything.
  819. With a numeric prefix ARG, show all headlines up to that level."
  820. (interactive "P")
  821. (save-excursion
  822. (cond
  823. ((integerp arg)
  824. (outline-show-all)
  825. (outline-hide-sublevels arg))
  826. ((eq last-command 'beancount-cycle-overview)
  827. ;; We just created the overview - now do table of contents
  828. ;; This can be slow in very large buffers, so indicate action
  829. ;; Visit all headings and show their offspring
  830. (goto-char (point-max))
  831. (while (not (bobp))
  832. (condition-case nil
  833. (progn
  834. (outline-previous-visible-heading 1)
  835. (outline-show-branches))
  836. (error (goto-char (point-min)))))
  837. (beancount-message "CONTENTS")
  838. (setq this-command 'beancount-cycle-toc
  839. beancount-current-buffer-visibility-state 'contents))
  840. ((eq last-command 'beancount-cycle-toc)
  841. ;; We just showed the table of contents - now show everything
  842. (outline-show-all)
  843. (beancount-message "SHOW ALL")
  844. (setq this-command 'beancount-cycle-showall
  845. beancount-current-buffer-visibility-state 'all))
  846. (t
  847. ;; Default action: go to overview
  848. (let ((toplevel
  849. (cond
  850. (current-prefix-arg
  851. (prefix-numeric-value current-prefix-arg))
  852. ((save-excursion
  853. (beginning-of-line)
  854. (looking-at outline-regexp))
  855. (max 1 (funcall outline-level)))
  856. (t 1))))
  857. (outline-hide-sublevels toplevel))
  858. (beancount-message "OVERVIEW")
  859. (setq this-command 'beancount-cycle-overview
  860. beancount-current-buffer-visibility-state 'overview)))))
  861. (defun beancount-message (msg)
  862. "Display MSG, but avoid logging it in the *Messages* buffer."
  863. (let ((message-log-max nil))
  864. (message msg)))
  865. (defun beancount-next-line ()
  866. "Forward line, but mover over invisible line ends.
  867. Essentially a much simplified version of `next-line'."
  868. (interactive)
  869. (beginning-of-line 2)
  870. (while (and (not (eobp))
  871. (get-char-property (1- (point)) 'invisible))
  872. (beginning-of-line 2)))
  873. (provide 'beancount)
  874. ;;; beancount.el ends here