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

From: martin rudalics <[email-masked]>
Date: Tue, 6 Feb 2024 11:34:54 +0100

> Is 'keep-windows' doable for 'window-state-put' as well?

I attach a patch that adds a fourth argument to 'window-state-put'.  It
and a new 'set-window-configuration' now use a new function I called
'marker-last-position' that returns the last position of a marker even
after its buffer was killed.

The patch also fixes a bug in 'window--state-put-2' that can be
reproduced with the following simple scenario

(let ((buffer (get-buffer-create "*foo*"))
       state)
   (pop-to-buffer buffer)
   (setq state (window-state-get))
   (kill-buffer buffer)
   (window-state-put state))

Did you never see it?

 > So maybe the same option 'keep-windows' could call the same hook
 > 'post-set-window-configuration-functions' from 'window-state-put' too?

I added a new hook called 'window-state-put-keep-window-functions' with
the same arguments as 'post-set-window-configuration-functions'.  Maybe
people wanted to keep them apart.  If you think the hook should be also
run when there are no "kept" windows, we can do that as well.

I tested it here with

(defun foo (frame windows)
   (while windows
     (let* ((quad (car windows))
	   (window (car quad))
	   (buffer (find-file-noselect
		    (buffer-file-name (nth 1 quad)))))
       (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-state-put-keep-window-functions 'foo)

(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 nil nil t)
   (message "State restored"))

martin

diff --git a/lisp/window.el b/lisp/window.el
index e100f25526b..3fa06b0708c 100644
--- a/lisp/window.el
+++ b/lisp/window.el
@@ -6168,12 +6168,25 @@ window-state-get
      (min-pixel-width-safe    . ,(window-min-size window t 'safe t)))
    (window--state-get-1 window writable)))
 
+(defvar window-state-put-keep-window-functions nil
+  "List of functions run by `window-state-put' for windows to keep.
+These functions are called when `window-state-put' has been
+invoked with KEEP-WINDOWS non-nil.  `window-state-put' runs them
+after it has restored STATE, for each window whose buffer has
+been deleted since STATE was saved.  Each function is called with
+two arguments.  The frame on which the window was restored and a
+quadruple of four entries - the window, its old buffer and that
+buffer's start and point positions.")
+
 (defvar window-state-put-list nil
   "Helper variable for `window-state-put'.")
 
 (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)))
@@ -6258,7 +6271,7 @@ window--state-put-1
 	    ;; Continue with the last window split off.
 	    (setq window new))))))))
 
