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

- The links widget follows narrowing too ([[https://github.com/d12frosted/vulpea-ui/issues/68][#68]]): with the buffer narrowed, only links positioned inside the restriction are counted, with the same =narrowed= marker on the header count as backlinks. Positions come from the database, so unsaved edits can shift them until the next sync. The README also gained a recipe for refreshing the sidebar instantly on narrow/widen by wrapping =org-toggle-narrow-to-subtree= (thanks @alexjp).

- The narrowing scope is public API for custom widgets. The sidebar root computes it once per render pass (via the now-public =vulpea-ui-narrowing-scope=) and provides it as the =vulpea-ui-scope= context, so however many widgets follow narrowing, the buffer is scanned once. Built-in widgets (backlinks, outline, stats) consume the context instead of probing the buffer each. A custom widget follows narrowing with one hook call and one memo dependency: read =use-vulpea-ui-scope=, put the value in =vui-use-memo= deps, scope the data by it; see the README's "Narrowing-aware widgets" recipe.

- The sidebar follows narrowing ([[https://github.com/d12frosted/vulpea-ui/issues/68][#68]]). With the note's buffer narrowed - say =org-narrow-to-subtree= on one heading of a journal file - the backlinks widget shows only links targeting IDs inside the restriction (the header count gets a =narrowed= marker so a shorter list reads as scoped, not as missing backlinks), and outline and stats cover only the accessible portion. Outline positions now always point into the widened file, so jumping from a narrowed outline lands on the right heading instead of a spot offset by the restriction start. Narrowing has no change hook, so the sidebar picks the change up on the next idle refresh. =vulpea-ui-respect-narrowing= (default =t=) turns the whole behaviour off.
Expand Down
22 changes: 21 additions & 1 deletion README.org
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,27 @@ list of types.

;; Follow narrowing in the note's buffer (enabled by default). When
;; the buffer is narrowed (e.g. org-narrow-to-subtree), the backlinks
;; widget shows only links targeting IDs inside the restriction, and
;; widget shows only links targeting IDs inside the restriction, the
;; links widget shows only links positioned inside it, and
;; outline/stats cover only the accessible portion. Set to nil to
;; always see the whole file.
(setq vulpea-ui-respect-narrowing t)
#+end_src

Emacs has no narrowing hook, so the sidebar notices a narrow or widen
on the next idle refresh. If you want it instantly, refresh right
after toggling:

#+begin_src emacs-lisp
(defun my/org-toggle-narrow-and-sidebar ()
"Toggle subtree narrowing and refresh the sidebar immediately."
(interactive)
(org-toggle-narrow-to-subtree)
(vulpea-ui-sidebar-refresh))

(define-key org-mode-map (kbd "M-<insert>") #'my/org-toggle-narrow-and-sidebar)
#+end_src

* Commands

| Command | Description |
Expand Down Expand Up @@ -562,6 +577,11 @@ affecting both mention widgets), customise =vulpea-mentions-note-filter=.

Shows notes that the current note links to.

When the buffer is narrowed, only links positioned inside the
restriction are counted, and the header count is marked as narrowed
(see =vulpea-ui-respect-narrowing=). Link positions come from the
database, so unsaved edits can shift them until the next sync.

** Outgoing mentions widget

The mirror of the [[*Unlinked mentions widget][unlinked mentions widget]]:
Expand Down
60 changes: 55 additions & 5 deletions test/vulpea-ui-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -714,15 +714,44 @@ keep their :target-title annotation."
(should (member "file-id" queried-ids))
(should (member "id-b" queried-ids)))))

(ert-deftest vulpea-ui-test-backlinks-count-display ()
(ert-deftest vulpea-ui-test-scoped-count-display ()
"The count display marks a narrowed view."
(should (equal (vulpea-ui--backlinks-count-display 3 3 nil) "3"))
(should (equal (vulpea-ui--backlinks-count-display 1 3 nil) "1/3"))
(should (equal (vulpea-ui--backlinks-count-display 2 2 '(:ids ("id-b")))
(should (equal (vulpea-ui--scoped-count-display 3 3 nil) "3"))
(should (equal (vulpea-ui--scoped-count-display 1 3 nil) "1/3"))
(should (equal (vulpea-ui--scoped-count-display 2 2 '(:ids ("id-b")))
"2 · narrowed"))
(should (equal (vulpea-ui--backlinks-count-display 1 2 '(:ids ("id-b")))
(should (equal (vulpea-ui--scoped-count-display 1 2 '(:ids ("id-b")))
"1/2 · narrowed")))

(ert-deftest vulpea-ui-test-forward-links-scoped ()
"A narrowing scope keeps only links positioned inside the restriction."
(let* ((src (vulpea-ui-test--make-linked-note
"src" "Source" "/tmp/vulpea-ui-test-src.org"
'((:type "id" :dest "a" :pos 10)
(:type "id" :dest "b" :pos 150)
(:type "id" :dest "c" :pos 250))))
(note-a (vulpea-ui-test--make-linked-note
"a" "A" "/tmp/vulpea-ui-test-a.org" nil))
(note-b (vulpea-ui-test--make-linked-note
"b" "B" "/tmp/vulpea-ui-test-b.org" nil))
(note-c (vulpea-ui-test--make-linked-note
"c" "C" "/tmp/vulpea-ui-test-c.org" nil)))
(cl-letf (((symbol-function 'vulpea-db-query-by-file-paths)
(lambda (&rest _) (list src)))
((symbol-function 'vulpea-db-query-by-ids)
(lambda (ids)
(seq-filter (lambda (n) (member (vulpea-note-id n) ids))
(list note-a note-b note-c)))))
;; Scoped: only the link inside [100, 200) survives
(should (equal (mapcar (lambda (r) (vulpea-note-id (plist-get r :note)))
(vulpea-ui--get-forward-links
src '(:beg 100 :end 200 :ids ("id-x"))))
'("b")))
;; No scope: everything, as before
(should (equal (mapcar (lambda (r) (vulpea-note-id (plist-get r :note)))
(vulpea-ui--get-forward-links src))
'("a" "b" "c"))))))

(ert-deftest vulpea-ui-test-compute-stats-respects-narrowing ()
"Stats cover only the restriction while narrowed.
Chars and words come from the accessible portion; links are filtered
Expand Down Expand Up @@ -858,6 +887,27 @@ carries the narrowed marker."
(should (equal seen marker))
(should (string-match-p "Backlinks (0 · narrowed)" output)))))))

(ert-deftest vulpea-ui-test-links-widget-consumes-scope-context ()
"The links widget takes its scope from the context.
The provided scope reaches the collection function and the header
count carries the narrowed marker."
(vulpea-ui-test--with-clean-registry
(vulpea-ui-register-widget 'links
:component 'vulpea-ui-widget-links
:order 100)
(let ((note (vulpea-ui-test--make-mock-note "scoped" "Scoped"))
(marker '(:beg 10 :end 20 :ids ("id-x")))
(seen 'unset))
(cl-letf (((symbol-function 'vulpea-ui-narrowing-scope)
(lambda (_note) marker))
((symbol-function 'vulpea-ui--get-forward-links)
(lambda (_note &optional scope)
(setq seen scope)
nil)))
(vulpea-ui-test--mount-sidebar-root note
(should (equal seen marker))
(should (string-match-p "Links (0 · narrowed)" output)))))))

(ert-deftest vulpea-ui-test-stats-widget-narrowed-through-context ()
"A narrowed buffer flows through the context into the stats widget."
(vulpea-ui-test--with-narrowing-buffer
Expand Down
44 changes: 31 additions & 13 deletions vulpea-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -1205,7 +1205,7 @@ narrowed marker."
(groups (plist-get result :groups))
(filtered-count (plist-get result :filtered-count))
(total-count (plist-get result :total-count))
(count-display (vulpea-ui--backlinks-count-display
(count-display (vulpea-ui--scoped-count-display
filtered-count total-count scope)))
(vui-component 'vulpea-ui-widget
:title "Backlinks"
Expand All @@ -1218,12 +1218,12 @@ narrowed marker."
(seq-map #'vulpea-ui--render-backlink-group groups))
(vui-muted "No backlinks"))))))))

(defun vulpea-ui--backlinks-count-display (filtered total scope)
"Format the backlinks header count.
FILTERED and TOTAL are the mention counts after and before
`vulpea-ui-backlinks-note-filter' and context-type filtering. A
non-nil SCOPE (see `vulpea-ui--narrowing-scope') appends a narrowed
marker so a shrunken list reads as scoped, not as missing backlinks."
(defun vulpea-ui--scoped-count-display (filtered total scope)
"Format a widget header count that may be narrowing-scoped.
FILTERED and TOTAL are the item counts after and before presentation
filtering; widgets without such filtering pass the same number twice.
A non-nil SCOPE (see `vulpea-ui-narrowing-scope') appends a narrowed
marker so a shrunken list reads as scoped, not as missing items."
(let ((base (if (= filtered total)
(format "%d" filtered)
(format "%d/%d" filtered total))))
Expand Down Expand Up @@ -1737,15 +1737,20 @@ indicating which heading of the current note the mention targets."
;;; Forward links widget

(vui-defcomponent vulpea-ui-widget-links ()
"Widget displaying notes that the current note links to."
"Widget displaying notes that the current note links to.
When the note's buffer is narrowed (and `vulpea-ui-respect-narrowing'
is enabled), only links positioned inside the restriction are shown,
and the header count carries a narrowed marker."
:render
(let ((note (use-vulpea-ui-note)))
(when note
(let ((forward-links (vui-use-memo (note)
(vulpea-ui--get-forward-links note))))
(let* ((scope (use-vulpea-ui-scope))
(forward-links (vui-use-memo (note scope)
(vulpea-ui--get-forward-links note scope))))
(vui-component 'vulpea-ui-widget
:title "Links"
:count (length forward-links)
:count (vulpea-ui--scoped-count-display
(length forward-links) (length forward-links) scope)
:children
(lambda ()
(if forward-links
Expand All @@ -1763,9 +1768,14 @@ indicating which heading of the current note the mention targets."
forward-links))
(vui-muted "No links"))))))))

(defun vulpea-ui--get-forward-links (note)
(defun vulpea-ui--get-forward-links (note &optional scope)
"Get all notes linked from NOTE's file with counts.
Collects links from all headings in the file, not just the current note.
Collects links from all headings in the file, not just the current
note. A non-nil SCOPE (a narrowing scope plist, see
`vulpea-ui-narrowing-scope') keeps only links whose position falls
inside the restriction, so a narrowed buffer shows only what the
accessible portion links to. Positions come from the database, so
unsaved edits can shift them until the next sync.
Returns a list of plists with :note and :count, sorted by title."
(when note
(let* ((path (vulpea-note-path note))
Expand All @@ -1775,6 +1785,14 @@ Returns a list of plists with :note and :count, sorted by title."
(all-links (seq-mapcat #'vulpea-note-links file-notes))
;; Filter to note-to-note links (see `vulpea-ui-link-types')
(id-links (seq-filter #'vulpea-ui--note-link-p all-links))
(id-links (if scope
(let ((beg (plist-get scope :beg))
(end (plist-get scope :end)))
(seq-filter (lambda (link)
(let ((pos (plist-get link :pos)))
(and pos (>= pos beg) (< pos end))))
id-links))
id-links))
;; Count occurrences of each destination ID
(id-counts (make-hash-table :test 'equal)))
(dolist (link id-links)
Expand Down
Loading