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: 2 additions & 0 deletions CHANGELOG.org
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.

** Added

- Per-widget collapse state ([[https://github.com/d12frosted/vulpea-ui/issues/80][#80]]). =:collapsed= on =vulpea-ui-register-widget= decides whether that widget starts collapsed, overriding =vulpea-ui-default-widget-collapsed= for it alone; =vulpea-ui-widget-set= sets it on widgets registered elsewhere, built-in or from other packages. =nil= starts a widget expanded even when the global default folds everything, so your own widget can stay open while the built-ins start folded (or the other way around). Without =:collapsed= a widget follows the global value as before. The sidebar hands each widget its registry entry through a context that the =vulpea-ui-widget= wrapper reads, so no widget code changes. Thanks =@lhernanz= for the request.

- Property drawer entries in collection views. =(property KEY)= is a column over the note's =:PROPERTIES:= drawer (=(property "COMMIT_ID")=), offered as =prop:KEY= by =c a= for every property found on the notes in view. =:properties= filters on them with the same shape as =:meta= (=(("COMMIT_ID" . "abc") ("CREATED_TS" . t))=), written =prop:KEY=VALUE= / =prop:KEY=*= in queries and added with =/ p=. Keys are case-insensitive, like in org. A property condition is pushed down to vulpea's property index when no tag condition is cheaper. =e= on a property cell sets it on the selection (empty removes it, =Z= undoes), === narrows to its value, and adaptive columns show the properties a view is filtered by. Writes find the entry by its ID, so a batch touching several notes of one file lands in the right drawers.

- Computed collection columns: =(fn NAME FUNCTION)= calls FUNCTION with each note and shows what it returns, so any value derivable from a note can be a column without waiting for a built-in one.
Expand Down
26 changes: 24 additions & 2 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ list of types.
;; When nil, sidebar remains visible with stale content
(setq vulpea-ui-sidebar-auto-hide t)

;; Start widgets collapsed
;; Start widgets collapsed (a widget's own :collapsed wins, see
;; "Collapsed or expanded" below)
(setq vulpea-ui-default-widget-collapsed nil)

;; Auto-refresh sidebar on database changes and idle (enabled by
Expand Down Expand Up @@ -728,7 +729,8 @@ vulpea-ui registers these widgets by default:
(vulpea-ui-register-widget 'my-widget
:component 'my-custom-widget-component
:predicate #'my-note-predicate ; optional: only show when this returns non-nil
:order 150) ; optional: default 100
:order 150 ; optional: default 100
:collapsed nil) ; optional: nil starts expanded, t collapsed
#+end_src

** Narrowing-aware widgets
Expand Down Expand Up @@ -781,6 +783,26 @@ stays file-scoped - nothing else changes for it.
(vulpea-ui-unregister-widget 'links)
#+end_src

** Collapsed or expanded

By default every widget follows =vulpea-ui-default-widget-collapsed=. Give a widget =:collapsed= to decide for it alone: =t= starts it collapsed, =nil= starts it expanded, whatever the global value says. It works at registration and with =vulpea-ui-widget-set=, so built-in widgets and widgets from other packages can have their own default too.

#+begin_src emacs-lisp
;; Fold everything except your own widget
(setq vulpea-ui-default-widget-collapsed t)
(vulpea-ui-register-widget 'my-widget
:component 'my-custom-widget-component
:collapsed nil)

;; Or keep widgets expanded and fold only the long ones
(vulpea-ui-widget-set 'backlinks :collapsed t)
(vulpea-ui-widget-set 'unlinked-mentions :collapsed t)
#+end_src

This only decides how a widget starts. You can still toggle it, and the sidebar keeps the toggled state through refreshes and note switches. Changing =:collapsed= while the sidebar is open takes effect after you close and reopen it.

Widgets get this through the =vulpea-ui-widget= wrapper, so a custom widget built on it needs no code for it.

** Toggling a widget per note

Built-in widgets have no predicate by default, so they are shown for every note. You can install one with =vulpea-ui-widget-set= to decide per note whether the widget shows up. A common recipe is a global default variable combined with an org property that overrides it on individual notes.
Expand Down
161 changes: 151 additions & 10 deletions test/vulpea-ui-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,11 @@ raw org markup such as links and emphasis."

;;; Widget registry tests

(defun vulpea-ui-test--widget-components (note)
"Return the components of the widgets shown for NOTE, in display order."
(mapcar (lambda (widget) (plist-get widget :component))
(vulpea-ui--get-widgets-for-note note)))

(ert-deftest vulpea-ui-test-register-widget-stores-props ()
"Registering a widget stores component, predicate and order."
(vulpea-ui-test--with-clean-registry
Expand All @@ -2006,6 +2011,22 @@ raw org markup such as links and emphasis."
(should (eq (plist-get props :component) 'second))
(should (= (plist-get props :order) 200)))))

(ert-deftest vulpea-ui-test-register-widget-stores-collapsed ()
"An explicit :collapsed is stored, nil included; an absent one is not.
Nil starts the widget expanded whatever the global default says, so it
must stay distinguishable from not setting :collapsed at all."
(vulpea-ui-test--with-clean-registry
(vulpea-ui-register-widget 'folded :component 'c :collapsed t)
(vulpea-ui-register-widget 'open :component 'c :collapsed nil)
(vulpea-ui-register-widget 'plain :component 'c)
(let ((folded (gethash 'folded vulpea-ui--widget-registry))
(open (gethash 'open vulpea-ui--widget-registry))
(plain (gethash 'plain vulpea-ui--widget-registry)))
(should (eq (plist-get folded :collapsed) t))
(should (plist-member open :collapsed))
(should-not (plist-get open :collapsed))
(should-not (plist-member plain :collapsed)))))

(ert-deftest vulpea-ui-test-unregister-widget ()
"Unregistering removes the widget from the registry."
(vulpea-ui-test--with-clean-registry
Expand All @@ -2032,7 +2053,7 @@ raw org markup such as links and emphasis."
(vulpea-ui-test--with-clean-registry
(vulpea-ui-register-widget 'w :component 'w-component)
(should (memq 'w-component
(vulpea-ui--get-widgets-for-note
(vulpea-ui-test--widget-components
(vulpea-ui-test--make-mock-note))))))

(ert-deftest vulpea-ui-test-widgets-for-note-predicate-filters ()
Expand All @@ -2042,11 +2063,11 @@ raw org markup such as links and emphasis."
:component 'w-component
:predicate (lambda (_note) nil))
(should-not (memq 'w-component
(vulpea-ui--get-widgets-for-note
(vulpea-ui-test--widget-components
(vulpea-ui-test--make-mock-note))))
(vulpea-ui-widget-set 'w :predicate (lambda (_note) t))
(should (memq 'w-component
(vulpea-ui--get-widgets-for-note
(vulpea-ui-test--widget-components
(vulpea-ui-test--make-mock-note))))))

(ert-deftest vulpea-ui-test-widgets-for-note-ordering ()
Expand All @@ -2055,7 +2076,7 @@ raw org markup such as links and emphasis."
(vulpea-ui-register-widget 'a :component 'a-component :order 300)
(vulpea-ui-register-widget 'b :component 'b-component :order 100)
(vulpea-ui-register-widget 'c :component 'c-component :order 200)
(should (equal (vulpea-ui--get-widgets-for-note
(should (equal (vulpea-ui-test--widget-components
(vulpea-ui-test--make-mock-note))
'(b-component c-component a-component)))))

Expand All @@ -2074,26 +2095,146 @@ Mirrors the example from the README."
default-on)))
;; no property, variable nil -> hidden
(should-not (memq 'w-component
(vulpea-ui--get-widgets-for-note
(vulpea-ui-test--widget-components
(vulpea-ui-test--make-mock-note))))
;; no property, variable t -> shown
(setq default-on t)
(should (memq 'w-component
(vulpea-ui--get-widgets-for-note
(vulpea-ui-test--widget-components
(vulpea-ui-test--make-mock-note))))
;; property "nil" overrides variable t -> hidden
(should-not (memq 'w-component
(vulpea-ui--get-widgets-for-note
(vulpea-ui-test--widget-components
(vulpea-ui-test--make-mock-note
nil nil '(("SHOW_W" . "nil"))))))
;; property "t" overrides variable nil -> shown
(setq default-on nil)
(should (memq 'w-component
(vulpea-ui--get-widgets-for-note
(vulpea-ui-test--widget-components
(vulpea-ui-test--make-mock-note
nil nil '(("SHOW_W" . "t")))))))))


;;; Widget collapse tests

(vui-defcomponent vulpea-ui-test--collapse-alpha ()
"Probe widget titled Alpha with the body \"Alpha body\"."
:render
(vui-component 'vulpea-ui-widget
:title "Alpha"
:children (lambda () (vui-text "Alpha body"))))

(vui-defcomponent vulpea-ui-test--collapse-beta ()
"Probe widget titled Beta with the body \"Beta body\"."
:render
(vui-component 'vulpea-ui-widget
:title "Beta"
:children (lambda () (vui-text "Beta body"))))

(defun vulpea-ui-test--collapse-state (output title)
"Return how the probe widget TITLE shows in OUTPUT.
An expanded widget has the ▼ header and its body, a collapsed one the
▶ header only; the result is `expanded', `collapsed', or nil when
OUTPUT matches neither."
(let ((body (string-match-p (regexp-quote (concat title " body")) output)))
(cond ((and body (string-match-p (regexp-quote (concat "▼ " title)) output))
'expanded)
((and (not body) (string-match-p (regexp-quote (concat "▶ " title)) output))
'collapsed))))

(ert-deftest vulpea-ui-test-widget-collapse-follows-global ()
"Without :collapsed a widget follows `vulpea-ui-default-widget-collapsed'."
(vulpea-ui-test--with-clean-registry
(vulpea-ui-register-widget 'alpha :component 'vulpea-ui-test--collapse-alpha)
(dolist (case '((nil . expanded) (t . collapsed)))
(let ((vulpea-ui-default-widget-collapsed (car case)))
(vulpea-ui-test--mount-sidebar-root (vulpea-ui-test--make-mock-note)
(should (eq (vulpea-ui-test--collapse-state output "Alpha")
(cdr case))))))))

(ert-deftest vulpea-ui-test-widget-collapse-overrides-global ()
"A widget's :collapsed wins over `vulpea-ui-default-widget-collapsed'.
Each widget applies its own setting, so one sidebar can mix both
states: :collapsed nil starts expanded even when the global says
collapsed, and the other way around."
(vulpea-ui-test--with-clean-registry
(vulpea-ui-register-widget 'alpha
:component 'vulpea-ui-test--collapse-alpha
:collapsed nil
:order 100)
(vulpea-ui-register-widget 'beta
:component 'vulpea-ui-test--collapse-beta
:collapsed t
:order 200)
(dolist (global '(nil t))
(let ((vulpea-ui-default-widget-collapsed global))
(vulpea-ui-test--mount-sidebar-root (vulpea-ui-test--make-mock-note)
(should (eq (vulpea-ui-test--collapse-state output "Alpha") 'expanded))
(should (eq (vulpea-ui-test--collapse-state output "Beta") 'collapsed)))))))

(ert-deftest vulpea-ui-test-widget-set-collapsed ()
"`vulpea-ui-widget-set' changes :collapsed of a registered widget.
This is how a built-in or third-party widget gets its own default."
(vulpea-ui-test--with-clean-registry
(vulpea-ui-register-widget 'alpha :component 'vulpea-ui-test--collapse-alpha)
(vulpea-ui-widget-set 'alpha :collapsed t)
(let ((vulpea-ui-default-widget-collapsed nil))
(vulpea-ui-test--mount-sidebar-root (vulpea-ui-test--make-mock-note)
(should (eq (vulpea-ui-test--collapse-state output "Alpha")
'collapsed))))))

(ert-deftest vulpea-ui-test-widget-collapse-outside-sidebar ()
"Outside the sidebar `vulpea-ui-widget' follows the global default.
There is no registry entry to read there, so nothing overrides it."
(dolist (case '((nil . expanded) (t . collapsed)))
(let ((vulpea-ui-default-widget-collapsed (car case))
(buf-name "*vulpea-ui-collapse-test*"))
(unwind-protect
(progn
(vui-mount (vui-component 'vulpea-ui-test--collapse-alpha) buf-name)
(should (eq (vulpea-ui-test--collapse-state
(with-current-buffer buf-name
(buffer-substring-no-properties
(point-min) (point-max)))
"Alpha")
(cdr case))))
(when (get-buffer buf-name)
(kill-buffer buf-name))))))

(ert-deftest vulpea-ui-test-widget-collapse-keeps-toggle-across-notes ()
"A widget toggled by hand keeps its state when the sidebar changes notes.
The registered :collapsed only seeds the widget when it mounts.
Re-rendering the sidebar for another note must reuse the widget, not
remount it back into its registered state."
(vulpea-ui-test--with-clean-registry
(vulpea-ui-register-widget 'alpha
:component 'vulpea-ui-test--collapse-alpha
:collapsed t)
(let ((vui-render-delay nil)
(buf-name "*vulpea-ui-collapse-test*"))
(with-current-buffer (get-buffer-create buf-name)
(vulpea-ui-sidebar-mode))
(unwind-protect
(let ((instance (vui-mount
(vui-component 'vulpea-ui-sidebar-root
:note (vulpea-ui-test--make-mock-note
"note-a" "A"))
buf-name)))
(with-current-buffer buf-name
(goto-char (point-min))
(search-forward "▶ Alpha")
(vui-activate (match-beginning 0))
(should (eq (vulpea-ui-test--collapse-state (buffer-string) "Alpha")
'expanded))
(vui-update-props instance
(list :note (vulpea-ui-test--make-mock-note
"note-b" "B")))
(should (eq (vulpea-ui-test--collapse-state (buffer-string) "Alpha")
'expanded))))
(when (get-buffer buf-name)
(kill-buffer buf-name))))))


;;; Unlinked mentions grouping tests

(ert-deftest vulpea-ui-test-group-mentions-empty ()
Expand Down Expand Up @@ -5203,10 +5344,10 @@ Mirrors the sidebar split: data changes are announced on
(let ((note (vulpea-ui-test--make-mock-note)))
(let ((vulpea-ui-collection-views nil))
(should-not (memq 'vulpea-ui-widget-collections
(vulpea-ui--get-widgets-for-note note))))
(vulpea-ui-test--widget-components note))))
(let ((vulpea-ui-collection-views '(("wines" . (:filter nil)))))
(should (memq 'vulpea-ui-widget-collections
(vulpea-ui--get-widgets-for-note note))))))
(vulpea-ui-test--widget-components note))))))

(ert-deftest vulpea-ui-collection-test-bookmark ()
"Bookmarks capture the view (minus the predicate) and restore it."
Expand Down
Loading
Loading