From 014b52066abff85eac6b90933eec7d3aa0259fda Mon Sep 17 00:00:00 2001
From: Yuan Fu <casouri@gmail.com>
Date: Thu, 17 Oct 2019 17:35:48 -0400
Subject: [PATCH] Allow user to save/load window layout in gdb-mi
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add a feature that allows a user to save a gdb window layout to a file
with 'gdb-save-window-layout' and load it back it with
'gdb-load-window-layout'. Set a default window configuration by
setting 'gdb-default-window-layout-file'. Note that for the default
window configuration to take effect, 'gdb-many-windows' needs to be t.
In window.el, add 'with-selected-window-undedicated'.
* lisp/progmodes/gdb-mi.el (require): add 'pcase', wrap 'pcase' and 'cl-lib'
inside ‘eval-when-compile’.
(gdb-window-layout-directory, gdb-default-window-layout-file): New
variables.
(gud-menu-map): Add "Load window layout" and "Save window layout" to
menu.
(gdb-get-source-buffer): New function, extracted out of
'gdb-restore-window'.
(gdb-setup-windows): Add a condition branch that loads default window
layout when available.
(gdb-buffer-p, gdb-function-buffer-p, gdb--buffer-type,
gdb-save-window-layout, gdb-load-window-layout): New functions.
(gdb-many-windows, gdb-get-source-file): Add comments.
* lisp/window.el (with-selected-window-undedicated): New function.
---
 lisp/progmodes/gdb-mi.el | 217 +++++++++++++++++++++++++++++++++------
 lisp/window.el           |  13 +++
 2 files changed, 196 insertions(+), 34 deletions(-)

