Subject: bug#68235: 29.1.90; Switching tabs stops following process output in selected window

From: martin rudalics <[email-masked]>
Date: Mon, 19 Feb 2024 10:42:09 +0100

>> You mean to do away with the KEEP-WINDOWS argument and keep windows iff
 >> `window-state-put-keep-window-functions' is non-nil?
 >
 > Yes, I meant removing the KEEP-WINDOWS argument.

Patch attached.  I now provide only one hook I called
'window-kept-windows-functions' which is run by both
'set-window-configuration' and 'window-state-put'.
Tested with

(defun foo (frame windows)
   (while windows
     (let* ((quad (car windows))
	   (window (car quad))
	   (name (buffer-file-name (nth 1 quad)))
	   (buffer
	    (and name (find-file-noselect name))))
       (when buffer
	(set-window-buffer window buffer)
	(set-window-point window (nth 3 quad))
	(set-window-start window (nth 2 quad) t)))
     (setq windows (cdr windows))))

(add-hook 'window-kept-windows-functions 'foo)

(let ((window (selected-window))
       (buffer (pop-to-buffer
	       (find-file-noselect "...")))
       (window-1 (split-window))
       (window-2 (split-window nil nil t))
       configuration)
   (set-window-point window-1 5000)
   (set-window-point window-2 10000)
   (setq configuration (current-window-configuration))
   (y-or-n-p "Configuration saved ...")
   (delete-other-windows window)
   (kill-buffer buffer)
   (y-or-n-p "Configuration reset ...")
   (set-window-configuration configuration)
   (message "Configuration restored"))

(let ((window (selected-window))
       (buffer (pop-to-buffer
	       (find-file-noselect "...")))
       (window-1 (split-window))
       (window-2 (split-window nil nil t))
       state)
   (set-window-point window-1 5000)
   (set-window-point window-2 10000)
   (setq state (window-state-get))
   (y-or-n-p "State saved ...")
   (delete-other-windows window)
   (kill-buffer buffer)
   (y-or-n-p "State reset ...")
   (window-state-put state)
   (message "State restored"))

 >> This would be up to you to decide.  Only some practice can tell what's
 >> better here.
 >
 > Otherwise users won't be able to use this hook for existing commands
 > that don't set the KEEP-WINDOWS argument.

OK.  But note that we now run this hook even if nothing in the window
configuration changes, typically, when calling 'read-minibuffer'.  So
use it with due care (for example, have the tab code bind it exclusively
around its calls of 'set-window-configuration').

martin

diff --git a/lisp/window.el b/lisp/window.el
index e100f25526b..868eef0b63c 100644
--- a/lisp/window.el
+++ b/lisp/window.el
@@ -6174,6 +6174,9 @@ window-state-put-list
 (defvar window-state-put-stale-windows nil
   "Helper variable for `window-state-put'.")
 
