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

Reply via email to