diff --git a/lisp/progmodes/gdb-mi.el b/lisp/progmodes/gdb-mi.el
index b08d487af3..8c7e81d727 100644
--- a/lisp/progmodes/gdb-mi.el
+++ b/lisp/progmodes/gdb-mi.el
@@ -91,7 +91,8 @@
 (require 'gud)
 (require 'json)
 (require 'bindat)
-(require 'cl-lib)
+(eval-when-compile (require 'cl-lib))
+(eval-when-compile (require 'pcase))
 
 (declare-function speedbar-change-initial-expansion-list
                   "speedbar" (new-default))
@@ -589,6 +590,20 @@ gdb-show-main
   :group 'gdb
   :version "22.1")
 
+(defcustom gdb-window-layout-directory user-emacs-directory
+  "The default directory where window configuration files are stored.
+If nil, use `default-directory'."
+  :type 'string
+  :group 'gdb
+  :version "28.1")
+
+(defcustom gdb-default-window-layout-file nil
+  "If non-nil, gdb loads this window layout file on startup.
+If not absolute, GDB will look under `gdb-window-layout-directory'."
+  :type 'string
+  :group 'gdb
+  :version "28.1")
+
 (defvar gdbmi-debug-mode nil
   "When non-nil, print the messages sent/received from GDB/MI in *Messages*.")
 
@@ -4459,6 +4474,8 @@ gdb-preempt-existing-or-display-buffer
   (define-key gud-menu-map [displays]
     `(menu-item "GDB-Windows" ,menu
 		:visible (eq gud-minor-mode 'gdbmi)))
+  (define-key menu [load-layout] '("Load window layout" . gdb-load-window-layout))
+  (define-key menu [save-layout] '("Save window layout" . gdb-save-window-layout))
   (define-key menu [gdb] '("Gdb" . gdb-display-gdb-buffer))
   (define-key menu [threads] '("Threads" . gdb-display-threads-buffer))
   (define-key menu [memory] '("Memory" . gdb-display-memory-buffer))
@@ -4574,41 +4591,169 @@ gdb-set-window-buffer
   (set-window-buffer window (get-buffer name))
   (set-window-dedicated-p window t))
 
+(defun gdb-get-source-buffer ()
+  "Return a buffer displaying source file or nil.
+
+The source file would be the most relevant file or the main file."
+  (if gud-last-last-frame
+      (gud-find-file (car gud-last-last-frame))
+    (when gdb-main-file
+      (gud-find-file gdb-main-file))))
+
 (defun gdb-setup-windows ()
   "Layout the window pattern for option `gdb-many-windows'."
-  (gdb-get-buffer-create 'gdb-locals-buffer)
-  (gdb-get-buffer-create 'gdb-stack-buffer)
-  (gdb-get-buffer-create 'gdb-breakpoints-buffer)
-  (set-window-dedicated-p (selected-window) nil)
-  (switch-to-buffer gud-comint-buffer)
-  (delete-other-windows)
-  (let ((win0 (selected-window))
-        (win1 (split-window nil ( / ( * (window-height) 3) 4)))
-        (win2 (split-window nil ( / (window-height) 3)))
-        (win3 (split-window-right)))
-    (gdb-set-window-buffer (gdb-locals-buffer-name) nil win3)
-    (select-window win2)
-    (set-window-buffer
-     win2
-     (if gud-last-last-frame
-         (gud-find-file (car gud-last-last-frame))
-       (if gdb-main-file
-           (gud-find-file gdb-main-file)
-         ;; Put buffer list in window if we
-         ;; can't find a source file.
-         (list-buffers-noselect))))
-    (setq gdb-source-window (selected-window))
-    (let ((win4 (split-window-right)))
-      (gdb-set-window-buffer
-       (gdb-get-buffer-create 'gdb-inferior-io) nil win4))
-    (select-window win1)
-    (gdb-set-window-buffer (gdb-stack-buffer-name))
-    (let ((win5 (split-window-right)))
-      (gdb-set-window-buffer (if gdb-show-threads-by-default
-                                 (gdb-threads-buffer-name)
-                               (gdb-breakpoints-buffer-name))
-                             nil win5))
-    (select-window win0)))
+  (if gdb-default-window-layout-file
+      (gdb-load-window-layout
+       (if (file-name-absolute-p gdb-default-window-layout-file)
+           gdb-default-window-layout-file
+         (expand-file-name gdb-default-window-layout-file
+                           gdb-window-layout-directory)))
+    ;; Create default layout as before.
+    (gdb-get-buffer-create 'gdb-locals-buffer)
+    (gdb-get-buffer-create 'gdb-stack-buffer)
+    (gdb-get-buffer-create 'gdb-breakpoints-buffer)
+    (set-window-dedicated-p (selected-window) nil)
+    (switch-to-buffer gud-comint-buffer)
+    (delete-other-windows)
+    (let ((win0 (selected-window))
+          (win1 (split-window nil ( / ( * (window-height) 3) 4)))
+          (win2 (split-window nil ( / (window-height) 3)))
+          (win3 (split-window-right)))
+      (gdb-set-window-buffer (gdb-locals-buffer-name) nil win3)
+      (select-window win2)
+      (set-window-buffer
+       win2
+       (or (gdb-get-source-buffer)
+           (list-buffers-noselect)))
+      (setq gdb-source-window (selected-window))
+      (let ((win4 (split-window-right)))
+        (gdb-set-window-buffer
+         (gdb-get-buffer-create 'gdb-inferior-io) nil win4))
+      (select-window win1)
+      (gdb-set-window-buffer (gdb-stack-buffer-name))
+      (let ((win5 (split-window-right)))
+        (gdb-set-window-buffer (if gdb-show-threads-by-default
+                                   (gdb-threads-buffer-name)
+                                 (gdb-breakpoints-buffer-name))
+                               nil win5))
+      (select-window win0))))
+
+(defun gdb-buffer-p (buffer)
+  "Return t if BUFFER is gdb-related."
+  (with-current-buffer buffer
+    (eq gud-minor-mode 'gdbmi)))
+
+(defun gdb-function-buffer-p (buffer)
+  "Return t if BUFFER is a gdb function buffer.
+
+E.g., locals buffer, registers buffer, but don't include the main
+command buffer (the one in where you type gdb commands) or source
+buffers."
+  (with-current-buffer buffer
+    (derived-mode-p 'gdb-parent-mode 'gdb-inferior-io-mode)))
+
+(defun gdb--buffer-type (buffer)
+  "Return the buffer type of BUFFER or nil.
+
+Buffer type is like `gdb-registers-type', `gdb-stack-buffer'.
+This symbol can be passed to `gdb-get-buffer-create'.
+
+Return nil if BUFFER isn't a gdb function buffer."
+  (with-current-buffer buffer
+    (cl-loop for rule in gdb-buffer-rules
+             for mode-name = (gdb-rules-buffer-mode rule)
+             for type = (car rule)
+             if (eq mode-name major-mode)
+             return type
+             finally return nil)))
+
+(defun gdb-save-window-layout (file)
+  "Save current window layout (window configuration) to FILE.
+
+You can later restore this layout from that file by
+`gdb-load-window-layout'."
+  (interactive (list (read-file-name
+                      "Save window configuration to file: "
+                      (or gdb-window-layout-directory default-directory))))
+  ;; We replace the buffer in each window with a placeholder, store
+  ;; the buffer type (register, breakpoint, etc) in window parameters,
+  ;; and write the window configuration to the file.
+  (save-window-excursion
+    (let ((placeholder (get-buffer-create " *gdb-placeholder*"))
+          (window-persistent-parameters
+           (cons '(gdb-buffer-type . writable) window-persistent-parameters))
+          window-config)
+      (unwind-protect
+          (dolist (win (window-list nil 'no-minibuffer))
+            (select-window win)
+            (when (gdb-buffer-p (current-buffer))
+              (set-window-parameter
+               nil 'gdb-buffer-type
+               (cond ((gdb-function-buffer-p (current-buffer))
+                      ;; 1) If a user arranged the window
+                      ;; configuration herself and saves it, windows
+                      ;; are probably not dedicated.  2) We use the
+                      ;; same dedication flag as in
+                      ;; `gdb-display-buffer'.
+                      (set-window-dedicated-p nil t)
+                      ;; We save this gdb-buffer-type symbol so
+                      ;; we can later pass it to `gdb-get-buffer-create';
+                      ;; one example: `gdb-registers-buffer'.
+                      (or (gdb--buffer-type (current-buffer))
+                          (error "Unrecognized gdb buffer mode: %s" major-mode)))
+                     ;; Command buffer.
+                     ((derived-mode-p 'gud-mode) 'command)
+                     ((equal (selected-window) gdb-source-window) 'source)))
+              (with-selected-window-undedicated
+               (set-window-buffer nil placeholder)
+               (set-window-prev-buffers (selected-window) nil)
+               (set-window-next-buffers (selected-window) nil))))
+        ;; Save the window configuration to FILE.
+        (let ((window-config (window-state-get nil t)))
+          (with-temp-buffer
+            (prin1 window-config (current-buffer))
+            (write-file file t)))
+        (kill-buffer placeholder)))))
+
+(defun gdb-load-window-layout (file)
+  "Restore window layout (window configuration) from FILE.
+
+FILE should be a window layout file saved by
+`gdb-save-window-layout'."
+  (interactive (list (read-file-name
+                      "Restore window configuration from file: "
+                      (or gdb-window-layout-directory default-directory))))
+  ;; Basically, we restore window configuration and go through each
+  ;; window and restore the function buffers.
+  (let* ((placeholder (get-buffer-create " *gdb-placeholder*")))
+    (unwind-protect ; Don't leak buffer.
+        (let ((window-config (with-temp-buffer
+                               (insert-file-contents file)
+                               ;; We need to go to point-min even we
+                               ;; are reading the whole buffer.
+                               (goto-char (point-min))
+                               (read (current-buffer))))
+              (source-buffer (if gdb-source-window
+                                 (window-buffer gdb-source-window)
+                               (or (gdb-get-source-buffer)
+                                   ;; Do the same thing as in
+                                   ;; `gdb-setup-windows' if no source
+                                   ;; buffer is found.
+                                   (list-buffers-noselect))))
+              buffer-type)
+          (window-state-put window-config (frame-root-window))
+          (dolist (window (window-list nil 'no-minibuffer))
+            (with-selected-window window
+              (setq buffer-type (window-parameter nil 'gdb-buffer-type))
+              (pcase buffer-type
+                ('source (when source-buffer
+                           (set-window-buffer nil source-buffer)
+                           (setq gdb-source-window (selected-window))))
+                ('command (switch-to-buffer gud-comint-buffer))
+                (_ (let ((buffer (gdb-get-buffer-create buffer-type)))
+                     (with-selected-window-undedicated
+                      (set-window-buffer nil buffer))))))))
+      (kill-buffer placeholder))))
 
 (define-minor-mode gdb-many-windows
   "If nil just pop up the GUD buffer unless `gdb-show-main' is t.
@@ -4627,6 +4772,9 @@ gdb-many-windows
 (defun gdb-restore-windows ()
   "Restore the basic arrangement of windows used by gdb.
 This arrangement depends on the value of option `gdb-many-windows'."
+  ;; This function is used when the user messed up window
+  ;; configuration and want to "reset to default".  The function that
+  ;; sets up window configuration on start up is `gdb-get-source-file'.
   (interactive)
   (switch-to-buffer gud-comint-buffer) ;Select the right window and frame.
   (delete-other-windows)
@@ -4678,6 +4826,7 @@ gdb-reset
 (defun gdb-get-source-file ()
   "Find the source file where the program starts and display it with related
 buffers, if required."
+  ;; This function is called only once on startup.
   (goto-char (point-min))
   (if (re-search-forward gdb-source-file-regexp nil t)
       (setq gdb-main-file (read (match-string 1))))
diff --git a/lisp/window.el b/lisp/window.el
index 433486385d..0f4f242212 100644
--- a/lisp/window.el
+++ b/lisp/window.el
@@ -278,6 +278,19 @@ with-displayed-buffer-window
 	     (funcall ,vquit-function ,window ,value)
 	   ,value)))))
 
+(defmacro with-selected-window-undedicated (&rest body)
+  "Run BODY in the selected window temporarily undedicated."
+  (let ((window-dedicated-sym (gensym)))
+    `(let ((,window-dedicated-sym (window-dedicated-p)))
+       (when ,window-dedicated-sym
+         (set-window-dedicated-p nil nil))
+       ,@body
+       (when ,window-dedicated-sym
+         ;; `window-dedicated-p' returns the value set by
+         ;; `set-window-dedicated-p', which differentiates
+         ;; non-nil and t, so we cannot simply set to t.
+         (set-window-dedicated-p nil ,window-dedicated-sym)))))
+
 ;; The following two functions are like `window-next-sibling' and
 ;; `window-prev-sibling' but the WINDOW argument is _not_ optional (so
 ;; they don't substitute the selected window for nil), and they return
-- 
2.25.0

