Hi Ihor,
Thank you for the review.
Ihor Radchenko <[email protected]> writes:
> The docstring of org-columns--prepare-rows says
>
> (defun org-columns--prepare-rows (global columns-format)
> "Set up column view and return rows for the current scope.
> When GLOBAL is non-nil, use the whole buffer as the scope. Otherwise,
>
> But GLOBAL will no longer use the whole buffer.
GLOBAL still covers the entire accessible buffer (from `point-min` to
`point-max`). `org-columns` does not widen narrowed buffers, so in a
narrowed buffer the scope spans the accessible portion.
To make the architecture cleaner and avoid ambiguity, I refactored
`org-columns--prepare-rows` to not take `global` at all. Establishing
the scope is now handled solely by `org-columns-goto-top-level` at the
entry points (`org-columns` and `org-columns--capture-view`), and
`org-columns--prepare-rows` strictly operates on that already recorded
scope.
> Why did you remove the existing test case?
My intention was to generalize the test to cover multiple top-level
trees, but replacing the original test was a mistake (and inadvertently
dropped `<point>` on the sub-heading). I have restored the original
test case and kept the multi-tree test alongside it.
Attached is the updated patch.
Best,
--
Slawomir Grochowski
>From 2811e7caeee56b70ace91c7a6d1f57f91789af66 Mon Sep 17 00:00:00 2001
From: Slawomir Grochowski <[email protected]>
Date: Sat, 8 Aug 2026 20:46:40 +0200
Subject: [PATCH] org-colview: Determine column view scope once
* lisp/org-colview.el (org-columns--scope-end-marker): New variable.
(org-columns-goto-top-level): Accept optional global argument and record
both scope boundaries.
(org-columns): Record starting position and establish scope with
`org-columns-goto-top-level'.
(org-columns--prepare-rows): Remove `global' argument and operate on the
recorded scope.
(org-columns--compute-spec): Use the recorded scope.
(org-columns-remove-overlays): Clear the end marker.
(org-columns--capture-view): Establish scope with
`org-columns-goto-top-level' and clear the end marker.
(org-agenda-colview-compute): Use the recorded global scope.
* testing/lisp/test-org-colview.el (test-org-colview/columns-scope):
Test summaries across multiple top-level trees.
Record both scope boundaries when column view starts and reuse them.
This fixes a bug in global column view. When a buffer begins with a
heading, `org-columns-top-level-marker' points to `point-min'.
`org-columns--compute-spec' then called `org-end-of-subtree' and stopped
at the end of the first top-level tree. Rows were still collected from
the whole buffer, so later top-level trees appeared in the view, but
their parent summaries were not computed.
The missing end boundary caused another inconsistency. A local view
cleared `org-summaries' throughout the accessible buffer instead of only
within its scope.
---
lisp/org-colview.el | 58 +++++++++++++++++++-------------
testing/lisp/test-org-colview.el | 28 ++++++++++++++-
2 files changed, 62 insertions(+), 24 deletions(-)
diff --git a/lisp/org-colview.el b/lisp/org-colview.el
index 66fdb911f..542dedadc 100644
--- a/lisp/org-colview.el
+++ b/lisp/org-colview.el
@@ -183,6 +183,9 @@ This is the compiled version of the format.")
(defvar-local org-columns-top-level-marker nil
"Points to the position where the current columns region starts.")
+(defvar-local org-columns--scope-end-marker nil
+ "Marker at the end of the current column view scope.")
+
(defvar org-columns--time 0.0
"Number of seconds since the epoch, as a floating point number.")
@@ -780,6 +783,8 @@ This is needed to later remove this relative remapping.")
(set-marker org-columns-begin-marker nil))
(when (markerp org-columns-top-level-marker)
(set-marker org-columns-top-level-marker nil))
+ (when (markerp org-columns--scope-end-marker)
+ (set-marker org-columns--scope-end-marker nil))
(when org-columns-overlays
(when (local-variable-p 'org-previous-header-line-format)
(setq header-line-format org-previous-header-line-format)
@@ -1069,16 +1074,25 @@ back to the next source, ultimately to
(org-columns-compile-format selected-columns-format)
selected-columns-format))
-(defun org-columns-goto-top-level ()
+(defun org-columns-goto-top-level (&optional global)
"Move to the beginning of the column view area.
-Also sets `org-columns-top-level-marker' to the new position."
+Also set `org-columns-top-level-marker' and
+`org-columns--scope-end-marker' to the scope boundaries. When
+optional argument GLOBAL is non-nil, use the accessible portion of
+the buffer."
(goto-char
(setq org-columns-top-level-marker
(org-move-marker
org-columns-top-level-marker
- (cond ((org-before-first-heading-p) (point-min))
+ (cond ((or global (org-before-first-heading-p)) (point-min))
((org-entry-get nil "COLUMNS" t) org-entry-property-inherited-from)
- (t (org-back-to-heading) (point)))))))
+ (t (org-back-to-heading) (point))))))
+ (setq org-columns--scope-end-marker
+ (org-move-marker
+ org-columns--scope-end-marker
+ (if (and (not global) (org-at-heading-p))
+ (save-excursion (org-end-of-subtree t t))
+ (point-max)))))
(defun org-columns--display-rows (rows)
"Display the header line and ROWS as column view overlays.
@@ -1090,22 +1104,16 @@ ROWS must be a non-empty list of collected column rows."
(goto-char (car row))
(org-columns--display-line (cdr row))))
-(defun org-columns--prepare-rows (global columns-format)
+(defun org-columns--prepare-rows (&optional columns-format)
"Set up column view and return rows for the current scope.
-When GLOBAL is non-nil, use the whole buffer as the scope. Otherwise,
-use the subtree selected by `org-columns-goto-top-level'. When
-COLUMNS-FORMAT is non-nil, use it instead of the format selected from
-the buffer."
- (when global (goto-char (point-min)))
- (setq org-columns-begin-marker
- (org-move-marker org-columns-begin-marker))
- (org-columns-goto-top-level)
+When COLUMNS-FORMAT is non-nil, use it instead of the format selected
+from the buffer."
(org-columns-get-format columns-format)
- (unless org-columns-inhibit-recalculation (org-columns-compute-all))
(save-restriction
- (when (and (not global) (org-at-heading-p))
- (narrow-to-region (point) (org-end-of-subtree t t)))
+ (narrow-to-region org-columns-top-level-marker
+ org-columns--scope-end-marker)
(unless org-columns-inhibit-recalculation
+ (org-columns-compute-all)
(org-columns--compute-clock-summaries))
(org-columns--collect-rows)))
@@ -1123,8 +1131,11 @@ When COLUMNS-FORMAT is non-nil, use it as the column format."
(interactive "P" org-mode)
(org-columns-remove-overlays)
(setq-local org-columns-global global)
+ (setq org-columns-begin-marker
+ (org-move-marker org-columns-begin-marker))
(save-excursion
- (when-let* ((rows (org-columns--prepare-rows global columns-format)))
+ (org-columns-goto-top-level global)
+ (when-let* ((rows (org-columns--prepare-rows columns-format)))
(org-columns--display-rows rows))))
;;;;; Column definition editing
@@ -1579,10 +1590,8 @@ existing ones in properties drawers."
(collect-function (org-columns--collect-function operator))
(summarize-function (org-columns--summarize-function operator)))
(org-with-wide-buffer
- ;; Find the region to compute.
- (goto-char org-columns-top-level-marker)
- (org-end-of-subtree t)
;; Walk the tree from the back and do the computations.
+ (goto-char org-columns--scope-end-marker)
(while (re-search-backward
org-outline-regexp-bol org-columns-top-level-marker t)
(unless (= current-level 0) (setq previous-level current-level))
@@ -1780,8 +1789,9 @@ list whose first element is an integer indicating the outline level of
the entry, and whose remaining elements are strings with the contents
for the columns according to COLUMNS-FORMAT."
(org-columns-remove-overlays)
+ (org-columns-goto-top-level (not local))
(let* ((rows (save-excursion
- (org-columns--prepare-rows (not local) columns-format)))
+ (org-columns--prepare-rows columns-format)))
(has-item (assoc "ITEM" org-columns-current-fmt-compiled))
table)
(goto-char org-columns-top-level-marker)
@@ -1819,6 +1829,8 @@ for the columns according to COLUMNS-FORMAT."
(set-marker org-columns-begin-marker nil))
(when (markerp org-columns-top-level-marker)
(set-marker org-columns-top-level-marker nil))
+ (when (markerp org-columns--scope-end-marker)
+ (set-marker org-columns--scope-end-marker nil))
(setq org-columns-current-fmt nil)
;; Add column titles and a horizontal rule in front of the table.
(cons (mapcar #'org-columns--spec-title org-columns-current-fmt-compiled)
@@ -2178,8 +2190,8 @@ This will add overlays to the date lines, to show the summary for each day."
(org-with-wide-buffer
(with-silent-modifications
(remove-text-properties (point-min) (point-max) '(org-summaries t)))
- (goto-char (point-min))
- (org-columns-get-format-and-top-level)
+ (org-columns-goto-top-level 'global)
+ (org-columns-get-format)
(dolist (spec compiled-format)
(let ((prop (org-columns--spec-property spec)))
(cond
diff --git a/testing/lisp/test-org-colview.el b/testing/lisp/test-org-colview.el
index bfd9cf727..74aa8b4b6 100644
--- a/testing/lisp/test-org-colview.el
+++ b/testing/lisp/test-org-colview.el
@@ -389,7 +389,33 @@ https://list.orgmode.org/[email protected]/T/#u."
"Top\n* H1\n** <point>H2\n:PROPERTIES:\n:A: 1\n:END:"
(let ((org-columns-default-format "%A{+}")) (org-columns t))
(org-map-entries
- (lambda () (get-char-property (point) 'org-columns-value)))))))
+ (lambda () (get-char-property (point) 'org-columns-value))))))
+ ;; Compute summaries separately for each top-level tree when viewing
+ ;; the whole document, even with text before the first heading.
+ (should
+ (equal
+ '(("H1" . "1")
+ ("S1" . "1")
+ ("H2" . "2")
+ ("S2" . "2"))
+ (org-test-with-temp-text
+ "Top
+* H1
+** S1
+:PROPERTIES:
+:A: 1
+:END:
+* H2
+** S2
+:PROPERTIES:
+:A: 2
+:END:"
+ (let ((org-columns-default-format "%A{+}"))
+ (org-columns t))
+ (org-map-entries
+ (lambda ()
+ (cons (org-get-heading t t t t)
+ (get-char-property (point) 'org-columns-value))))))))
(ert-deftest test-org-colview/columns-width ()
"Test `org-columns' column widths."
--
2.39.5