Hi Ihor, Thanks for the review! I agree with your points and updated the patch:
Ihor Radchenko <[email protected]> writes: > Now, I find the function name misleading. > Previously, it moved to the beginning "top level" and stored the point > position as side effect. Now, it does more, which is surprising if one > just looks at the function name. Reverted `org-columns-goto-top-level' to its original behavior and moved the full scope logic into `org-columns--set-scope'. > Also, the docstring is not very clear. What does "use the accessible > portion of the buffer" mean? It is not obvious that it implies ignoring > current subtree. Rewrote the docstring for `org-columns--set-scope' to clarify buffer scope and narrowing. Attached is the updated v2 patch. Best, -- Slawomir Grochowski
>From dc2e11a0fb4eb3b9468c994d807b72ae508a644d Mon Sep 17 00:00:00 2001 From: Slawomir Grochowski <[email protected]> Date: Sun, 30 Aug 2026 20:00:35 +0200 Subject: [PATCH] org-colview: Determine column view scope once * lisp/org-colview.el (org-columns--scope-end-marker): New variable. (org-columns--set-scope): New function. (org-columns-get-format-and-top-level): Call `org-columns--set-scope'. (org-columns-remove-overlays, org-columns--capture-view): Clean up `org-columns--scope-end-marker'. (org-columns--prepare-rows): Remove GLOBAL argument and narrow to recorded scope. (org-columns): Call `org-columns--set-scope'. (org-columns--compute-spec): Walk backward from `org-columns--scope-end-marker'. * testing/lisp/test-org-colview.el (test-org-colview/columns-scope): Test summaries across multiple top-level trees in global column view. Previously, `org-columns--compute-spec' assumed that the scope always ended at the end of the subtree starting at `org-columns-top-level-marker'. When column view was invoked globally, summaries were only computed for the first top-level tree. Establish the scope boundaries once at the entry points and reuse them across summary computation and row preparation. --- lisp/org-colview.el | 69 +++++++++++++++++++++++--------- testing/lisp/test-org-colview.el | 27 ++++++++++++- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/lisp/org-colview.el b/lisp/org-colview.el index 9556c2957..41d5d0624 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 + "Points to the position where the current columns region ends.") + (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) @@ -1043,7 +1048,7 @@ dynamic scoping for `org-overriding-columns-format'.") ;;;###autoload (defun org-columns-get-format-and-top-level () (prog1 (org-columns-get-format) - (org-columns-goto-top-level))) + (org-columns--set-scope))) (defun org-columns--get-columns-keyword () "Return the first COLUMNS keyword value in the current buffer." @@ -1080,6 +1085,34 @@ Also sets `org-columns-top-level-marker' to the new position." ((org-entry-get nil "COLUMNS" t) org-entry-property-inherited-from) (t (org-back-to-heading t) (point))))))) +(defun org-columns--set-scope (&optional global) + "Set column view scope markers and move point to the scope beginning. + +When optional argument GLOBAL is non-nil, the scope covers the entire +buffer (or the accessible portion if the buffer is narrowed), +regardless of the heading at point. + +When GLOBAL is nil: +- If point is before the first heading, the scope covers the whole + accessible buffer. +- Otherwise, the scope is restricted to the current subtree, or to + the subtree of the first ancestor heading defining a \"COLUMNS\" + property. + +Set `org-columns-top-level-marker' to the beginning of the scope and +`org-columns--scope-end-marker' to its end, then move point to +`org-columns-top-level-marker'." + (when global (goto-char (point-min))) + (let ((before-first (org-before-first-heading-p))) + (org-columns-goto-top-level) + (setq org-columns--scope-end-marker + (org-move-marker org-columns--scope-end-marker + (if (or global before-first) + (point-max) + (save-excursion + (org-end-of-subtree t t) + (point))))))) + (defun org-columns--display-rows (rows) "Display the header line and ROWS as column view overlays. ROWS must be a non-empty list of collected column rows." @@ -1090,21 +1123,17 @@ 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) - "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) +(defun org-columns--prepare-rows (columns-format) + "Set up column view and return rows for the recorded scope. +When COLUMNS-FORMAT is non-nil, use it instead of the format selected +from the buffer. + +The scope is bounded by `org-columns-top-level-marker' and +`org-columns--scope-end-marker'." (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-clock-summaries)) (org-columns--collect-rows))) @@ -1123,8 +1152,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--set-scope global) + (when-let* ((rows (org-columns--prepare-rows columns-format))) (org-columns--display-rows rows)))) ;;;;; Column definition editing @@ -1561,10 +1593,8 @@ existing ones in properties drawers." (summarize-function (org-columns--summarize-function operator)) (stack nil)) (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) (let* ((pos (match-beginning 0)) @@ -1750,8 +1780,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--set-scope (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) @@ -1789,6 +1820,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) diff --git a/testing/lisp/test-org-colview.el b/testing/lisp/test-org-colview.el index bfd9cf727..20dd538ed 100644 --- a/testing/lisp/test-org-colview.el +++ b/testing/lisp/test-org-colview.el @@ -389,7 +389,32 @@ 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. + (should + (equal + '(("H1" . "1") + ("S1" . "1") + ("H2" . "2") + ("S2" . "2")) + (org-test-with-temp-text + "* 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.47.3