+(defvar window-state-put-kept-windows nil
+  "Helper variable for `window-state-put'.")
+
 (defun window--state-put-1 (state &optional window ignore totals pixelwise)
   "Helper function for `window-state-put'."
   (let ((type (car state)))
@@ -6278,9 +6281,10 @@ window--state-put-2
 	  (set-window-parameter window (car parameter) (cdr parameter))))
       ;; Process buffer related state.
       (when state
-	(let ((buffer (get-buffer (car state)))
-	      (state (cdr state)))
-	  (if buffer
+	(let* ((old-buffer-or-name (car state))
+	       (buffer (get-buffer old-buffer-or-name))
+	       (state (cdr state)))
+	  (if (buffer-live-p buffer)
 	      (with-current-buffer buffer
 		(set-window-buffer window buffer)
 		(set-window-hscroll window (cdr (assq 'hscroll state)))
@@ -6375,7 +6379,20 @@ window--state-put-2
 	    ;; save the window with the intention of deleting it later
 	    ;; if possible.
 	    (switch-to-prev-buffer window)
-	    (push window window-state-put-stale-windows)))))))
+	    (if window-kept-windows-functions
+		(let* ((start (cdr (assq 'start state)))
+		       ;; Handle both - marker positions from writable
+		       ;; states and markers from non-writable states.
+		       (start-pos (if (markerp start)
+				      (marker-last-position start)
+				    start))
+		       (point (cdr (assq 'point state)))
+		       (point-pos (if (markerp point)
+				      (marker-last-position point)
+				    point)))
+		  (push (list window old-buffer-or-name start-pos point-pos)
+			window-state-put-kept-windows))
+	      (push window window-state-put-stale-windows))))))))
 
 (defun window-state-put (state &optional window ignore)
   "Put window state STATE into WINDOW.
@@ -6388,8 +6405,20 @@ window-state-put
 Optional argument IGNORE non-nil means ignore minimum window
 sizes and fixed size restrictions.  IGNORE equal `safe' means
 windows can get as small as `window-safe-min-height' and
-`window-safe-min-width'."
+`window-safe-min-width'.
+
+If the abnormal hook `window-kept-windows-functions' is non-nil,
+never delete any windows saved by STATE whose buffers were
+deleted since STATE was saved.  Rather show some live buffer in
+them and call each function in `window-kept-windows-functions'
+with a list of two arguments: the frame where STATE was put and a
+list of entries for each such window.  Each entry contains four
+elements - the window, its old buffer and the last positions of
+`window-start' and `window-point' for the buffer in that window.
+Always check the window for liveness because another function run
+by this hook may have deleted it."
   (setq window-state-put-stale-windows nil)
+  (setq window-state-put-kept-windows nil)
 
   ;; When WINDOW is internal or nil, reduce it to a live one,
   ;; then create a new window on the same frame to put STATE into.
@@ -6492,12 +6521,17 @@ window-state-put
 	(window--state-put-2 ignore pixelwise))
       (while window-state-put-stale-windows
 	(let ((window (pop window-state-put-stale-windows)))
-          ;; Avoid that 'window-deletable-p' throws an error if window
+	  ;; Avoid that 'window-deletable-p' throws an error if window
           ;; was already deleted when exiting 'with-temp-buffer' above
           ;; (Bug#54028).
 	  (when (and (window-valid-p window)
                      (eq (window-deletable-p window) t))
 	    (delete-window window))))
+      (when window-kept-windows-functions
+	(run-hook-with-args
+	 'window-kept-windows-functions
+	 frame window-state-put-kept-windows)
+	(setq window-state-put-kept-windows nil))
       (window--check frame))))
 
 (defun window-state-buffers (state)
diff --git a/src/marker.c b/src/marker.c
index 377f6fbe8db..14b9f63f0cd 100644
--- a/src/marker.c
+++ b/src/marker.c
@@ -458,6 +458,18 @@ DEFUN ("marker-position", Fmarker_position, Smarker_position, 1, 1, 0,
   return Qnil;
 }
 
+DEFUN ("marker-last-position", Fmarker_last_position, Smarker_last_position, 1, 1, 0,
+       doc: /* Return last position of MARKER in its buffer.
+This is like `marker-position' with one exception:  If the buffer of
+MARKER is dead, it returns the last position of MARKER in that buffer
+before it was killed.  */)
+  (Lisp_Object marker)
+{
+  CHECK_MARKER (marker);
+
+  return make_fixnum (XMARKER (marker)->charpos);
+}
+
 /* Change M so it points to B at CHARPOS and BYTEPOS.  */
 
 static void
@@ -825,6 +837,7 @@ verify_bytepos (ptrdiff_t charpos)
 syms_of_marker (void)
 {
   defsubr (&Smarker_position);
+  defsubr (&Smarker_last_position);
   defsubr (&Smarker_buffer);
   defsubr (&Sset_marker);
   defsubr (&Scopy_marker);
diff --git a/src/window.c b/src/window.c
index 3a54f7ce7b1..8e002d70db6 100644
--- a/src/window.c
+++ b/src/window.c
@@ -7090,6 +7090,24 @@ DEFUN ("set-window-configuration", Fset_window_configuration,
 the mini-window of the frame doesn't get set to the corresponding element
 of CONFIGURATION.
 
+Normally, this function will try to delete any dead window in
+CONFIGURATION whose buffer has been deleted since CONFIGURATION was
+made.  However, if the abnormal hook `window-kept-windows-functions' is
+non-nil, it will preserve such a window in the restored layout and show
+another buffer in it.
+
+After restoring the frame layout, this function runs the abnormal hook
+`window-kept-windows-functions' with two arguments - the frame whose
+layout it has restored and a list of entries for each window whose
+buffer has been found dead when it tried to restore CONFIGURATION: Each
+entry is a list of four elements <window, buffer, start, point> where
+`window' denotes the window whose buffer was found dead, `buffer'
+denotes the dead buffer, and `start' and `point' denote the last known
+positions of `window-start' and `window-point' of the buffer in that
+window.  Any function run by this hook should check the window for
+liveness because another function run by this hook may have deleted it
+in the meantime."
+
 If CONFIGURATION was made from a frame that is now deleted,
 only frame-independent values can be restored.  In this case,
 the return value is nil.  Otherwise the value is t.  */)
@@ -7100,6 +7118,7 @@ DEFUN ("set-window-configuration", Fset_window_configuration,
   struct Lisp_Vector *saved_windows;
   Lisp_Object new_current_buffer;
   Lisp_Object frame;
+  Lisp_Object kept_windows = Qnil;
   Lisp_Object old_frame = selected_frame;
   struct frame *f;
   ptrdiff_t old_point = -1;
@@ -7340,6 +7359,11 @@ DEFUN ("set-window-configuration", Fset_window_configuration,
 		   BUF_PT (XBUFFER (w->contents)),
 		   BUF_PT_BYTE (XBUFFER (w->contents)));
 	      w->start_at_line_beg = true;
+	      if (!NILP (Vwindow_kept_windows_functions))
+		kept_windows = Fcons (list4 (window, p->buffer,
+					     Fmarker_last_position (p->start),
+					     Fmarker_last_position (p->pointm)),
+				      kept_windows);
 	    }
 	  else if (!NILP (w->start))
 	    /* Leaf window has no live buffer, get one.  */
@@ -7360,6 +7384,11 @@ DEFUN ("set-window-configuration", Fset_window_configuration,
 		dead_windows = Fcons (window, dead_windows);
 	      /* Make sure window is no more dedicated.  */
 	      wset_dedicated (w, Qnil);
+	      if (!NILP (Vwindow_kept_windows_functions))
+		kept_windows = Fcons (list4 (window, p->buffer,
+					     Fmarker_last_position (p->start),
+					     Fmarker_last_position (p->pointm)),
+				      kept_windows);
 	    }
 	}
 
@@ -7411,12 +7440,13 @@ DEFUN ("set-window-configuration", Fset_window_configuration,
       unblock_input ();
 
       /* Scan dead buffer windows.  */
-      for (; CONSP (dead_windows); dead_windows = XCDR (dead_windows))
-	{
-	  window = XCAR (dead_windows);
-	  if (WINDOW_LIVE_P (window) && !EQ (window, FRAME_ROOT_WINDOW (f)))
-	    delete_deletable_window (window);
-	}
+      if (!NILP (Vwindow_kept_windows_functions))
+	for (; CONSP (dead_windows); dead_windows = XCDR (dead_windows))
+	  {
+	    window = XCAR (dead_windows);
+	    if (WINDOW_LIVE_P (window) && !EQ (window, FRAME_ROOT_WINDOW (f)))
+	      delete_deletable_window (window);
+	  }
 
       /* Record the selected window's buffer here.  The window should
 	 already be the selected one from the call above.  */
@@ -7463,6 +7493,11 @@ DEFUN ("set-window-configuration", Fset_window_configuration,
   minibuf_selected_window = data->minibuf_selected_window;
 
   SAFE_FREE ();
+
+  if (!NILP (Vrun_hooks) && !NILP (Vwindow_kept_windows_functions))
+    run_hook_with_args_2 (Qwindow_kept_windows_functions, frame,
+			  kept_windows);
+
   return FRAME_LIVE_P (f) ? Qt : Qnil;
 }
 
@@ -8460,6 +8495,8 @@ syms_of_window (void)
   DEFSYM (Qheader_line_format, "header-line-format");
   DEFSYM (Qtab_line_format, "tab-line-format");
   DEFSYM (Qno_other_window, "no-other-window");
+  DEFSYM (Qwindow_kept_windows_functions,
+	  "window-kept-windows-functions");
 
   DEFVAR_LISP ("temp-buffer-show-function", Vtemp_buffer_show_function,
 	       doc: /* Non-nil means call as function to display a help buffer.
@@ -8617,6 +8654,28 @@ syms_of_window (void)
 call is performed with the frame temporarily selected.  */);
   Vwindow_configuration_change_hook = Qnil;
 
+  DEFVAR_LISP ("window-kept-windows-functions",
+	       Vwindow_kept_windows_functions,
+	       doc: /* Functions run after restoring a window configuration or state.
+These functions are called by `set-window-configuration' and
+`window-state-put'.  When the value of this variable is non-nil, these
+functions restore any window whose buffer has been deleted since the
+corresponding configuration or state was saved.  Rather than deleting
+such a window, `set-window-configuration' and `window-state-put' show
+some live buffer in it.
+
+The value should be a list of functions that take two arguments.  The
+first argument specifies the frame whose configuration has been
+restored.  The second argument, if non-nil, specifies a list of entries
+for each window whose buffer has been found dead at the time
+'set-window-configuration' or `window-state-put' tried to restore it in
+that window.  Each entry is a list of four values - the window whose
+buffer was found dead, the dead buffer, and the positions of start and
+point of the buffer in that window.  Note that the window may be already
+dead since another function on this list may have deleted it in the
+meantime.  */);
+  Vwindow_kept_windows_functions = Qnil;
+
   DEFVAR_LISP ("recenter-redisplay", Vrecenter_redisplay,
 	       doc: /* Non-nil means `recenter' redraws entire frame.
 If this option is non-nil, then the `recenter' command with a nil