-(defun window--state-put-2 (ignore pixelwise)
+(defun window--state-put-2 (ignore pixelwise keep-windows)
   "Helper function for `window-state-put'."
   (dolist (item window-state-put-list)
     (let ((window (car item))
@@ -6278,9 +6291,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,9 +6389,22 @@ 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)))))))
-
-(defun window-state-put (state &optional window ignore)
+	    (if keep-windows
+		(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 keep-windows)
   "Put window state STATE into WINDOW.
 STATE should be the state of a window returned by an earlier
 invocation of `window-state-get'.  Optional argument WINDOW must
@@ -6388,8 +6415,19 @@ 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'.
+
+Optional argument KEEP-WINDOWS non-nil means to never delete any
+windows saved by STATE whose buffers were deleted since STATE was
+saved.  `window-state-put' records such windows in a list and
+calls the functions in `window-state-put-keep-window-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.
+KEEP-WINDOWS nil means such windows are deleted, if possible."
   (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.
@@ -6489,15 +6527,21 @@ window-state-put
       (with-temp-buffer
 	(set-window-buffer window (current-buffer))
 	(window--state-put-1 state window nil totals pixelwise)
-	(window--state-put-2 ignore pixelwise))
+	(window--state-put-2 ignore pixelwise keep-windows))
       (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-state-put-kept-windows
+	(when window-state-put-keep-window-functions
+	  (run-hook-with-args
+	   'window-state-put-keep-window-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/keyboard.c b/src/keyboard.c
index e1d738dd6ef..b6ba3c57ff8 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -2281,7 +2281,7 @@ read_char_help_form_unwind (void)
   Lisp_Object window_config = XCAR (help_form_saved_window_configs);
   help_form_saved_window_configs = XCDR (help_form_saved_window_configs);
   if (!NILP (window_config))
-    Fset_window_configuration (window_config, Qnil, Qnil);
+    Fset_window_configuration (window_config, Qnil, Qnil, Qnil);
 }
 
 #define STOP_POLLING					\
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..cb37983b648 100644
--- a/src/window.c
+++ b/src/window.c
@@ -7079,7 +7079,7 @@ DEFUN ("window-configuration-frame", Fwindow_configuration_frame, Swindow_config
 }
 
 DEFUN ("set-window-configuration", Fset_window_configuration,
-       Sset_window_configuration, 1, 3, 0,
+       Sset_window_configuration, 1, 4, 0,
        doc: /* Set the configuration of windows and buffers as specified by CONFIGURATION.
 CONFIGURATION must be a value previously returned
 by `current-window-configuration' (which see).
@@ -7090,16 +7090,35 @@ 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 KEEP-WINDOWS 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
+'post-set-window-configuration-functions' with two arguments - the frame
+whose layout is has restored and, provided KEEP-WINDOWS is non-nil, 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 positions of `window-start' and `window-point' of
+that window at the time CONFIGURATION was made. Note that these
+positions are no markers and may be no more accurate if the buffer has
+been modified afterwards.  If KEEP-WINDOWS is nil, the second argument
+is nil.
+
 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.  */)
   (Lisp_Object configuration, Lisp_Object dont_set_frame,
-   Lisp_Object dont_set_miniwindow)
+   Lisp_Object dont_set_miniwindow, Lisp_Object keep_windows)
 {
   register struct save_window_data *data;
   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 (keep_windows))
+		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 (keep_windows))
+		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 (keep_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);
+	  }
 
       /* 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 (Vpost_set_window_configuration_functions))
+    run_hook_with_args_2 (Qpost_set_window_configuration_functions, frame,
+			  kept_windows);
+
   return FRAME_LIVE_P (f) ? Qt : Qnil;
 }
 
@@ -7472,12 +7507,12 @@ restore_window_configuration (Lisp_Object configuration)
   if (CONSP (configuration))
     Fset_window_configuration (XCAR (configuration),
 			       Fcar_safe (XCDR (configuration)),
-			       Fcar_safe (Fcdr_safe (XCDR (configuration))));
+			       Fcar_safe (Fcdr_safe (XCDR (configuration))),
+			       Qnil);
   else
-    Fset_window_configuration (configuration, Qnil, Qnil);
+    Fset_window_configuration (configuration, Qnil, Qnil, Qnil);
 }
 
-
 /* If WINDOW is an internal window, recursively delete all child windows
    reachable via the next and contents slots of WINDOW.  Otherwise setup
    WINDOW to not show any buffer.  */
@@ -7690,6 +7725,7 @@ save_window_save (Lisp_Object window, struct Lisp_Vector *vector, ptrdiff_t i)
 				      BUF_PT_BYTE (XBUFFER (w->contents)));
 	  else
 	    p->pointm = Fcopy_marker (w->pointm, Qnil);
+
 	  p->old_pointm = Fcopy_marker (w->old_pointm, Qnil);
 	  XMARKER (p->pointm)->insertion_type = window_point_insertion_type;
 	  XMARKER (p->old_pointm)->insertion_type = window_point_insertion_type;
@@ -8460,6 +8496,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 (Qpost_set_window_configuration_functions,
+	  "post-set-window-configuration-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 +8655,21 @@ syms_of_window (void)
 call is performed with the frame temporarily selected.  */);
   Vwindow_configuration_change_hook = Qnil;
 
+  DEFVAR_LISP ("post-set-window-configuration-functions",
+	       Vpost_set_window_configuration_functions,
+	       doc: /* Functions called after restoring a window configuration.
+The value should be a list of functions that take two arguments.
+
+This function is called by `set-window-configuration' after it has
+restored the layout of a frame.  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' 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.  */);
+  Vpost_set_window_configuration_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