branch: elpa/beancount
commit bdd71c570df3466560260072a863e215f7d5ba1e
Author: Vladimir Kazanov <vkaza...@inbox.ru>
Commit: Daniele Nicolodi <dani...@grinta.net>

    Implement xref backend
---
 beancount-tests.el | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 beancount.el       | 68 +++++++++++++++++++++++++++++++++++++
 2 files changed, 166 insertions(+)

diff --git a/beancount-tests.el b/beancount-tests.el
index 0ffd5c636f..fee1f486a1 100644
--- a/beancount-tests.el
+++ b/beancount-tests.el
@@ -16,6 +16,7 @@
 
 
 (require 'ert)
+(require 'eieio)
 (require 'beancount)
 (require 'imenu)
 
@@ -350,3 +351,100 @@ known option nmaes."
     (goto-char 0)
     (beancount-date-down-day)
     (should (equal (thing-at-point 'line) "2024-05-10\n"))))
+
+;;; Xref backend
+
+(defun beancount-test-xref-definition-pos (identifier position)
+  "Check if IDENTIFIER's position is the same is the same as
+POSITION provided by Beancount's xref-backend-definitions lookup."
+  (let ((defs (xref-backend-definitions 'beancount identifier)))
+    (should (equal (length defs) 1))
+    (let* ((def (car (xref-backend-definitions 'beancount identifier)))
+           (loc (xref-item-location def))
+           ;; Pre Emacs-28.1, defclass was used for
+           ;; xref-buffer-location.
+           (pos (if (version< emacs-version "28.1")
+                    (oref loc position)
+                  (xref-buffer-location-position loc))))
+      (should (equal pos position)))))
+
+(ert-deftest beancount/xref-backend-definitions ()
+  :tags '(xref)
+  (with-temp-buffer
+    (insert "
+2019-01-01 open Assets:Account1 TDB900
+2019-01-01 open Assets:Account2 TDB900
+2019-01-01 open Assets:Account3 TDB900
+
+2019-01-10  * \"Opening Balances\"
+  Equity:Opening-Balances
+  Assets:Account1        1.00 TDB900
+")
+    (beancount-test-xref-definition-pos "Assets:Account1" 2)
+    (beancount-test-xref-definition-pos "Assets:Account2" 41)
+    (beancount-test-xref-definition-pos "Assets:Account3" 80)))
+
+(defmacro beancount-with-temp-file (&rest body)
+  "Generate a temporary file and open it as a current buffer.
+Run BODY forms in the buffer's context. Remove both the buffer
+and a backing file having completed the test."
+  (declare (indent 1))
+  `(let ((file (make-temp-file "beancount-test-"))
+        buf)
+    (unwind-protect
+        (progn (setq buf (find-file-literally file))
+               ,@body)
+      (ignore-errors (delete-file file))
+      (ignore-errors
+        (with-current-buffer buf
+          (set-buffer-modified-p nil))
+        (kill-buffer buf)))))
+
+(ert-deftest beancount/xref-backend-references ()
+  :tags '(xref)
+  ;; Creating Xref file locations assumes a buffer backed by a file.
+  (beancount-with-temp-file
+      (insert "
+2019-01-01 open Assets:Account1 TDB900
+2019-01-01 open Assets:Account2 TDB900
+2019-01-01 open Assets:Account3 TDB900
+
+2019-01-10  * \"Opening Balances\"
+  Equity:Opening-Balances
+  Assets:Account1        1.00 TDB900
+  Assets:Account2        2.00 TDB900
+
+2019-01-10  * \"More Balances\"
+  Equity:Opening-Balances
+  Assets:Account1        1.00 TDB900
+
+")
+    (should (equal (length (xref-backend-references 'beancount 
"Assets:Account1")) 3))
+    (should (equal (length (xref-backend-references 'beancount 
"Assets:Account2")) 2))
+    (should (equal (length (xref-backend-references 'beancount 
"Assets:Account3")) 1))))
+
+(ert-deftest beancount/xref-backend-apropos ()
+  :tags '(xref)
+  ;; Creating Xref file locations assumes a buffer backed by a file.
+  (beancount-with-temp-file
+      (insert "
+2019-01-01 open Assets:Account1 TDB900
+2019-01-01 open Assets:Account2 TDB900
+2019-01-01 open Assets:Account3 TDB900
+
+2019-01-10  * \"Opening Balances\"
+  Equity:Opening-Balances
+  Assets:Account1        1.00 TDB900
+  Assets:Account2        2.00 TDB900
+
+2019-01-10  * \"More Balances\"
+  Equity:Opening-Balances
+  Assets:Account1        1.00 TDB900
+
+")
+    (should (equal (length (xref-backend-apropos 'beancount "Assets")) 6))
+    (should (equal (length (xref-backend-apropos 'beancount "Assets Account")) 
6))
+    (should (equal (length (xref-backend-apropos 'beancount "Assets 
Account1")) 3))
+    (should (equal (length (xref-backend-apropos 'beancount "Equity")) 2))
+    (should (equal (length (xref-backend-apropos 'beancount "Opening")) 2))
+    (should (equal (length (xref-backend-apropos 'beancount "Opening Assets")) 
0))))
diff --git a/beancount.el b/beancount.el
index fd18248611..a893b75f81 100644
--- a/beancount.el
+++ b/beancount.el
@@ -35,6 +35,8 @@
 (require 'outline)
 (require 'thingatpt)
 (require 'cl-lib)
+(require 'xref)
+(require 'apropos)
 
 ;;;###autoload
 (add-to-list 'auto-mode-alist '("\\.beancount\\'" . beancount-mode))
@@ -259,6 +261,11 @@ from the open directive for the relevant account."
 (defconst beancount-metadata-regexp
   "^\\s-+\\([a-z][A-Za-z0-9_-]+:\\)\\s-+\\(.+\\)")
 
+(defconst beancount-open-directive-regexp
+  (concat "^\\(" beancount-date-regexp "\\) +"
+          "\\(open\\) +"
+          "\\(" beancount-account-regexp "\\)"))
+
 ;; This is a grouping regular expression because the subexpression is
 ;; used in determining the outline level in `beancount-outline-level'.
 (defvar beancount-outline-regexp "\\(;;;+\\|\\*+\\)")
@@ -402,6 +409,7 @@ are reserved for the mode anyway.)")
 
   (setq-local outline-regexp beancount-outline-regexp)
   (setq-local outline-level #'beancount-outline-level)
+  (setq-local xref-backend-functions #'beancount-xref-backend)
 
   (setq imenu-generic-expression
        (list (list nil (concat "^" beancount-outline-regexp "\\s-+\\(.*\\)$") 
2))))
@@ -1248,5 +1256,65 @@ Essentially a much simplified version of `next-line'."
   (if-let ((url (string-match "Running Fava on \\(http://.+:[0-9]+\\)\n" 
output)))
       (browse-url (match-string 1 output))))
 
+;;; Xref backend
+
+(defun beancount-xref-backend ()
+  "Beancount Xref backend."
+  'beancount)
+
+(cl-defmethod xref-backend-definitions ((_ (eql beancount)) identifier)
+  "Find definitions of IDENTIFIER."
+  (let ((buf (current-buffer)))
+    (cl-loop
+     for (def-id . def-pos) in
+     (beancount-collect-pos-alist beancount-open-directive-regexp 3)
+     if (equal def-id identifier)
+     collect
+     (xref-make def-id (xref-make-buffer-location buf def-pos)))))
+
+(cl-defmethod xref-backend-references ((_ (eql beancount)) identifier)
+  "Find references of IDENTIFIER."
+  (let ((fname (buffer-file-name)))
+    (cl-loop
+     for (ref-id . ref-pos) in
+     (beancount-collect-pos-alist beancount-account-regexp 0)
+     if (equal ref-id identifier)
+     collect
+     (xref-make ref-id
+                (xref-make-file-location
+                 fname (line-number-at-pos ref-pos) 0)))))
+
+;; NOTE: This is a backport from Emacs 27 and newer versions. Can be
+;; removed once beancount-mode no longer supports Emacs 26.
+(defun beancount-xref-apropos-regexp (pattern)
+  "Return an Emacs regexp from PATTERN similar to `apropos'."
+  (apropos-parse-pattern
+   (if (string-equal (regexp-quote pattern) pattern)
+       ;; Split into words
+       (or (split-string pattern "[ \t]+" t)
+           (user-error "No word list given"))
+     pattern)))
+
+(cl-defmethod xref-backend-apropos ((_ (eql beancount)) pattern)
+  "Find all symbols that match PATTERN string."
+  (let ((pattern-re (beancount-xref-apropos-regexp pattern))
+        (fname (buffer-file-name)))
+    (cl-loop
+     for (ref-id . ref-pos) in
+     (beancount-collect-pos-alist beancount-account-regexp 0)
+     if (string-match-p pattern-re ref-id)
+     collect
+     (xref-make ref-id
+                (xref-make-file-location
+                 fname (line-number-at-pos ref-pos) 0)))))
+
+(cl-defmethod xref-backend-identifier-completion-table ((_ (eql beancount)))
+  (beancount-get-account-names))
+
+(cl-defmethod xref-backend-identifier-at-point ((_ (eql beancount)))
+  "Extract a symbol at point, check if it is an account, return it"
+  (when-let ((acc (thing-at-point 'beancount-account)))
+    (substring-no-properties acc)))
+
 (provide 'beancount)
 ;;; beancount.el ends here

Reply via email to