Hi,

I have been thinking about this for quite some time and spent a fair
amount of time exploring different approaches.  Initially, I thought
that the scope could be established once using a macro around column
view processing, but that made the code considerably more complicated.

The current approach-recording both scope boundaries when column view
starts and reusing them, seems to be the simplest one.  It also fixes a
bug where global column view computed summaries only for the first
top-level tree, even though all trees were displayed.

I would be grateful for feedback, particularly on whether this is the
right way to represent and reuse the scope.

Best,
-- 
Slawomir Grochowski
>From 8378475bb639801c9e7544c409d0eb92b7146f11 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--prepare-rows, org-columns--compute-spec): Use the recorded
scope.
(org-columns-remove-overlays, org-columns--capture-view): 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 in 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              | 40 +++++++++++++++++++++-----------
 testing/lisp/test-org-colview.el | 25 ++++++++++++++++----
 2 files changed, 48 insertions(+), 17 deletions(-)

diff --git a/lisp/org-colview.el b/lisp/org-colview.el
index 66fdb911f..ac1a9cd53 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.
@@ -1099,13 +1113,13 @@ 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)
+  (org-columns-goto-top-level global)
   (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)))
 
@@ -1579,10 +1593,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))
@@ -1819,6 +1831,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 +2192,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..d0f72bdbe 100644
--- a/testing/lisp/test-org-colview.el
+++ b/testing/lisp/test-org-colview.el
@@ -382,14 +382,31 @@ https://list.orgmode.org/[email protected]/T/#u.";
       (let ((org-columns-default-format "%ITEM")) (org-columns t))
       (org-map-entries
        (lambda () (get-char-property (point) 'org-columns-value))))))
+  ;; Compute summaries separately for each top-level tree when viewing
+  ;; the whole document.
   (should
    (equal
-    '("1" "1")
+    '(("H1" . "1")
+      ("S1" . "1")
+      ("H2" . "2")
+      ("S2" . "2"))
     (org-test-with-temp-text
-	"Top\n* H1\n** <point>H2\n:PROPERTIES:\n:A: 1\n:END:"
-      (let ((org-columns-default-format "%A{+}")) (org-columns t))
+	"* 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 () (get-char-property (point) 'org-columns-value)))))))
+       (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