Keyboard navigation in Emacs GDB mode

I love Emacs GDB mode, but I always found it annoying that there is no given key binding (or function which could be directly mapped into a key binding) for switching between the different views given by gdb-many-windows. The usual window and buffer switching functions are insufficient because the GDB windows are flagged as dedicated (so switch-buffer refuses to swap them in place), and in the case of the Locals/Registers and Breakpoints/Threads windows, the buffer you want to visit doesn’t necessarily even exist until you click that button.

This went from mildly annoying to a major headache when I needed to run the debugger in a remote Emacs session, over SSH from Mac OS X’s which does not support xterm mouse emulation. So I wrote this gdb-select-window function and corresponding key bindings to finally get the desired behavior…

;; For the consistency of gdb-select-window's calling convention...
(defun gdb-comint-buffer-name ()
  (buffer-name gud-comint-buffer))
(defun gdb-source-buffer-name ()
  (buffer-name (window-buffer gdb-source-window)))

(defun gdb-select-window (header)
  "Switch directly to the specified GDB window.
Moves the cursor to the requested window, switching between
`gdb-many-windows' \"tabs\" if necessary in order to get there.

Recognized window header names are: 'comint, 'locals, 'registers,
'stack, 'breakpoints, 'threads, and 'source."

  (interactive "Sheader: ")

  (let* ((header-alternate (case header
                             ('locals      'registers)
                             ('registers   'locals)
                             ('breakpoints 'threads)
                             ('threads     'breakpoints)))
         (buffer (intern (concat "gdb-" (symbol-name header) "-buffer")))
         (buffer-names (mapcar (lambda (header)
                                 (funcall (intern (concat "gdb-"
                                                          (symbol-name header)
                               (if (null header-alternate)
                                   (list header)
                                 (list header header-alternate))))
         (window (if (eql header 'source)
                   (or (get-buffer-window (car buffer-names))
                       (when (not (null (cadr buffer-names)))
                         (get-buffer-window (cadr buffer-names)))))))

    (when (not (null window))
      (let ((was-dedicated (window-dedicated-p window)))
        (select-window window)
        (set-window-dedicated-p window nil)
        (when (member header '(locals registers breakpoints threads))
          (switch-to-buffer (gdb-get-buffer-create buffer))
          (setq header-line-format (gdb-set-header buffer)))
        (set-window-dedicated-p window was-dedicated))

;; Use global keybindings for the window selection functions so that they
;; work from the source window too...
(mapcar (lambda (setting)
          (lexical-let ((key    (car setting))
                        (header (cdr setting)))
            (global-set-key (concat "\C-c\C-g" key) #'(lambda ()
                                                        (gdb-select-window header)))))
        '(("c" . comint)
          ("l" . locals)
          ("r" . registers)
          ("u" . source)
          ("s" . stack)
          ("b" . breakpoints)
          ("t" . threads)))

Put this in your ~/.emacs.el or init.el and fire up gdb-many-windows, and then you’ll be able to quickly switch between the GDB windows with the following keyboard shortcuts:

  • C-c C-g c — comint (GDB command) buffer
  • C-c C-g l — locals buffer
  • C-c C-g r — registers buffer
  • C-c C-g u — source window
  • C-c C-g s — stack buffer
  • C-c C-g b — breakpoints buffer
  • C-c C-g t — threads buffer