diff --git a/README.org b/README.org index f2d0281..ad8496f 100644 --- a/README.org +++ b/README.org @@ -26,6 +26,10 @@ Put this in your ~init.el~: ;; Use either `obsidian-insert-wikilink' or `obsidian-insert-link': (bind-key (kbd "C-c C-l") 'obsidian-insert-wikilink 'obsidian-mode-map) + ;; Following backlinks + (bind-key (kbd "C-c C-b") 'obsidian-backlink-jump 'obsidian-mode-map) + + ;; Optionally you can also bind `obsidian-jump' and `obsidian-capture' ;; replace "YOUR_BINDING" with the key of your choice: (bind-key (kbd "YOUR_BINDING") 'obsidian-jump) @@ -48,10 +52,12 @@ Put this in your ~init.el~: ;; This directory will be used for `obsidian-capture' if set. (obsidian-inbox-directory "Inbox") :bind (:map obsidian-mode-map - ;; Replace C-c C-o with Obsidian.el's implementation. It's ok to use another key binding. - ("C-c C-o" . obsidian-follow-link-at-point) - ;; If you prefer you can use `obsidian-insert-link' - ("C-c C-l" . obsidian-insert-wikilink))) + ;; Replace C-c C-o with Obsidian.el's implementation. It's ok to use another key binding. + ("C-c C-o" . obsidian-follow-link-at-point) + ;; Jump to backlinks + ("C-c C-b" . obsidian-backlink-jump) + ;; If you prefer you can use `obsidian-insert-link' + ("C-c C-l" . obsidian-insert-wikilink))) #+end_src Optionally you can specify ~obsidian-inbox-directory~, it will be used by ~obsidian-capture~ to @@ -74,7 +80,8 @@ Obsidian.el must empower us to stay in Emacs for things that make sense in Emacs - [X] Jumping between notes - [X] Searching all notes - [X] Finding all notes with a tag -- [ ] Viewing and following backlinks +- [X] Following backlinks +- [ ] Viewing backlinks in a separate list When all of the above is ready we will almost never need the Obsidian app on desktop, but will still be able to use it on mobile or when specifically needed. @@ -157,6 +164,13 @@ Quickly jump between notes using ~obsidian-jump~ M-x obsidian-jump RET #+end_src +** Following backlinks +You can quickly jump to backlinks to current file using ~obsidian-backlink-jump~ + +#+begin_src + M-x obsidian-backlink-jump RET +#+end_src + *** Aliases If you have YAML front matter in your note, Obsidian.el will find aliases in it and add them to the ~obsidian-jump~ selection. Both ~aliases~ and ~alias~ keys are supported. @@ -193,6 +207,7 @@ Use ~obsidian-tag-find~ to list all notes that contain a tag. Let's you choose a - [X] Obsidian minor for matching .md files - [X] Jumping between notes - [X] Following links +- [X] Following backlinks * Why obsidian.el and not... ** Obsidian App itself, Athens Research or any other great app? diff --git a/obsidian.el b/obsidian.el index 458d950..6f3ef82 100644 --- a/obsidian.el +++ b/obsidian.el @@ -5,7 +5,7 @@ ;; Author: Mykhaylo Bilyanskyy ;; URL: https://github.com./licht1stein/obsidian.el ;; Keywords: obsidian, pkm, convenience -;; Version: 1.1.5 +;; Version: 1.1.6 ;; Package-Requires: ((emacs "27.2") (s "1.12.0") (dash "2.13") (markdown-mode "2.5") (elgrep "1.0.0") (yaml "0.5.1")) ;; This file is NOT part of GNU Emacs. @@ -76,6 +76,11 @@ When run interactively asks user to specify the path." (defvar obsidian--tag-regex "#[[:alnum:]-_/+]+" "Regex pattern used to find tags in Obsidian files.") +(defvar obsidian--basic-wikilink-regex "\\[\\[[[:graph:][:blank:]]*\\]\\]" + "Regex pattern used to find wikilinks.") +(defvar obsidian--basic-markdown-link-regex "\\[[[:graph:][:blank:]]+\\]\([[:graph:][:blank:]]*\)" + "Regex pattern used to find markdown links.") + (defvar obsidian--aliases-map (make-hash-table :test 'equal) "Alist of all Obsidian aliases.") (defun obsidian--clear-aliases-map () @@ -437,6 +442,49 @@ See `markdown-follow-link-at-point' and "Find RE in the Obsidian vault." (elgrep obsidian-directory "\.md" re :recursive t :case-fold-search t :exclude-file-re "~")) +(defun obsidian--link-p (s) + "Check if S matches any of the link regexes." + (or (s-matches-p obsidian--basic-wikilink-regex s) + (s-matches-p obsidian--basic-markdown-link-regex s))) + +(defun obsidian--elgrep-get-context (match) + "Get :context out of MATCH produced by elgrep." + (let* ((result (->> match + (nth 1) + -flatten)) + (context (plist-get result :context))) + context)) + +(defun obsidian--mention-link-p (match) + "Check if MATCH produced by `obsidian--grep' is a link." + (obsidian--link-p (obsidian--elgrep-get-context match))) + +(defun obsidian--find-links-to-file (filename) + "Find any mention of FILENAME in the vault." + (->> (file-name-sans-extension filename) + obsidian--grep + (-filter #'obsidian--mention-link-p) + (-map #'car))) + +(defun obsidian--completing-read-for-matches (coll) + "Take a COLL of matches produced by elgrep and make a list for completing read." + (let* ((dict (make-hash-table :test 'equal)) + (_ (-map (lambda (f) (puthash f (obsidian--expand-file-name f) dict)) coll))) + dict)) + +;;;###autoload +(defun obsidian-backlink-jump () + "Select a backlink to this file and follow it." + (interactive) + (let* ((backlinks (obsidian--find-links-to-file (file-name-nondirectory (buffer-file-name)))) + (dict (obsidian--completing-read-for-matches backlinks)) + (choices (-sort #'string< (-distinct (hash-table-keys dict))))) + (if choices + (let* ((choice (completing-read "Jump to: " choices)) + (target (obsidian--get-alias choice (gethash choice dict)))) + (find-file target)) + (message "No backlinks found.")))) + ;;;###autoload (defun obsidian-search () "Search Obsidian vault for input." diff --git a/tests/test-obsidian.el b/tests/test-obsidian.el index afb5881..0fd41ce 100644 --- a/tests/test-obsidian.el +++ b/tests/test-obsidian.el @@ -98,3 +98,25 @@ key4: (it "check that front-matter is ignored if not at the top of file" (expect (obsidian-find-yaml-front-matter obsidian--test-incorret-front-matter--not-start-of-file) :to-equal nil))) + +(describe "obsidian--link-p" + (it "non link" + (expect (obsidian--link-p "not link") :to-equal nil)) + + (it "wiki link" + (expect (obsidian--link-p "[[foo.md]]") :to-equal t) + (expect (obsidian--link-p "[[foo]]") :to-equal t) + (expect (obsidian--link-p "[[foo|annotated link]]") :to-equal t)) + + (it "markdown link" + (expect (obsidian--link-p "[foo](bar)") :to-equal t) + (expect (obsidian--link-p "[foo](bar.md)") :to-equal t))) + +(describe "obsidian--find-links-to-file" + (before-all (obsidian-specify-path obsidian--test-dir)) + (after-all (obsidian-specify-path obsidian--test--original-dir)) + + (it "1.md" + (expect (obsidian--find-links-to-file "1.md") :to-equal '("2.md")))) + +(provide 'test-obsidian) diff --git a/tests/test_vault/1.md b/tests/test_vault/1.md index 04d2090..2ccea1a 100644 --- a/tests/test_vault/1.md +++ b/tests/test_vault/1.md @@ -8,3 +8,4 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i #tag1 #nested/tag + diff --git a/tests/test_vault/2.md b/tests/test_vault/2.md index c7f048b..02950a1 100644 --- a/tests/test_vault/2.md +++ b/tests/test_vault/2.md @@ -1,6 +1,6 @@ #Tag2 -[insert link md](subdir/2-sub.md) +[insert link md](subdir/2-sub.md) text after markdown link [[inbox/2022-07-24.md|insert link wiki]] @@ -9,9 +9,9 @@ wiki ext url: [[http://example.com|example]] md link: [2-sub with spaces and буквы](subdir/2-sub%20with%20spaces%20and%20буквы.md) -wiki link: [[Subdir/2-sub with spaces and буквы.md|2-sub with spaces and буквы]] +wiki link: [[Subdir/2-sub with spaces and буквы.md|2-sub with spaces and буквы]] some text after link -[[subdir/2-sub with spaces and буквы.md]] +[[subdir/2-sub with spaces and буквы.md]] foo bar spam [2-sub with spaces and буквы](2-sub%20with%20spaces%20and%20буквы.md) diff --git a/tests/test_vault/subdir/1.md b/tests/test_vault/subdir/1.md index be4bc00..99a3c84 100644 --- a/tests/test_vault/subdir/1.md +++ b/tests/test_vault/subdir/1.md @@ -1 +1,4 @@ subdir/1.md + + +[[2-sub with spaces and буквы.md]] foo bar spam