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.

845 lines
31 KiB

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