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
