You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

979 lines
37 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. ;;; package --- Summary
  2. ;;; Commentary:
  3. ;; A humble try to port leder-mode to beancount
  4. ;;; Code:
  5. ;(require 'beancount-regex)
  6. (require 'font-lock)
  7. (require 'company) ; for company-mode
  8. (require 'pcomplete)
  9. (require 'cl-lib)
  10. (require 'subr-x) ; for hash-table-keys
  11. (defgroup beancount ()
  12. "Editing mode for beancount files."
  13. :group 'beancount)
  14. (defconst beancount-timestamped-directive-names
  15. '("balance"
  16. "open"
  17. "close"
  18. "pad"
  19. "document"
  20. "note"
  21. ;; the ones below are not followed by an account name.
  22. "event"
  23. "price"
  24. "commodity"
  25. "query"
  26. "txn"
  27. "*")
  28. "Directive names that can appear after a date.")
  29. (defconst beancount-account-categories
  30. '("Assets"
  31. "Liabilities"
  32. "Equity"
  33. "Income"
  34. "Expenses"
  35. ; TODO: Fill changed root accounts automatically
  36. "Aktiva"
  37. "Verbindlichkeiten"
  38. "Eigenkapital"
  39. "Ertraege"
  40. "Aufwendungen"))
  41. (defconst beancount-nontimestamped-directive-names
  42. '("pushtag"
  43. "poptag"
  44. "option"
  45. "include"
  46. "plugin")
  47. "Directive names that can appear after a date.")
  48. (defconst beancount-option-names
  49. ;; this list has to be kept in sync with the options definied in
  50. ;; beancount/parser/options.py
  51. '("title"
  52. "name_assets"
  53. "name_equity"
  54. "name_income"
  55. "name_expenses"
  56. "bookin_algorithm"
  57. "bookin_method"
  58. "account_previous_balances"
  59. "account_previous_earnings"
  60. "account_previous_conversions"
  61. "account_current_earnings"
  62. "account_current_conversions"
  63. "account_rounding"
  64. "conversion_currency"
  65. "inferred_tolerance_default"
  66. "inferred_tolerance_multiplier"
  67. "infer_tolerance_from_cost"
  68. "documents"
  69. "operating_currency"
  70. "render_commas"
  71. "plugin_processing_mode"
  72. "plugin"
  73. "long_string_maxlines"
  74. ))
  75. (defconst beancount-directive-names
  76. (append beancount-nontimestamped-directive-names
  77. beancount-timestamped-directive-names)
  78. "A list of directive names.")
  79. (defconst beancount-tag-chars
  80. "[:alnum:]-_/."
  81. "Allowed tag characters.")
  82. (defconst beancount-account-chars
  83. "[:alnum:]-_:"
  84. "Allowed account characters.")
  85. ;beancount-account-regexp original
  86. ;"\\(?:A\\(?:ktiva\\|ssets\\|ufwendungen\\)\\|E\\(?:igenkapital\\|quity\\|rtraege\\|xpenses\\)\\|Income\\|Liabilities\\|Verbindlichkeiten\\)\\(?::[[:upper:]][[:alnum:]-_]+\\)+"
  87. ;"\\(?:A\\(?:ktiva\\|ssets\\|ufwendungen\\)\\|E\\(?:igenkapital\\|quity\\|rtraege\\|xpenses\\)\\|Income\\|Liabilities\\|Verbindlichkeiten\\)\\(?::[[:upper:]][[:alnum:]-_]+\\)+"
  88. ; ^[\s\{2\}][Aufwendungen\|Aktiva\[:[:alnum:]-_]+
  89. (defconst beancount-account-regexp
  90. "^[ ][ ][a-z]+"
  91. ; (concat (regexp-opt beancount-account-categories)
  92. ;; (rx line-start (seq space
  93. ;; space
  94. ;; (or (literal (regexp-opt beancount-account-categories)))
  95. ;; ; (or "Aufwendungen" "Ertraege")
  96. ;; (zero-or-more (seq ":"
  97. ;; (one-or-more (any alnum "-_"))))))
  98. ; (concat (regexp-opt beancount-account-categories)
  99. ; "\\(?::[[:upper:]][" beancount-account-chars "]+\\)")
  100. "A regular expression to match account names.")
  101. ; TODO: currently shows all texts between ""
  102. (defconst beancount-payee-regexp
  103. "\"\\(.*?\\)\"")
  104. (defconst beancount-date-regexp
  105. "^[12][901][0-9]\\{2\\}-\\(0[1-9]\\|1[012]\\)-\\([012][0-9]\\|3[01]\\)"
  106. ;"\\(^[12][901][0-9]\\{2\\}-\\(0[1-9]\\|1[012]\\)-\\([012][0-9]\\|3[01]\\)\\)"
  107. ;"\\(^[12][901][0-9]\\{2\\}-\\(0[1-9]\\|1[012]\\)-\\(\\([012][0-9]\\)\\(\\|3[01]\\)\\)\\)"
  108. ; "^[12][901][0-9]\\{2\\}-0[1-9]1[012]-[012][0-9]3[01]"
  109. ; "^[12][901][0-9]\\{2\\}-\\(\\(0[1-9]\\)\\|\\(1[012]\\)\\)-\\(\\([012][0-9]\\)\\|\\(3[01]\\)\\)"
  110. "Regular expression for dates.")
  111. (defconst beancount-number-regexp
  112. "[-+]?[0-9,]+\\(?:\\.[0-9]*\\)"
  113. "Regular expression to match decimal numbers.")
  114. (defconst beancount-tag-regexp
  115. (concat "#"
  116. "["
  117. beancount-tag-chars
  118. "]+")
  119. "Regular expression for valid tags.")
  120. (defconst beancount-currency-regexp
  121. "[A-Z][A-Z-_'.]*"
  122. "Regular expression to match currencies.")
  123. (defconst beancount-timestamped-accounts-regexp
  124. (concat beancount-date-regexp
  125. " " ;"\\(\\s-+\\)"
  126. (regexp-opt beancount-timestamped-directive-names)
  127. " ") ;"\\(\\s-+\\)")
  128. "A regular expression to match valid preceding characters before an account name.")
  129. (defconst beancount-amount-and-currency-regex
  130. "\s-*[-]?[0-9,]+[.][0-9]\\{2\\}\s[A-Za-z0-9.]+"
  131. "A regular expression for amounts including currency.")
  132. (defconst beancount-payee-any-status-regex
  133. "^[0-9]+[-/][-/.=0-9]+\\(\\s-+\\*\\)?\\(\\s-+(.*?)\\)?\\s-+\\(.+?\\)\\s-*\\(;\\|$\\)")
  134. (defconst beancount-date-and-status-regex
  135. (concat beancount-date-regexp
  136. "\\(\\s-+[\\*!]\\)")
  137. "Returns true for YYYY-MM-DD ! and YYYY-MM-DD *.")
  138. (defconst beancount-valid-prefix-for-directives-regex
  139. (concat "^"
  140. beancount-date-regexp
  141. "[ ]+"
  142. "[a-z]*$"))
  143. (defconst beancount-prefix-for-payee-regex
  144. (concat beancount-date-and-status-regex
  145. " \"" ; space and opening quotes
  146. "\\(?1:" ; opening term, calling it segment 1
  147. "[^\"\n]*" ; any number of chars except quotes and newline
  148. "\\)" ; closing term
  149. ))
  150. (defconst beancount-prefix-for-payeetext-regex
  151. (concat beancount-date-and-status-regex
  152. "\\s-+\"" ; space and opening quotes
  153. "[^\"\n]*" ; any number of chars except quotes and newline
  154. "\"" ; closing quotes
  155. "\\s-+\"" ; space and opening quotes
  156. "\\(?1:" ; opening term, calling it segment 1
  157. "[^\"\n]*" ; any number of chars except quotes and newline
  158. "\\)" ; closing term
  159. ))
  160. (defconst beancount-valid-prefix-for-payee-completion-regex
  161. (concat "\\("
  162. beancount-date-and-status-regex
  163. " \"" ; empty space and open quotes
  164. "[^\"\n]*" ; any number of chars except quotes and newline
  165. "\\(" ; start of optional second quoted term
  166. "\"[ ]+\"" ; closing quotes, whitespaces, opening quotes
  167. "[^\"\n]*" ; any number of chars except quotes and newline
  168. "\\)?" ; end of optional second quoted term
  169. "\\)$" ; the whole regex looks from the right side of the line
  170. ))
  171. ; "^[12][901][0-9]\\{2\\}-\\(0[1-9]\\|1[012]\\)-\\([012][0-9]\\|3[01]\\)\\(\\s-+[\\*!]\\)\\s-+\"[^\"]*\"\\s-+\"[^\"]*\"\\(?1:\\s-+#\\)"
  172. ; "^[12][901][0-9]\\{2\\}-\\(0[1-9]\\|1[012]\\)-\\([012][0-9]\\|3[01]\\)\\(\\s-+[\\*!]\\)\\s-+\"[^\"\n]*\"\\s-+\"[^\"\n]*\"\\(?1:\\s-+#\\)"
  173. (defconst beancount-prefix-for-tag-completion-regex
  174. (concat ;"\\("
  175. beancount-date-and-status-regex
  176. "\\s-+\"" ; empty space and open quotes
  177. "[^\"\n]*" ; any number of chars except quotes and newline
  178. ; "\\(" ; start of optional second quoted term
  179. "\"\\s-+\"" ; closing quotes, whitespaces, opening quotes
  180. "[^\"\n]*" ; any number of chars except quotes and newline
  181. "\"" ; end of optional second quoted term
  182. "\\s-+#"
  183. "\\(?1:"
  184. "[^\"\n]*"
  185. "\\)"
  186. ;"\\)$" ; the whole regex looks from the right side of the line
  187. ))
  188. (defconst beancount-comments-regex
  189. (concat ";[^\"\n]*$")) ; right part of the line after a comment symbol if no quote or newline is included
  190. (defconst beancount-empty-line-regex
  191. "^\\(\\s-+\\)" ;; maybe "^[ \t]+" is better
  192. "Returns true for preceding whitespaces.")
  193. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  194. ;; Faces
  195. (defgroup beancount-faces nil "Beancount mode highlighting" :group 'beancount)
  196. (defface beancount-font-default-face
  197. `((t :inherit default))
  198. "Default face"
  199. :group 'beancount-faces)
  200. (defface beancount-font-xact-cleared-face
  201. `((t :foreground "#AAAAAA" :weight normal))
  202. "Default face for cleared transactions"
  203. :group 'beancount-faces)
  204. (defface beancount-font-xact-pending-face
  205. `((t :foreground "#dc322f" :weight bold))
  206. "Default face for pending transactions"
  207. :group 'beancount-faces)
  208. (defface beancount-font-payee-cleared-face
  209. `((t :inherit beancount-font-other-face))
  210. "Default face for pending transactions"
  211. :group 'beancount-faces)
  212. (defface beancount-font-payee-pending-face
  213. `((t :foreground "#f24b61" :weight normal))
  214. "Default face for pending (!) transactions"
  215. :group 'beancount-faces)
  216. (defface beancount-font-other-face
  217. `((t :foreground "#657b83" :weight normal))
  218. "Default face for other transactions"
  219. :group 'beancount-faces)
  220. (defface beancount-font-posting-date-face
  221. `((t :foreground "#cb4b16" :weight normal))
  222. "Default face for dates"
  223. :group 'beancount-faces)
  224. (defface beancount-font-amount-face
  225. `((t :foreground "#cb4b16" :weight normal))
  226. "Default face for amounts"
  227. :group 'beancount-faces)
  228. (defvar beancount-font-lock-directives
  229. `(;; reserved keywords
  230. (,(concat beancount-date-regexp
  231. "\\s-+\\(?1:"
  232. (regexp-opt beancount-directive-names)
  233. "\\)")
  234. . (1 font-lock-keyword-face)) ; use group one of regex search for highlighting, not the whole result
  235. ;; tags & links
  236. ("[#\\^][A-Za-z0-9\-_/.]+" . font-lock-type-face)
  237. ;; comments
  238. (,beancount-comments-regex (0 font-lock-comment-face))
  239. ;; date
  240. (,beancount-date-regexp . 'beancount-font-posting-date-face)
  241. ;; account
  242. (,beancount-account-regexp . 'beancount-font-other-face)
  243. ;; payees
  244. ("\"\\(.*?\\)\"" . font-lock-comment-face)
  245. ;; txn warning
  246. ("! " . font-lock-warning-face)
  247. ))
  248. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  249. ;; Alignments
  250. (defmacro beancount-for-line-in-region (begin end &rest exprs)
  251. "Iterate over each line in region from BEGIN to END (EXPRS)
  252. until an empty line is encountered."
  253. `(save-excursion
  254. (let ((end-marker (copy-marker ,end)))
  255. (goto-char ,begin)
  256. (beginning-of-line)
  257. (while (and (not (eobp)) (< (point) end-marker))
  258. (beginning-of-line)
  259. (progn ,@exprs)
  260. (forward-line 1)
  261. ))))
  262. (defun beancount-align-numbers (begin end)
  263. "Align all numbers in the current buffer from BEGIN to END."
  264. (interactive "r")
  265. ;; loop once in the buffer to find the length of the longest string before the
  266. ;; number.
  267. (let (prefix-widths
  268. number-widths
  269. (number-padding " "))
  270. (beancount-for-line-in-region
  271. begin end
  272. (let ((line (thing-at-point 'line)))
  273. (when (string-match (concat "\\(.*?\\)"
  274. "[ \t]+"
  275. "\\(" beancount-number-regexp "\\)"
  276. "[ \t]+"
  277. beancount-currency-regexp)
  278. line)
  279. (push (length (match-string 1 line)) prefix-widths)
  280. (push (length (match-string 2 line)) number-widths)
  281. )))
  282. (when prefix-widths
  283. ;; Loop again to make the adjustments to the numbers.
  284. (let* ((number-width (apply 'max number-widths))
  285. (number-format (format "%%%ss" number-width))
  286. ;; compute the rightmost column of prefix
  287. (max-prefix-width (apply 'max prefix-widths))
  288. (prefix-format (format "%%-%ss" max-prefix-width))
  289. )
  290. (beancount-for-line-in-region
  291. begin end
  292. (let ((line (thing-at-point 'line)))
  293. (when (string-match (concat "^\\([^\"]*?\\)"
  294. "[ \t]+"
  295. "\\(" beancount-number-regexp "\\)"
  296. "[ \t]+"
  297. "\\(.*\\)$")
  298. line)
  299. (delete-region (line-beginning-position) (line-end-position))
  300. (let* ((prefix (match-string 1 line))
  301. (number (match-string 2 line))
  302. (rest (match-string 3 line)))
  303. (insert (format prefix-format prefix))
  304. (insert number-padding)
  305. (insert (format number-format number))
  306. (insert " ")
  307. (insert rest)))))))))
  308. (defun beancount-hash-keys (hashtable)
  309. "Extract all the keys of the given HASHTABLE. Return a sorted list."
  310. (let (rlist)
  311. (maphash (lambda (k _v) (push k rlist)) hashtable)
  312. (sort rlist 'string<)))
  313. (defvar beancount-accounts nil
  314. "A list of the accounts available in this buffer.
  315. This is a cache of the value computed by `beancount-get-accounts'.")
  316. (make-variable-buffer-local 'beancount-accounts)
  317. (defun beancount-init-accounts ()
  318. "Initialize or reset the list of accounts."
  319. (interactive)
  320. (setq beancount-accounts (beancount-get-accounts-in-buffer)) ;-new
  321. (pcomplete-uniqify-list (nreverse beancount-accounts))
  322. ;; (setq beancount-accounts (beancount-get-accounts))
  323. (message "Accounts updated."))
  324. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  325. ;; Navigation
  326. (defun beancount-navigate-start-xact-or-directive-p ()
  327. "Return t if at the beginning of an empty or all-whitespace line."
  328. (not (looking-at "[ \t]\\|\\(^$\\)")))
  329. (defun beancount-navigate-prev-xact-or-directive ()
  330. "Move to the beginning of the next xact or directive."
  331. (interactive)
  332. (beginning-of-line)
  333. (if (beancount-navigate-start-xact-or-directive-p) ;if we are at the start of an xact, move backward to the previous xact
  334. (progn
  335. (forward-line -1)
  336. (if (not (beancount-navigate-start-xact-or-directive-p)) ; we have moved backward and are not at another xact, recurse backward
  337. (beancount-navigate-prev-xact-or-directive)))
  338. (while (not (or (bobp)
  339. (beancount-navigate-start-xact-or-directive-p)))
  340. (forward-line -1))))
  341. (defun beancount-navigate-next-xact-or-directive ()
  342. "Move to the beginning of the next xact or directive."
  343. (interactive)
  344. (beginning-of-line)
  345. (if (beancount-navigate-start-xact-or-directive-p) ; if we are at the start of an xact, move forward to the next xact
  346. (progn
  347. (forward-line)
  348. (if (not (beancount-navigate-start-xact-or-directive-p)) ; we have moved forward and are not at another xact, recurse forward
  349. (beancount-navigate-next-xact-or-directive)))
  350. (while (not (or (eobp) ; we didn't stsrt off at the beginning of an xact
  351. (beancount-navigate-start-xact-or-directive-p)))
  352. (forward-line))))
  353. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  354. ;; Completion
  355. (defun beancount-thing-at-point ()
  356. "Describe thing at points. Return 'transaction, 'posting, or nil.
  357. Leave point at the beginning of the thing under point.")
  358. (defun beancount-trim-trailing-whitespace (str)
  359. "Replace trailing whitespaces in STR."
  360. (replace-regexp-in-string "[ \t]*$" "" str))
  361. ;; (defun beancount-fully-complete-xact ()
  362. ;; "Completes a transaction if there is another matching payee in the buffer."
  363. ;; (interactive)
  364. ;; (let* ((name (beancount-trim-trailing-whitespace (caar (beancount-parse-arguments))))
  365. ;; xacf)))
  366. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  367. ;; Functions for external programs
  368. (defvar beancount-filename-main buffer-file-name
  369. "File name of the main beancount file for beancount-check.")
  370. (defvar beancount-terminal-name "kitty"
  371. "Name of the terminal emulator to run fava.")
  372. (defvar beancount-fava-exec "/bin/fava"
  373. "Full path of fava executable.")
  374. (defvar beancount-install-dir nil
  375. "Directory in which Beancount's source is located.
  376. Only useful if you have not installed Beancount properly in your PATH")
  377. (defun beancount--run (prog &rest args)
  378. "Random text PROG ARGS."
  379. (let ((process-environment
  380. (if beancount-install-dir
  381. `(,(concat "PYTHONPATH=" beancount-install-dir)
  382. ,(concat "PATH="
  383. (expand-file-name "bin" beancount-install-dir)
  384. ":"
  385. (getenv "PATH"))
  386. ,@process-environment)
  387. process-environment))
  388. (compile-command (mapconcat (lambda (arg)
  389. (if (stringp arg)
  390. (shell-quote-argument arg) ""))
  391. (cons prog args)
  392. " ")))
  393. (call-interactively 'compile)))
  394. (defvar beancount-check-program "bean-check"
  395. "Program to run the parser and validator on an input file.")
  396. (defun beancount-check ()
  397. "Run `beancount-check-program'."
  398. (interactive)
  399. (let ((compilation-read-command nil))
  400. (beancount--run beancount-check-program beancount-filename-main)))
  401. ; (file-relative-name buffer-file-name))))
  402. (defun beancount-fava ()
  403. "Run `beancount-fava' and open the URL in the default browser."
  404. (interactive)
  405. (start-process "termx" nil beancount-terminal-name "-e" beancount-fava-exec beancount-filename-main)
  406. (sleep-for 0.5) ; necessary to prevent an error
  407. (browse-url "127.0.0.1:5000"))
  408. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  409. ;; Completions
  410. (defun beancount-get-accounts-in-buffer ()
  411. "Return a list of all accounts."
  412. (let ((origin (point))
  413. accounts-list)
  414. (save-excursion
  415. (goto-char (point-min))
  416. (while (re-search-forward
  417. beancount-account-regexp nil t)
  418. (setq accounts-list (cons (match-string-no-properties 0)
  419. accounts-list))))
  420. (pcomplete-uniquify-list (nreverse accounts-list))))
  421. (defun beancount-get-payees-in-buffer ()
  422. "Return a list of all payees."
  423. (let ((origin (point))
  424. payees-list)
  425. (save-excursion
  426. (goto-char (point-min))
  427. (while (re-search-forward
  428. "^[0-9- *]*\"\\(?1:.*?\\)\" \"\\(?2:.*?\\)\"" nil t) ; matches are in brackets
  429. (setq payees-list (cons (match-string-no-properties 1) ; get first match, generally the payee
  430. payees-list))))
  431. ; (setq payees-list (cons (match-string-no-properties 2) ; get second match, generally a description
  432. ; payees-list))))
  433. (pcomplete-uniquify-list (nreverse payees-list))))
  434. (defun beancount-get-payee-text-in-buffer ()
  435. "Return a list of all payees."
  436. (let ((origin (point))
  437. payees-text-list)
  438. (save-excursion
  439. (goto-char (point-min))
  440. (while (re-search-forward
  441. "^[0-9- *]*\"\\(?1:.*?\\)\" \"\\(?2:.*?\\)\"" nil t) ; matches are in brackets
  442. (setq payees-text-list (cons (match-string-no-properties 2) ; get second match, generally a description
  443. payees-text-list))))
  444. (pcomplete-uniquify-list (nreverse payees-text-list))))
  445. (defun beancount-get-tags-in-buffer ()
  446. "Return a list of all tags."
  447. (let ((origin (point))
  448. tags-list)
  449. (save-excursion
  450. (goto-char (point-min))
  451. (while (re-search-forward
  452. beancount-tag-regexp nil t)
  453. (setq tags-list (cons (substring (match-string-no-properties 0) 1)
  454. tags-list))))
  455. (pcomplete-uniquify-list (nreverse tags-list))))
  456. (defun beancount-get-commodities-in-buffer ()
  457. "Return a list of all commodities / currencies."
  458. (let ((origin (point))
  459. commodities-list)
  460. (save-excursion
  461. (goto-char (point-min))
  462. (while (re-search-forward
  463. "^[ ]+[A-Za-z0-9-_:]+[ ]+[-]?[0-9,.]+[ ]+\\([A-Z][A-Z0-9-_.']*[A-Z0-9]\\)" nil t)
  464. (setq commodities-list (cons (match-string-no-properties 1)
  465. commodities-list))))
  466. (pcomplete-uniquify-list (nreverse commodities-list))))
  467. (defun beancount-company--regexp-filter (regexp list)
  468. "Use REGEXP to filter LIST of strings."
  469. (let (new)
  470. (dolist (string list)
  471. (when (string-match regexp string)
  472. (setq new (cons string new))))
  473. ;;new
  474. (setq new (nreverse new))
  475. ; (setq new (reverse new))
  476. new))
  477. (defun beancount-get-transactions-in-buffer ()
  478. "Return a list of all transactions."
  479. (beancount-company--regexp-filter
  480. "[0-9][0-9][0-9][0-9][-/][0-9][0-9][-/][0-9][0-9]"
  481. (mapcar (lambda (s) (substring s 1))
  482. (split-string (buffer-string) "^$" t))))
  483. (defun beancount-company--fuzzy-word-match (prefix candidate)
  484. "Return non-nil if each (partial) word in PREFIX is also in CANDIDATE."
  485. (eq nil
  486. (memq nil
  487. (mapcar
  488. (lambda (pre) (string-match-p (regexp-quote pre) candidate))
  489. (split-string prefix)))))
  490. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  491. ;; Finishing setup
  492. (defvar beancount-mode-map
  493. (let ((map (make-sparse-keymap)))
  494. (define-key map [(meta ?p)] #'beancount-navigate-prev-xact-or-directive)
  495. (define-key map [(meta ?n)] #'beancount-navigate-next-xact-or-directive)
  496. map)
  497. "Keymap for `bean-mode'.")
  498. (easy-menu-define beancount-mode-menu beancount-mode-map
  499. "Beancount menu"
  500. '("Beancount"
  501. ["Customize Ledger Mode" (lambda () (interactive) (customize-group 'beanmode))]))
  502. ;; (defun beancount-get-accounts (&optional string)
  503. ;; "Return list of account names with STRING infix present.
  504. ;; STRING can be multiple words separated by a space."
  505. ;; (let* ((accounts-string (shell-command-to-string
  506. ;; (concat "bean-query -q -f csv "
  507. ;; "/home/marc/Archiv/Finanzen/transactions_ingdiba.beancount "
  508. ;; "'SELECT DISTINCT account ORDER BY ACCOUNT ASC'")))
  509. ;; (accounts-list (split-string accounts-string)))
  510. ;; accounts-list))
  511. (defvar beancount-accounts-cache nil
  512. "List of accounts cached for capf.")
  513. (defvar beancount-payees-cache nil
  514. "List of payees cached for capf.")
  515. (defvar beancount-payeetext-cache nil
  516. "List of payee texts cached for capf.")
  517. (defvar beancount-tags-cache nil
  518. "List of tags cached for capf.")
  519. (defvar beancount-commodities-cache nil
  520. "List of commodities / currencies cached for capf.")
  521. (defun beancount-update-payees-text ()
  522. "Get list of all payees text in current buffer."
  523. (interactive)
  524. (setq beancount-payeetext-cache (beancount-get-payee-text-in-buffer)) ;-new
  525. (pcomplete-uniquify-list (nreverse beancount-payeetext-cache)))
  526. (defun beancount-update-accounts-and-payees ()
  527. "Initialize or reset the list of accounts."
  528. (interactive)
  529. (setq beancount-accounts-cache (beancount-get-accounts-in-buffer)) ;-new
  530. (pcomplete-uniquify-list (nreverse beancount-accounts-cache))
  531. (message "Accounts updated.")
  532. (setq beancount-payees-cache (beancount-get-payees-in-buffer)) ;-new
  533. (pcomplete-uniquify-list (nreverse beancount-payees-cache))
  534. (message "Payees updated.")
  535. (setq beancount-payeetext-cache (beancount-get-payee-text-in-buffer)) ;-new
  536. (pcomplete-uniquify-list (nreverse beancount-payeetext-cache))
  537. (message "Payee texts updated.")
  538. (setq beancount-commodities-cache (beancount-get-commodities-in-buffer))
  539. (pcomplete-uniquify-list (nreverse beancount-commodities-cache))
  540. (setq beancount-tags-cache (beancount-get-tags-in-buffer))
  541. (pcomplete-uniquify-list (nreverse beancount-tags-cache))
  542. (message "Tags updated."))
  543. ;; first test for conditional completions
  544. ;; e.g. only account names when completion is after a date
  545. ;; (defun beancount-company-candidates (prefix)
  546. ;; "Check what PREFIX does."
  547. ;; (let* (accounts (beancount-accounts-cache))
  548. ;; accounts))
  549. ;;##################################
  550. ;;stuff from https://github.com/debanjum/company-ledger/blob/master/company-ledger.el
  551. (defun company-ledger--regexp-filter (regexp list)
  552. "Use REGEXP to filter LIST of strings."
  553. (let (new)
  554. (dolist (string list)
  555. (when (string-match regexp string)
  556. (setq new (cons string new))))
  557. new))
  558. (defun company-ledger--get-all-postings ()
  559. "Get all paragraphs in buffer containing YYYY[-/]MM[-/]DD in them."
  560. (company-ledger--regexp-filter
  561. "[0-9][0-9][0-9][0-9][-/][0-9][0-9][-/][0-9][0-9]"
  562. (mapcar (lambda (s) (substring s 1))
  563. (split-string (buffer-string) "^$" t))))
  564. (defun company-ledger--fuzzy-word-match (prefix candidate)
  565. "Return non-nil if each (partial) word in PREFIX is also in CANDIDATE."
  566. (eq nil
  567. (memq nil
  568. (mapcar
  569. (lambda (pre) (string-match-p (regexp-quote pre) candidate))
  570. (split-string prefix)))))
  571. (defun company-ledger--next-line-empty-p ()
  572. "Return non-nil if next line empty else false."
  573. (save-excursion
  574. (beginning-of-line)
  575. (forward-line 1)
  576. (or (looking-at "[[:space:]]*$")
  577. (eolp)
  578. (eobp))))
  579. ;; hier werden Vorschläge zeitlich rückwärts gemacht (jüngste Einträge zuerst)!
  580. ;;;###autoload
  581. (defun company-beancountGIT (command &optional arg &rest ignored)
  582. "Fuzzy company back-end for ledger, beancount and other ledger-like modes.
  583. Provide completion info based on COMMAND and ARG. IGNORED, not used."
  584. (interactive (list 'interactive))
  585. (cl-case command
  586. (interactive (company-begin-backend 'company-beancount))
  587. (`interactive (company-begin-backend 'company-beancount))
  588. (`prefix (and (eq major-mode 'beancount-mode)
  589. (company-grab-symbol)))
  590. ; (prefix (and (or (bound-and-true-p beancount-mode)
  591. ; (derived-mode-p 'ledger-mode))
  592. ; (company-ledger--next-line-empty-p)
  593. ; (thing-at-point 'line t)))
  594. (candidates
  595. (cl-remove-if-not
  596. (lambda (c) (company-ledger--fuzzy-word-match arg c))
  597. (company-ledger--get-all-postings)))
  598. (sorted t)))
  599. ;;NEW;;;;;;;;;;;;;
  600. ;;#####################################
  601. (defvar beancount-accounts nil
  602. "A list of accounts available in buffer.")
  603. (make-variable-buffer-local 'beancount-accounts)
  604. (defcustom beancount-account-files nil
  605. "A list of files to provide candidates for account completion."
  606. :type 'list
  607. :group 'beancount)
  608. (defconst beancount-account-regexp
  609. (concat (regexp-opt beancount-account-categories)
  610. "\\(?::[[:upper:]][[:alnum:]-_]+\\)+")
  611. "A regular expression to match account names.")
  612. (defun beancount-collect (regexp n &optional files)
  613. "Return a unique list of REGEXP group N in the current buffer. Optionally
  614. also look at data in selected files."
  615. (let ((pos (point)))
  616. (save-excursion
  617. (save-match-data
  618. (let ((hash (make-hash-table :test 'equal)))
  619. (goto-char (point-min))
  620. (while (re-search-forward regexp nil t)
  621. ;; ignore matches around `pos' since that's presumably
  622. ;; what we are currently trying to complete.
  623. (unless (<= (match-beginning 0) pos (match-end 0))
  624. (puthash (match-string-no-properties n) nil hash)))
  625. ;; if `files' are provided, also look into them
  626. (when files
  627. (save-excursion
  628. (dolist (f files)
  629. (with-current-buffer (find-file-noselect f)
  630. (goto-char (point-min))
  631. (while (re-search-forward regexp nil t)
  632. (puthash (match-string-no-properties n) nil hash))))))
  633. (hash-table-keys hash))))))
  634. (defun beancount-account-completion-table (string pred action)
  635. (if (eq action 'metadata) '(metadata (category . beancount-account))
  636. (if (null beancount-accounts)
  637. (setq beancount-accounts
  638. (sort (beancount-collect beancount-account-regexp 0 beancount-account-files) #'string<)))
  639. (complete-with-action action beancount-accounts string pred)))
  640. (defun beancount-looking-at (regexp n pos)
  641. (print (and (looking-at regexp)
  642. (>= pos (match-beginning n))
  643. (<= pos (match-end n))))
  644. (and (looking-at regexp)
  645. (>= pos (match-beginning n))
  646. (<= pos (match-end n))))
  647. ;;#####################################
  648. ;2022-12-12 ! "Ne"
  649. ;;;###autoload
  650. (defun beancount-completion-at-point ()
  651. "CAPF for corfu."
  652. (interactive)
  653. (save-excursion
  654. (save-match-data
  655. (let ((pos (point)))
  656. (beginning-of-line)
  657. (cond
  658. ;;works!!!! #####################
  659. ; explicity named group \\(?1:.....\\) for the matching part
  660. ; after date, if a letter follows, offer timestamped directives.
  661. ((beancount-looking-at (concat "\\(?2:" beancount-date-regexp "[ ]\\)" "\\(?1:[A-Za-z]*\\)") 1 pos)
  662. (message "found test rx")
  663. (list (match-beginning 1) (match-end 1)
  664. (mapcar (lambda (s) (concat s " ")) beancount-timestamped-directive-names)))
  665. ;; if line starts with 2 spaces, offer accounts
  666. ;;works!!!! #####################
  667. ((beancount-looking-at "^[ ][ ][A-Za-z]+" 0 pos)
  668. (message "found acc rx")
  669. (list (match-beginning 0) (match-end 0)
  670. (mapcar (lambda (s) (concat " " s)) beancount-accounts-cache)))
  671. ;; if line ends with date and status and an open quote, offer payees
  672. ;;works!!!! #####################
  673. ((beancount-looking-at beancount-prefix-for-payee-regex 1 pos)
  674. (message "found payee rx")
  675. (list (match-beginning 1) (match-end 1)
  676. (mapcar (lambda (s) (concat s "\"")) beancount-payees-cache)))
  677. ;;works!!!! #####################
  678. ((beancount-looking-at beancount-prefix-for-payeetext-regex 1 pos)
  679. ; (beancount-update-payees-text)
  680. (message "found payeetext rx")
  681. (list (match-beginning 1) (match-end 1)
  682. (mapcar (lambda (s) (concat s "\"")) beancount-payeetext-cache)))
  683. ;;works!!!! #####################
  684. ((beancount-looking-at beancount-prefix-for-tag-completion-regex 1 pos)
  685. (message "found tags rx")
  686. (list (match-beginning 1) (match-end 1)
  687. beancount-tags-cache))
  688. ;; new function for whole transactions!
  689. ;; source https://github.com/debanjum/company-ledger/blob/master/company-ledger.el
  690. ;; TODO reverse list, newest one first
  691. ;; current workaround: keyword and year
  692. ((beancount-looking-at "^[A-za-z ]+" 0 pos)
  693. (list (match-beginning 0) (match-end 0)
  694. ; (lambda (c) (company-ledger--fuzzy-word-match arg c))
  695. (company-ledger--get-all-postings)))
  696. ; (sort (company-ledger--get-all-postings) #'string-lessp)))
  697. )))))
  698. ;; (sorted t))
  699. ; (list (match-beginning 1) (match-end 1)
  700. ; (mapcar (lambda (s) (concat s " ")) beancount-directive-names)))
  701. ; (delq nil
  702. ; (mapcar (lambda (c)
  703. ; (and (string-prefix-p arg c) c))
  704. ; beancount-payees-cache)))
  705. ;; ;; if line starts with date, offer directives
  706. ;; ((string-match-p beancount-valid-prefix-for-directives-regex (thing-at-point 'line))
  707. ;; (delq nil
  708. ;; (mapcar (lambda (c)
  709. ;; (and (string-prefix-p arg c) c))
  710. ;; beancount-timestamped-directive-names)))
  711. ;; ;; if line starts with date, status and payees, offer tags
  712. ;; ((string-match-p beancount-valid-prefix-for-tag-completion-regex (thing-at-point 'line))
  713. ;; (delq nil
  714. ;; (mapcar (lambda (c)
  715. ;; (and (string-prefix-p (substring arg 1) c) c))
  716. ;; beancount-tags-cache)))
  717. ;; ;; if line starts with accounts and amounts, offer commodities
  718. ;; ((string-match-p (concat "^[ ]+[A-Za-z0-9-_:]+[ ]+[-]?[0-9,.]+[ ]+" beancount-currency-regexp) (thing-at-point 'line))
  719. ;; (delq nil
  720. ;; (mapcar (lambda (c)
  721. ;; (and (string-prefix-p arg c) c))
  722. ;; beancount-commodities-cache)))
  723. ;; ;; if line is empty, offer accounts
  724. ; ((string-match-p "^\\s-+" (thing-at-point 'line))
  725. ; (setq beancount-accounts nil)
  726. ; (list (match-beginning 1) (match-end 1) #'beancount-account-completion-table))
  727. ; ((or (string-match-p "^\\s-+" (thing-at-point 'line))
  728. ;; if the preceding text is allowed before an account, offer accounts
  729. ;; TODO: Not yet working!
  730. ; (string-match-p beancount-timestamped-accounts-regexp (thing-at-point; 'line)))
  731. ; (delq nil
  732. ; (mapcar (lambda (c)
  733. ; (message c)
  734. ; (and (string-prefix-p arg c) c))
  735. ; beancount-accounts-cache)))
  736. ;; ;; if line is empty, offer accounts
  737. ; ((or (string-match-p "^\\s-+" (thing-at-point 'line))
  738. ;; if the preceding text is allowed before an account, offer accounts
  739. ;; TODO: Not yet working!
  740. ; (string-match-p beancount-timestamped-accounts-regexp (thing-at-point 'line)))
  741. ; (delq nil
  742. ; (mapcar (lambda (c)
  743. ; (message c)
  744. ; (and (string-prefix-p arg c) c))
  745. ; beancount-accounts-cache)))
  746. ;; ;; new function for whole transactions!
  747. ;; ;; source https://github.com/debanjum/company-ledger/blob/master/company-ledger.el
  748. ;; ;; these suggestions go when there are no matches for the stuff before
  749. ;; ;;TODO matches only work for first word.
  750. ;; ;;TODO reverse list, newest one first
  751. ; ((delq nil
  752. ; (cl-remove-if-not
  753. ; (lambda (c) (company-ledger--fuzzy-word-match arg c))
  754. ; (company-ledger--get-all-postings))))
  755. ;; ; (cl-remove-if-not
  756. ;; ; (lambda (c) (beancount-company--fuzzy-word-match arg c))
  757. ;; ; (beancount-get-transactions-in-buffer))))
  758. ; )))))
  759. ;; ;;erst hier wird Reihenfolge umgekehrt (jüngste zuerst)!
  760. ;; (sorted t))
  761. ;;end stuff
  762. ;;##################################
  763. ;;;###autoload
  764. (defun company-beancount (command &optional arg &rest ignored)
  765. "Company backend for 'beancount-mode'.
  766. COMMAND, ARG, and IGNORED the regular meanings."
  767. (interactive (list 'interactive))
  768. (message arg)
  769. (cl-case command
  770. (`interactive (company-begin-backend 'company-beancount))
  771. (`prefix (and (eq major-mode 'beancount-mode)
  772. (company-grab-symbol)))
  773. (`candidates
  774. (cond
  775. ;; if line ends with date and status, at max one quoted text, and an open quote, offer payees and explainations
  776. ((string-match-p beancount-valid-prefix-for-payee-completion-regex (thing-at-point 'line))
  777. (delq nil
  778. (mapcar (lambda (c)
  779. ; (and (string-prefix-p (substring arg 1) c) c)
  780. (and (string-prefix-p arg c) c))
  781. beancount-payees-cache)))
  782. ;; if line starts with date, offer directives
  783. ((string-match-p beancount-valid-prefix-for-directives-regex (thing-at-point 'line))
  784. (delq nil
  785. (mapcar (lambda (c)
  786. (and (string-prefix-p arg c) c))
  787. beancount-timestamped-directive-names)))
  788. ;; if line starts with date, status and payees, offer tags
  789. ((string-match-p beancount-valid-prefix-for-tag-completion-regex (thing-at-point 'line))
  790. (delq nil
  791. (mapcar (lambda (c)
  792. (and (string-prefix-p (substring arg 1) c) c))
  793. beancount-tags-cache)))
  794. ;; if line starts with accounts and amounts, offer commodities
  795. ((string-match-p (concat "^[ ]+[A-Za-z0-9-_:]+[ ]+[-]?[0-9,.]+[ ]+" beancount-currency-regexp) (thing-at-point 'line))
  796. (delq nil
  797. (mapcar (lambda (c)
  798. (and (string-prefix-p arg c) c))
  799. beancount-commodities-cache)))
  800. ;; if line is empty, offer accounts
  801. ((or (string-match-p "^\\s-+" (thing-at-point 'line))
  802. ;; if the preceding text is allowed before an account, offer accounts
  803. ;; TODO: Not yet working!
  804. (string-match-p beancount-timestamped-accounts-regexp (thing-at-point 'line)))
  805. (delq nil
  806. (mapcar (lambda (c)
  807. (message c)
  808. (and (string-prefix-p arg c) c))
  809. beancount-accounts-cache)))
  810. ;; new function for whole transactions!
  811. ;; source https://github.com/debanjum/company-ledger/blob/master/company-ledger.el
  812. ;; these suggestions go when there are no matches for the stuff before
  813. ;;TODO matches only work for first word.
  814. ;;TODO reverse list, newest one first
  815. ((delq nil
  816. (cl-remove-if-not
  817. (lambda (c) (company-ledger--fuzzy-word-match arg c))
  818. (company-ledger--get-all-postings))))
  819. ; (cl-remove-if-not
  820. ; (lambda (c) (beancount-company--fuzzy-word-match arg c))
  821. ; (beancount-get-transactions-in-buffer))))
  822. ))
  823. ;;erst hier wird Reihenfolge umgekehrt (jüngste zuerst)!
  824. (sorted t)))
  825. ;;;###autoload
  826. (define-derived-mode beancount-mode text-mode "Beancount"
  827. "A mode for editing beancount files.
  828. \\{beancount-mode-map}"
  829. :init-value nil
  830. :lighter " Beancount"
  831. :group 'beancount
  832. ;; customize font-lock for beancount.
  833. (set-syntax-table beancount-mode-syntax-table)
  834. (font-lock-add-keywords nil beancount-font-lock-directives)
  835. (or beancount-accounts-cache
  836. (setq beancount-accounts-cache (beancount-get-accounts-in-buffer))) ;-new
  837. (or beancount-payees-cache
  838. (setq beancount-payees-cache (beancount-get-payees-in-buffer)))
  839. (or beancount-payeetext-cache
  840. (setq beancount-payeetext-cache (beancount-get-payee-text-in-buffer)))
  841. (or beancount-commodities-cache
  842. (setq beancount-commodities-cache (beancount-get-commodities-in-buffer)))
  843. (or beancount-tags-cache
  844. (setq beancount-tags-cache (beancount-get-tags-in-buffer)))
  845. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  846. ;;NEW for completa-at-point-function
  847. ; (add-hook 'completion-at-point-functions #'beancount-completion-at-point nil t)
  848. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  849. )
  850. (provide 'beancount)
  851. ;;; beancount-mode.el ends here