Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Jonathan Chu <me@jonathanchu.is>
Guilherme Thomazi Bonicontro <guilherme@binarly.io>
Guilherme Thomazi Bonicontro <thomazi@linux.com>
53 changes: 33 additions & 20 deletions grove-backlink.el
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@
;;; Code:

(require 'grove-core)
(require 'subr-x)

(defconst grove-backlink-buffer-name "*grove-backlinks*"
"Name of the backlinks buffer.")

(defconst grove-backlink-ripgrep-executable "rg"
"Ripgrep executable used for backlink search.")

;;;; Core

(defun grove-backlink--title-for-file (file)
Expand All @@ -47,28 +51,37 @@ Uses the cache if available, otherwise the filename."
"Return a list of backlink results for TITLE.
Each result is a plist (:file :line :context) found via ripgrep."
(grove--ensure-directory)
(unless (executable-find grove-backlink-ripgrep-executable)
(user-error "Ripgrep not found. Install `%s` and ensure it is on your PATH"
grove-backlink-ripgrep-executable))
(let* ((pattern (format "\\[\\[%s\\]\\]" (regexp-quote title)))
(cmd (format "rg --no-heading --line-number --context 1 --glob='*.org' %s %s"
(shell-quote-argument pattern)
(shell-quote-argument grove-directory)))
(output (shell-command-to-string cmd))
(args (list "--no-heading" "--line-number" "--context" "1"
"--glob=*.org" pattern grove-directory))
results current-file)
(dolist (line (split-string output "\n" t))
(cond
;; Context separator
((string-match-p "^--$" line))
;; Match line: file:line:content
((string-match "^\\(.+\\.org\\):\\([0-9]+\\):\\(.*\\)$" line)
(let ((file (match-string 1 line))
(lnum (string-to-number (match-string 2 line)))
(context (string-trim (match-string 3 line))))
;; Skip self-references
(unless (and current-file
(string= file current-file))
(push (list :file file :line lnum :context context)
results))))
;; Context line: file-line-content
((string-match "^\\(.+\\.org\\)-\\([0-9]+\\)-\\(.*\\)$" line))))
(with-temp-buffer
(let ((exit-code (apply #'process-file grove-backlink-ripgrep-executable
nil t nil args)))
(unless (memq exit-code '(0 1))
(user-error "Ripgrep failed (exit %s): %s"
exit-code
(string-trim (buffer-string)))))
(goto-char (point-min))
(dolist (line (split-string (buffer-string) "\n" t))
(cond
;; Context separator
((string-match-p "^--$" line))
;; Match line: file:line:content
((string-match "^\\(.+\\.org\\):\\([0-9]+\\):\\(.*\\)$" line)
(let ((file (match-string 1 line))
(lnum (string-to-number (match-string 2 line)))
(context (string-trim (match-string 3 line))))
;; Skip self-references
(unless (and current-file
(string= file current-file))
(push (list :file file :line lnum :context context)
results))))
;; Context line: file-line-content
((string-match "^\\(.+\\.org\\)-\\([0-9]+\\)-\\(.*\\)$" line)))))
(setq current-file (buffer-file-name))
;; Filter out self-references
(cl-remove-if
Expand Down
20 changes: 3 additions & 17 deletions grove-capture.el
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

(require 'grove-core)

(declare-function grove--unique-path "grove-core" (directory filename))

(defvar grove-capture-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'grove-capture-finalize)
Expand All @@ -42,22 +44,6 @@
:lighter " Grove-Capture"
:keymap grove-capture-mode-map)

(defun grove-capture--unique-path (directory filename)
"Return a unique file path in DIRECTORY for FILENAME.
Appends a numeric suffix if the file already exists."
(let ((base (file-name-sans-extension filename))
(ext (file-name-extension filename t))
(path (expand-file-name filename directory)))
(if (not (file-exists-p path))
path
(let ((n 1))
(while (file-exists-p
(setq path (expand-file-name
(concat base (format "-%d" n) ext)
directory)))
(setq n (1+ n)))
path))))

;;;###autoload
(defun grove-capture ()
"Open a buffer for quick note capture.
Expand Down Expand Up @@ -88,7 +74,7 @@ Type freely, then press \\[grove-capture-finalize] to save or
(title (string-trim (car lines)))
(body (string-join (cdr lines) "\n"))
(filename (concat (grove--sanitize-filename title) ".org"))
(path (grove-capture--unique-path (grove--inbox-path) filename)))
(path (grove--unique-path (grove--inbox-path) filename)))
(with-temp-file path
(insert "#+title: " title "\n")
(unless (string-empty-p body)
Expand Down
32 changes: 29 additions & 3 deletions grove-core.el
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ Each value is a plist with keys :title :tags :links :mtime.")
"\\(?:^\\|[^[:alnum:]_#]\\)#\\([[:alnum:]_][[:alnum:]_/-]*\\)\\b"
"Regexp matching inline hashtags in note content.")

(defconst grove--org-link-protocol-regexp
"\\`[[:alpha:]][[:alnum:]+.-]*:"
"Regexp matching an Org-style link protocol prefix.")

(defun grove--org-link-target-p (target)
"Return non-nil if TARGET looks like a standard Org link target.
This excludes Grove wikilinks such as note titles containing colons."
(and (string-match-p grove--org-link-protocol-regexp target)
(not (string-match-p "[[:space:]]" target))))

(defun grove--collect-inline-tags ()
"Return inline hashtags from the current buffer.
Matches #tag-style markers while ignoring Org keyword lines such as
Expand Down Expand Up @@ -123,13 +133,29 @@ Matches #tag-style markers while ignoring Org keyword lines such as
(make-directory path t))
(file-name-as-directory path)))

(defun grove--unique-path (directory filename)
"Return a unique file path in DIRECTORY for FILENAME.
Appends a numeric suffix if the file already exists."
(let ((base (file-name-sans-extension filename))
(ext (file-name-extension filename t))
(path (expand-file-name filename directory)))
(if (not (file-exists-p path))
path
(let ((n 1))
(while (file-exists-p
(setq path (expand-file-name
(concat base (format "-%d" n) ext)
directory)))
(setq n (1+ n)))
path))))

(defun grove--parse-note (file)
"Parse an org FILE and return a metadata plist.
Returns (:title TITLE :tags TAGS :links LINKS :mtime MTIME)."
(let ((mtime (file-attribute-modification-time (file-attributes file)))
title tags links)
(with-temp-buffer
(insert-file-contents file nil 0 4096)
(insert-file-contents file)
;; Extract #+title:
(goto-char (point-min))
(when (re-search-forward "^#\\+title:\\s-*\\(.+\\)" nil t)
Expand All @@ -143,8 +169,8 @@ Returns (:title TITLE :tags TAGS :links LINKS :mtime MTIME)."
(goto-char (point-min))
(while (re-search-forward "\\[\\[\\([^]]+\\)\\]\\]" nil t)
(let ((link (match-string 1)))
;; Skip standard org links (contain a colon protocol)
(unless (string-match-p ":" link)
;; Skip standard org links like http: or file:
(unless (grove--org-link-target-p link)
(push link links)))))
(list :title (or title (file-name-sans-extension
(file-name-nondirectory file)))
Expand Down
8 changes: 7 additions & 1 deletion grove-inbox.el
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ NOTES is a list of (TITLE . PATH)."
(grove--ensure-directory)
(grove--refresh-cache)
(let* ((untagged (grove-inbox--untagged-notes))
(unlinked (grove-inbox--unlinked-notes))
(buf (get-buffer-create grove-inbox-buffer-name)))
(with-current-buffer buf
(grove-inbox-mode)
Expand All @@ -115,9 +116,14 @@ NOTES is a list of (TITLE . PATH)."
(grove-inbox--insert-section
(format "Untagged (%d)" (length untagged))
untagged)
(grove-inbox--insert-section
(format "No backlinks (%d)" (length unlinked))
unlinked)
(goto-char (point-min))))
(switch-to-buffer buf)
(message "Found %d untagged note(s)" (length untagged))))
(message "Found %d untagged and %d unlinked note(s)"
(length untagged)
(length unlinked))))

(defun grove-inbox-close ()
"Close the inbox review buffer."
Expand Down
28 changes: 16 additions & 12 deletions grove-link.el
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

(require 'grove-core)

(declare-function grove--unique-path "grove-core" (directory filename))

;;;; Faces

(defface grove-link
Expand All @@ -40,9 +42,9 @@
;;;; Font-lock

(defconst grove-link--regexp
"\\[\\[\\([^]:\n]+\\)\\]\\]"
"\\[\\[\\([^]\n]+\\)\\]\\]"
"Regexp matching grove wikilinks.
Matches [[text]] but not [[protocol:text]] (standard org links).")
Matches [[text]], including note titles containing colons.")

(defvar grove-link--keymap
(let ((map (make-sparse-keymap)))
Expand All @@ -55,15 +57,17 @@ Matches [[text]] but not [[protocol:text]] (standard org links).")
"Font-lock matcher for grove wikilinks up to LIMIT.
Adds the grove-link face and a clickable keymap."
(while (re-search-forward grove-link--regexp limit t)
(let ((start (match-beginning 0))
(end (match-end 0)))
(add-text-properties
start end
(list 'face 'grove-link
'mouse-face 'highlight
'keymap grove-link--keymap
'help-echo (format "grove: %s" (match-string 1))
'grove-link-target (match-string 1)))))
(let ((target (match-string 1)))
(unless (grove--org-link-target-p target)
(let ((start (match-beginning 0))
(end (match-end 0)))
(add-text-properties
start end
(list 'face 'grove-link
'mouse-face 'highlight
'keymap grove-link--keymap
'help-echo (format "grove: %s" target)
'grove-link-target target))))))
nil)

(defun grove-link-setup-font-lock ()
Expand Down Expand Up @@ -122,7 +126,7 @@ If the note doesn't exist, offer to create it."
(find-file path)
(if (y-or-n-p (format "Note \"%s\" not found. Create it? " title))
(let* ((filename (concat (grove--sanitize-filename title) ".org"))
(path (expand-file-name filename grove-directory)))
(path (grove--unique-path grove-directory filename)))
(find-file path)
(insert "#+title: " title "\n\n"))
(message "Link not followed")))))
Expand Down
49 changes: 47 additions & 2 deletions grove-tree.el
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ Requires a Nerd Font to be installed and active."
(defconst grove-tree-buffer-name "*grove-tree*"
"Name of the tree sidebar buffer.")

(defvar grove-tree--tracking-enabled nil
"Non-nil when Grove is tracking the current file across window changes.")

(defun grove-tree--indent-string (depth)
"Return an indent guide string for DEPTH levels of nesting."
(if (zerop depth)
Expand Down Expand Up @@ -305,8 +308,23 @@ Directories come first, then files. Hidden files are excluded."
(erase-buffer)
(setq grove-tree--ewoc
(ewoc-create #'grove-tree--print "" ""))
(dolist (node (grove-tree--list-entries grove-directory 0))
(ewoc-enter-last grove-tree--ewoc node)))))))
(grove-tree--populate grove-directory 0 nil))))))

(defun grove-tree--populate (directory depth parent-node)
"Insert DIRECTORY entries at DEPTH after PARENT-NODE.
If a directory is marked expanded in `grove-tree--expanded', recursively
insert its visible descendants."
(let ((prev parent-node))
(dolist (node (grove-tree--list-entries directory depth))
(setq prev (if prev
(ewoc-enter-after grove-tree--ewoc prev node)
(ewoc-enter-last grove-tree--ewoc node)))
(when (and (grove-tree-node-directory-p node)
(gethash (grove-tree-node-path node) grove-tree--expanded))
(setq prev (grove-tree--populate (grove-tree-node-path node)
(1+ depth)
prev))))
prev))

;;;; Current file tracking

Expand All @@ -324,6 +342,31 @@ Directories come first, then files. Hidden files are excluded."
(goto-char pos)
(hl-line-highlight))))))))

(defun grove-tree--track-current-file (&rest _)
"Update the tree highlight to match the currently selected Grove file."
(when grove-tree--tracking-enabled
(let ((file (buffer-file-name (window-buffer (selected-window)))))
(when (grove-file-p file)
(grove-tree--set-current-file file)))))

(defun grove-tree--enable-tracking ()
"Enable tree updates when the selected window changes."
(unless grove-tree--tracking-enabled
(add-hook 'window-selection-change-functions
#'grove-tree--track-current-file)
(add-hook 'buffer-list-update-hook
#'grove-tree--track-current-file)
(setq grove-tree--tracking-enabled t)))

(defun grove-tree--disable-tracking ()
"Disable tree updates for current file tracking."
(when grove-tree--tracking-enabled
(remove-hook 'window-selection-change-functions
#'grove-tree--track-current-file)
(remove-hook 'buffer-list-update-hook
#'grove-tree--track-current-file)
(setq grove-tree--tracking-enabled nil)))

;;;; Mode

(defvar grove-tree-mode-map
Expand Down Expand Up @@ -371,11 +414,13 @@ Directories come first, then files. Hidden files are excluded."
(no-delete-other-windows . t)))))))
(when win
(window-preserve-size win t t))
(grove-tree--enable-tracking)
buf)))

(defun grove-tree-close ()
"Close the tree sidebar."
(interactive)
(grove-tree--disable-tracking)
(let ((win (get-buffer-window grove-tree-buffer-name)))
(when win
(delete-window win)))
Expand Down
Loading