From 581a8681b605612b3f7ba9075bf59b7484d62c8e Mon Sep 17 00:00:00 2001 From: sdoyle88 Date: Fri, 8 May 2026 09:46:33 -0700 Subject: [PATCH 1/3] enh: adding grove-profiles --- grove-backlink.el | 17 +++++-- grove-core.el | 122 ++++++++++++++++++++++++++++++++++++---------- grove-graph.el | 4 +- grove-inbox.el | 6 +-- grove-search.el | 8 +-- grove-tree.el | 8 ++- grove-ui.el | 6 ++- grove.el | 19 ++++++++ 8 files changed, 147 insertions(+), 43 deletions(-) diff --git a/grove-backlink.el b/grove-backlink.el index a9351c7..d498d17 100644 --- a/grove-backlink.el +++ b/grove-backlink.el @@ -31,6 +31,8 @@ (require 'grove-core) (require 'subr-x) +(declare-function grove--profile-for-file "grove-core") + (defconst grove-backlink-buffer-name "*grove-backlinks*" "Name of the backlinks buffer.") @@ -42,7 +44,7 @@ (defun grove-backlink--title-for-file (file) "Return the grove title for FILE. Uses the cache if available, otherwise the filename." - (let ((meta (gethash file grove--cache))) + (let ((meta (gethash file (grove--active-cache)))) (if meta (plist-get meta :title) (file-name-sans-extension (file-name-nondirectory file))))) @@ -56,7 +58,7 @@ Each result is a plist (:file :line :context) found via ripgrep." grove-backlink-ripgrep-executable)) (let* ((pattern (format "\\[\\[%s\\]\\]" (regexp-quote title))) (args (list "--no-heading" "--line-number" "--context" "1" - "--glob=*.org" pattern grove-directory)) + "--glob=*.org" pattern grove--active-directory)) results current-file) (with-temp-buffer (let ((exit-code (apply #'process-file grove-backlink-ripgrep-executable @@ -162,10 +164,15 @@ Each result is a plist (:file :line :context) found via ripgrep." (defun grove-backlinks () "Show backlinks for the current note." (interactive) - (unless (and (buffer-file-name) (grove-file-p (buffer-file-name))) - (user-error "Not visiting a grove note")) + (unless (buffer-file-name) + (user-error "Not visiting a file")) + (unless (grove-file-p (buffer-file-name)) + (let ((other (grove--profile-for-file (buffer-file-name)))) + (if other + (user-error "Note is in profile [%s] — switch profiles first" other) + (user-error "Not visiting a grove note")))) (grove--refresh-cache) - (let* ((meta (gethash (buffer-file-name) grove--cache)) + (let* ((meta (gethash (buffer-file-name) (grove--active-cache))) (title (or (plist-get meta :title) (file-name-sans-extension (file-name-nondirectory (buffer-file-name))))) diff --git a/grove-core.el b/grove-core.el index f29b71d..4afdcad 100644 --- a/grove-core.el +++ b/grove-core.el @@ -39,10 +39,24 @@ (defcustom grove-directory nil "Root directory for grove notes. All org files in this directory and its subdirectories are -considered part of the vault." +considered part of the vault. +When `grove-profiles' is non-nil, this is used only as a fallback +for configs that have not yet migrated to profiles." :type '(choice (const nil) directory) :group 'grove) +(defcustom grove-profiles nil + "Alist of grove profiles, each of the form (NAME :directory DIR). +NAME is a symbol identifying the profile. DIR is the root directory. +Example: + (setq grove-profiles + \\='((personal :directory \"~/notes\") + (work :directory \"~/work/notes\"))) +Use `grove-switch-profile' to select the active profile." + :type '(alist :key-type symbol + :value-type (plist :key-type keyword :value-type sexp)) + :group 'grove) + (defcustom grove-inbox-directory "inbox" "Subdirectory of `grove-directory' for captured notes. Relative to `grove-directory'." @@ -61,10 +75,52 @@ Passed to `format-time-string'." :type 'string :group 'grove) +;;;; Profile state + +(defvar grove--active-profile nil + "Symbol naming the currently active profile, or nil to use `grove-directory'.") + +(defvar grove--profile-caches (make-hash-table :test #'eq) + "Hash table mapping profile name symbols to their vault cache hash tables.") + +(defun grove--active-directory () + "Return the root directory for the active profile or `grove-directory'. +Returns nil if neither is configured." + (if grove--active-profile + (let ((profile (assq grove--active-profile grove-profiles))) + (unless profile + (user-error "Unknown grove profile: %s" grove--active-profile)) + (file-name-as-directory + (expand-file-name (plist-get (cdr profile) :directory)))) + (when grove-directory + (file-name-as-directory (expand-file-name grove-directory))))) + +(defun grove--active-cache () + "Return the cache hash table for the active profile. +Creates a new empty cache lazily on first access per profile." + (if grove--active-profile + (or (gethash grove--active-profile grove--profile-caches) + (let ((cache (make-hash-table :test #'equal))) + (puthash grove--active-profile cache grove--profile-caches) + cache)) + grove--cache)) + +(defun grove--profile-for-file (file) + "Return the profile name symbol whose directory contains FILE, or nil." + (when grove-profiles + (catch 'found + (dolist (profile grove-profiles) + (let* ((name (car profile)) + (dir (file-name-as-directory + (expand-file-name (plist-get (cdr profile) :directory))))) + (when (string-prefix-p dir (expand-file-name file)) + (throw 'found name))))))) + ;;;; Vault cache (defvar grove--cache (make-hash-table :test #'equal) "Hash table mapping absolute file paths to note metadata plists. +Used when no profile is active. Each value is a plist with keys :title :tags :links :mtime.") (defconst grove--hashtag-regexp @@ -105,22 +161,34 @@ Matches #tag-style markers while ignoring Org keyword lines such as (nreverse result))) (defun grove--ensure-directory () - "Ensure `grove-directory' is set and exists, or prompt the user." - (unless grove-directory - (setq grove-directory - (read-directory-name "Grove vault directory: "))) - (unless (file-directory-p grove-directory) - (if (y-or-n-p (format "Directory %s does not exist. Create it? " - grove-directory)) - (make-directory grove-directory t) - (user-error "Grove requires a vault directory"))) - (setq grove-directory (file-name-as-directory - (expand-file-name grove-directory)))) + "Ensure the active grove directory is set and exists, or prompt the user." + (when (and grove-profiles (not grove--active-profile)) + (setq grove--active-profile + (intern (completing-read "Grove profile: " + (mapcar (lambda (p) (symbol-name (car p))) + grove-profiles) + nil t)))) + (if grove--active-profile + (let ((dir (grove--active-directory))) + (unless (file-directory-p dir) + (if (y-or-n-p (format "Directory %s does not exist. Create it? " dir)) + (make-directory dir t) + (user-error "Grove requires a vault directory")))) + (unless grove-directory + (setq grove-directory + (read-directory-name "Grove vault directory: "))) + (unless (file-directory-p grove-directory) + (if (y-or-n-p (format "Directory %s does not exist. Create it? " + grove-directory)) + (make-directory grove-directory t) + (user-error "Grove requires a vault directory"))) + (setq grove-directory (file-name-as-directory + (expand-file-name grove-directory))))) (defun grove--inbox-path () "Return the absolute path to the inbox directory, creating it if needed." (grove--ensure-directory) - (let ((path (expand-file-name grove-inbox-directory grove-directory))) + (let ((path (expand-file-name grove-inbox-directory (grove--active-directory)))) (unless (file-directory-p path) (make-directory path t)) (file-name-as-directory path))) @@ -128,7 +196,7 @@ Matches #tag-style markers while ignoring Org keyword lines such as (defun grove--daily-path () "Return the absolute path to the daily notes directory, creating it if needed." (grove--ensure-directory) - (let ((path (expand-file-name grove-daily-directory grove-directory))) + (let ((path (expand-file-name grove-daily-directory (grove--active-directory)))) (unless (file-directory-p path) (make-directory path t)) (file-name-as-directory path))) @@ -179,33 +247,35 @@ Returns (:title TITLE :tags TAGS :links LINKS :mtime MTIME)." :mtime mtime))) (defun grove--refresh-cache () - "Refresh the vault cache by scanning `grove-directory'. + "Refresh the vault cache by scanning the active grove directory. Only re-parses files whose mtime has changed." (grove--ensure-directory) - (let ((files (directory-files-recursively grove-directory "\\.org\\'")) - (seen (make-hash-table :test #'equal))) + (let* ((dir (grove--active-directory)) + (cache (grove--active-cache)) + (files (directory-files-recursively dir "\\.org\\'")) + (seen (make-hash-table :test #'equal))) ;; Update or add entries (dolist (file files) (puthash file t seen) - (let* ((cached (gethash file grove--cache)) + (let* ((cached (gethash file cache)) (current-mtime (file-attribute-modification-time (file-attributes file))) (cached-mtime (plist-get cached :mtime))) (when (or (null cached) (not (equal current-mtime cached-mtime))) - (puthash file (grove--parse-note file) grove--cache)))) + (puthash file (grove--parse-note file) cache)))) ;; Remove deleted files (maphash (lambda (key _val) (unless (gethash key seen) - (remhash key grove--cache))) - grove--cache))) + (remhash key cache))) + cache))) (defun grove--note-titles () "Return an alist of (TITLE . PATH) for all cached notes." (let (result) (maphash (lambda (path meta) (push (cons (plist-get meta :title) path) result)) - grove--cache) + (grove--active-cache)) (sort result (lambda (a b) (string< (car a) (car b)))))) (defun grove--sanitize-filename (title) @@ -218,11 +288,9 @@ Downcases, replaces spaces with hyphens, strips non-alphanumeric characters." (if (string-empty-p name) "untitled" name))) (defun grove-file-p (file) - "Return non-nil if FILE is inside `grove-directory'." - (and grove-directory - file - (string-prefix-p (expand-file-name grove-directory) - (expand-file-name file)))) + "Return non-nil if FILE is inside the active grove directory." + (when-let ((dir (grove--active-directory))) + (and file (string-prefix-p dir (expand-file-name file))))) (provide 'grove-core) ;;; grove-core.el ends here diff --git a/grove-graph.el b/grove-graph.el index b15e519..c0583a5 100644 --- a/grove-graph.el +++ b/grove-graph.el @@ -90,7 +90,7 @@ Returns an alist of (SOURCE-TITLE . (TARGET-TITLE ...))." ;; Collect all note titles (maphash (lambda (_path meta) (puthash (plist-get meta :title) t all-titles)) - grove--cache) + (grove--active-cache)) ;; Build edges from cached link data (maphash (lambda (_path meta) (let ((source (plist-get meta :title)) @@ -98,7 +98,7 @@ Returns an alist of (SOURCE-TITLE . (TARGET-TITLE ...))." (dolist (target links) (when (gethash target all-titles) (push target (gethash source adjacency)))))) - grove--cache) + (grove--active-cache)) ;; Convert to alist and include isolated nodes (let (result) (maphash (lambda (title _) diff --git a/grove-inbox.el b/grove-inbox.el index 5765fe3..8315eec 100644 --- a/grove-inbox.el +++ b/grove-inbox.el @@ -42,7 +42,7 @@ (lambda (path meta) (when (null (plist-get meta :tags)) (push (cons (plist-get meta :title) path) result))) - grove--cache) + (grove--active-cache)) (sort result (lambda (a b) (string< (car a) (car b)))))) (defun grove-inbox--unlinked-notes () @@ -55,7 +55,7 @@ Checks each note for backlinks via ripgrep." (backlinks (grove-backlink--find title))) (when (null backlinks) (push (cons title path) result)))) - grove--cache) + (grove--active-cache)) (sort result (lambda (a b) (string< (car a) (car b)))))) ;;;; Display @@ -110,7 +110,7 @@ NOTES is a list of (TITLE . PATH)." (erase-buffer) (insert (propertize "Grove Inbox Review" 'face 'bold) "\n") (insert (propertize (format "%d notes in vault" - (hash-table-count grove--cache)) + (hash-table-count (grove--active-cache))) 'face 'shadow) "\n\n") (grove-inbox--insert-section diff --git a/grove-search.el b/grove-search.el index 0423f1e..10a167f 100644 --- a/grove-search.el +++ b/grove-search.el @@ -54,14 +54,14 @@ available, otherwise falls back to `grep'." (let ((consult-ripgrep-args (concat consult-ripgrep-args " --glob=*.org"))) (consult--grep "Grove search" #'consult--grep-make-builder - grove-directory initial))) + (grove--active-directory) initial))) (defun grove-search--grep (&optional initial) "Search vault with grep. INITIAL is the initial input." (let ((pattern (read-string "Grove search: " initial))) (grep (format "rg --no-heading --line-number --glob='*.org' %s %s" (shell-quote-argument pattern) - (shell-quote-argument grove-directory))))) + (shell-quote-argument (grove--active-directory)))))) ;;;; Find note by title @@ -108,10 +108,10 @@ With optional INITIAL input string. Searches for both org-style (let ((consult-ripgrep-args (concat consult-ripgrep-args " --glob=*.org"))) (consult--grep "Grove tags" #'consult--grep-make-builder - grove-directory pattern)) + (grove--active-directory) pattern)) (grep (format "rg --no-heading --line-number --glob='*.org' %s %s" (shell-quote-argument pattern) - (shell-quote-argument grove-directory)))))) + (shell-quote-argument (grove--active-directory))))))) (provide 'grove-search) ;;; grove-search.el ends here diff --git a/grove-tree.el b/grove-tree.el index b8d26f9..791c434 100644 --- a/grove-tree.el +++ b/grove-tree.el @@ -306,9 +306,15 @@ Directories come first, then files. Hidden files are excluded." (with-current-buffer buf (let ((inhibit-read-only t)) (erase-buffer) + (setq header-line-format + (propertize + (if grove--active-profile + (format " Grove [%s]" grove--active-profile) + " Grove") + 'face 'bold)) (setq grove-tree--ewoc (ewoc-create #'grove-tree--print "" "")) - (grove-tree--populate grove-directory 0 nil)))))) + (grove-tree--populate grove--active-directory 0 nil)))))) (defun grove-tree--populate (directory depth parent-node) "Insert DIRECTORY entries at DEPTH after PARENT-NODE. diff --git a/grove-ui.el b/grove-ui.el index 5c26dcb..93f4ff7 100644 --- a/grove-ui.el +++ b/grove-ui.el @@ -60,7 +60,11 @@ and displays a note in the main area." (when-let ((file (buffer-file-name (window-buffer main-win)))) (grove-tree--set-current-file file))) (setq grove--active-p t) - (message "Grove opened: %s" (abbreviate-file-name grove-directory))) + (message "Grove opened: %s%s" + (if grove--active-profile + (format "[%s] " grove--active-profile) + "") + (abbreviate-file-name (grove--active-directory)))) (defun grove--open-initial-note () "Open an initial note in the current window. diff --git a/grove.el b/grove.el index 44e4f24..b9f1ea8 100644 --- a/grove.el +++ b/grove.el @@ -84,6 +84,24 @@ grove--turn-on :group 'grove) +;;;; Profile switching + +;;;###autoload +(defun grove-switch-profile (name) + "Switch the active grove profile to NAME. +NAME must be a key in `grove-profiles'." + (interactive + (list (completing-read "Grove profile: " + (mapcar (lambda (p) (symbol-name (car p))) + grove-profiles) + nil t))) + (setq grove--active-profile (intern name)) + (when (get-buffer grove-tree-buffer-name) + (grove-tree-refresh)) + (message "Grove profile: %s (%s)" + name + (abbreviate-file-name (grove--active-directory)))) + ;;;; Global keymap (defvar grove-command-map @@ -99,6 +117,7 @@ (define-key map (kbd "i") #'grove-inbox-review) (define-key map (kbd "l") #'grove-link-insert) (define-key map (kbd "g") #'grove-graph) + (define-key map (kbd "p") #'grove-switch-profile) map) "Keymap for grove commands, bound under a prefix key. Bind this to a prefix key in your init file, e.g.: From 791add845b4f4321ccd9278fc69c3f6fafcd9b6b Mon Sep 17 00:00:00 2001 From: sdoyle88 Date: Fri, 8 May 2026 09:46:56 -0700 Subject: [PATCH 2/3] fix: link warning if in other profile --- grove-link.el | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/grove-link.el b/grove-link.el index a0f7a2d..ea91b33 100644 --- a/grove-link.el +++ b/grove-link.el @@ -118,18 +118,37 @@ prompt the user to choose." (unless target (user-error "No grove link at point")) (grove-link-follow target))) +(defun grove-link--profile-with-title (title) + "Return the profile name that has a cached note matching TITLE, or nil. +Only searches profiles other than the currently active one." + (catch 'found + (maphash + (lambda (profile-name cache) + (unless (eq profile-name grove--active-profile) + (maphash + (lambda (_file meta) + (when (string-equal-ignore-case (plist-get meta :title) title) + (throw 'found profile-name))) + cache))) + grove--profile-caches))) + (defun grove-link-follow (title) "Follow a grove wikilink to TITLE. If the note doesn't exist, offer to create it." (let ((path (grove-link--resolve title))) (if path (find-file path) - (if (y-or-n-p (format "Note \"%s\" not found. Create it? " title)) + (let ((other-profile (grove-link--profile-with-title title))) + (cond + (other-profile + (message "Note \"%s\" is in profile [%s] — switch profiles to follow" + title other-profile)) + ((y-or-n-p (format "Note \"%s\" not found. Create it? " title)) (let* ((filename (concat (grove--sanitize-filename title) ".org")) - (path (grove--unique-path grove-directory filename))) + (path (grove--unique-path grove--active-directory filename))) (find-file path) - (insert "#+title: " title "\n\n")) - (message "Link not followed"))))) + (insert "#+title: " title "\n\n"))) + (t (message "Link not followed"))))))) ;;;; Insert From 5e95d223e35d4cfda774edf2b06db2dfdfdeb861 Mon Sep 17 00:00:00 2001 From: sdoyle88 Date: Sat, 9 May 2026 13:45:39 -0700 Subject: [PATCH 3/3] refactor: updated and rebased, add test, update readme --- README.org | 6 ++ grove-backlink.el | 2 +- grove-link.el | 2 +- grove-tree.el | 2 +- grove.el | 6 ++ tests/grove-profiles-test.el | 125 +++++++++++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 tests/grove-profiles-test.el diff --git a/README.org b/README.org index 9be26cd..16311c1 100644 --- a/README.org +++ b/README.org @@ -151,6 +151,12 @@ Use =+= / =-= to zoom in/out and =0= to fit to window. ;; Required: set your vault directory (setq grove-directory "~/notes/") +;; Optional: set profiles for separation of note vaults +(setq grove-profiles + '((personal :directory "~/remoteFolder/personal") + (work :directory "~/localFolder/work")) + (other :directory "~/otherNotes"))) + ;; Optional: customize subdirectories (defaults shown) (setq grove-inbox-directory "inbox") (setq grove-daily-directory "daily") diff --git a/grove-backlink.el b/grove-backlink.el index d498d17..645cca8 100644 --- a/grove-backlink.el +++ b/grove-backlink.el @@ -58,7 +58,7 @@ Each result is a plist (:file :line :context) found via ripgrep." grove-backlink-ripgrep-executable)) (let* ((pattern (format "\\[\\[%s\\]\\]" (regexp-quote title))) (args (list "--no-heading" "--line-number" "--context" "1" - "--glob=*.org" pattern grove--active-directory)) + "--glob=*.org" pattern (grove--active-directory))) results current-file) (with-temp-buffer (let ((exit-code (apply #'process-file grove-backlink-ripgrep-executable diff --git a/grove-link.el b/grove-link.el index ea91b33..50f38c1 100644 --- a/grove-link.el +++ b/grove-link.el @@ -145,7 +145,7 @@ If the note doesn't exist, offer to create it." title other-profile)) ((y-or-n-p (format "Note \"%s\" not found. Create it? " title)) (let* ((filename (concat (grove--sanitize-filename title) ".org")) - (path (grove--unique-path grove--active-directory filename))) + (path (grove--unique-path (grove--active-directory) filename))) (find-file path) (insert "#+title: " title "\n\n"))) (t (message "Link not followed"))))))) diff --git a/grove-tree.el b/grove-tree.el index 791c434..ec2be6a 100644 --- a/grove-tree.el +++ b/grove-tree.el @@ -314,7 +314,7 @@ Directories come first, then files. Hidden files are excluded." 'face 'bold)) (setq grove-tree--ewoc (ewoc-create #'grove-tree--print "" "")) - (grove-tree--populate grove--active-directory 0 nil)))))) + (grove-tree--populate (grove--active-directory) 0 nil)))))) (defun grove-tree--populate (directory depth parent-node) "Insert DIRECTORY entries at DEPTH after PARENT-NODE. diff --git a/grove.el b/grove.el index b9f1ea8..da504b8 100644 --- a/grove.el +++ b/grove.el @@ -39,7 +39,13 @@ ;; - Inbox review for triaging untagged notes ;; ;; Usage: +;; ;;;; Single note location ;; (setq grove-directory "~/notes/") +;; ;;;; Profile mode for different storage locations. +;; (setq grove-profiles +;; '((personal :directory "~/remoteFolder/personal") +;; (work :directory "~/localFolder/work")) +;; (other :directory "~/otherNotes"))) ;; (global-grove-mode 1) ; auto-enable grove-mode in vault files ;; (grove-open) diff --git a/tests/grove-profiles-test.el b/tests/grove-profiles-test.el new file mode 100644 index 0000000..05eda06 --- /dev/null +++ b/tests/grove-profiles-test.el @@ -0,0 +1,125 @@ +;;; grove-profiles-test.el --- Tests for grove profile functionality -*- lexical-binding: t -*- + +;; Copyright 2026 Jonathan Chu + +;;; Commentary: + +;; Tests covering grove--active-directory, grove--active-cache, +;; grove--profile-for-file, and grove-switch-profile. + +;;; Code: + +(require 'ert) +(require 'grove-core) +(require 'grove-link) +(require 'grove) + +(defmacro grove-test--with-profiles (profiles &rest body) + "Run BODY with PROFILES bound and all profile state reset." + (declare (indent 1)) + `(let ((grove-profiles ,profiles) + (grove--active-profile nil) + (grove--profile-caches (make-hash-table :test #'eq)) + (grove--cache (make-hash-table :test #'equal))) + ,@body)) + +;;; grove--active-directory + +(ert-deftest grove--active-directory-falls-back-to-grove-directory () + (grove-test--with-profiles nil + (let ((grove-directory "/tmp/my-notes")) + (should (string= (grove--active-directory) + (file-name-as-directory (expand-file-name "/tmp/my-notes"))))))) + +(ert-deftest grove--active-directory-returns-nil-when-unconfigured () + (grove-test--with-profiles nil + (let ((grove-directory nil)) + (should-not (grove--active-directory))))) + +(ert-deftest grove--active-directory-returns-profile-directory () + (let ((dir (make-temp-file "grove-profile" t))) + (unwind-protect + (grove-test--with-profiles `((work :directory ,dir)) + (setq grove--active-profile 'work) + (should (string= (grove--active-directory) + (file-name-as-directory (expand-file-name dir))))) + (delete-directory dir t)))) + +(ert-deftest grove--active-directory-errors-on-unknown-profile () + (grove-test--with-profiles '((work :directory "/tmp/work")) + (setq grove--active-profile 'nonexistent) + (should-error (grove--active-directory) :type 'user-error))) + +;;; grove--active-cache + +(ert-deftest grove--active-cache-returns-grove--cache-when-no-profile () + (grove-test--with-profiles nil + (should (eq (grove--active-cache) grove--cache)))) + +(ert-deftest grove--active-cache-returns-separate-cache-per-profile () + (grove-test--with-profiles '((personal :directory "/tmp/personal") + (work :directory "/tmp/work")) + (setq grove--active-profile 'personal) + (let ((personal-cache (grove--active-cache))) + (setq grove--active-profile 'work) + (let ((work-cache (grove--active-cache))) + (should-not (eq personal-cache work-cache)) + (should-not (eq personal-cache grove--cache)))))) + +(ert-deftest grove--active-cache-creates-cache-lazily () + (grove-test--with-profiles '((work :directory "/tmp/work")) + (setq grove--active-profile 'work) + (should-not (gethash 'work grove--profile-caches)) + (let ((cache (grove--active-cache))) + (should (hash-table-p cache)) + (should (eq cache (gethash 'work grove--profile-caches)))))) + +;;; grove--profile-for-file + +(ert-deftest grove--profile-for-file-returns-matching-profile () + (let ((dir (make-temp-file "grove-profile" t))) + (unwind-protect + (grove-test--with-profiles `((work :directory ,dir)) + (should (eq (grove--profile-for-file (expand-file-name "note.org" dir)) + 'work))) + (delete-directory dir t)))) + +(ert-deftest grove--profile-for-file-returns-nil-when-no-match () + (grove-test--with-profiles '((work :directory "/tmp/work-notes")) + (should-not (grove--profile-for-file "/tmp/other/note.org")))) + +(ert-deftest grove--profile-for-file-returns-nil-when-no-profiles () + (grove-test--with-profiles nil + (should-not (grove--profile-for-file "/tmp/any/note.org")))) + +;;; grove-switch-profile + +(ert-deftest grove-switch-profile-sets-active-profile () + (let ((dir (make-temp-file "grove-profile" t))) + (unwind-protect + (grove-test--with-profiles `((personal :directory ,dir) + (work :directory ,dir)) + (grove-switch-profile "work") + (should (eq grove--active-profile 'work))) + (delete-directory dir t)))) + +;;; grove-link-follow with active profile + +(ert-deftest grove-link-follow-creates-note-in-active-profile-directory () + (let ((dir (make-temp-file "grove-vault" t))) + (unwind-protect + (grove-test--with-profiles `((personal :directory ,dir)) + (setq grove--active-profile 'personal) + (cl-letf (((symbol-function 'grove-link--resolve) (lambda (_) nil)) + ((symbol-function 'grove-link--profile-with-title) (lambda (_) nil)) + ((symbol-function 'y-or-n-p) (lambda (&rest _) t))) + (grove-link-follow "New Note") + (should (buffer-file-name)) + (should (string-prefix-p (file-name-as-directory (expand-file-name dir)) + (buffer-file-name))) + (when (buffer-live-p (current-buffer)) + (kill-buffer (current-buffer))))) + (delete-directory dir t)))) + +(provide 'grove-profiles-test) +;;; grove-profiles-test.el ends here