C and C++

UPDATE <November 16 2019> switching to clangd + lsp-mode

This is my C++ "IDE" set-up. I wen't from CEDET to rtags to this. A simple LSP + clangd set-up. I also distribute a version of LLVM on my web server for quick bootstrapping.

Coding Style

(c-add-style "llvm"
             '("stroustrup"
               (c-basic-offset . 2)
               (c-offsets-alist . ((arglist-intro . ++)
                                   (member-init-intro . ++)
                                   (statement-cont . (c-lineup-assignments ++))
                                   (innamespace . 0)
                                   (inextern-lang . 0)))
               (fill-column . 80)
               (indent-tabs-mode . nil)))
(setq c-default-style "llvm")

Clang-format

Run clang-format before saving

(defvar do.cpp/clang-format-executable "clang-format"
      "Path to the clang-format executable")

(use-package clang-format
  :ensure t
  :init
  (setq-default clang-format-style "{BasedOnStyle: llvm}")
  (defun do.cpp.clang-format/enable ()
    (make-variable-buffer-local 'before-save-hook)
    (add-hook 'before-save-hook 'clang-format-buffer))
  (add-hook 'c++-mode-hook 'do.cpp.clang-format/enable)
  (add-hook 'c-mode-hook 'do.cpp.clang-format/enable)
  (defun do.cpp.clang-format/disable ()
    (interactive)
    (remove-hook 'before-save-hook 'clang-format-buffer))
  (defalias 'disable-clang-format 'do.cpp.clang-format/disable)
  :config
  (setq clang-format-executable do.cpp/clang-format-executable))

GDB

<April 23 2020> This is pretty crufty stuff, I notice that gdb-mi improved a lot since I wrote all of this. Maybe need to go over this again.

The built-in GDB mode is fantastic. That being said, we want to customize it a little.

(use-package gdb-mi
  :demand
  :bind
  <<do.cpp.gdb/keys>>
  :init
  <<do.cpp.gdb/settings>>
  <<do.cpp.gdb/alt-layout>>
  <<do.cpp.gdb/disassembly-layout>>
  <<do.cpp.gdb/switch-layout>>)

General Settings

;; tell the mode-line that we are in a comint-derived mode
(add-hook 'gdb-inferior-io-mode-hook 'no-trailing-whitespace)
(add-hook 'gdb-mode-hook 'no-trailing-whitespace)
(add-hook 'gdb-inferior-io-mode-hook '(lambda () (setq-local ml-interactive? t)))
(add-hook 'gdb-mode-hook '(lambda () (setq-local ml-interactive? t)))
(setq gdb-many-windows t) ;; always activate many-windows mode
(setq gdb-display-io-nopopup t)
(setq gdb-restore-window-configuration-after-quit t)

Alternate Layout

The default layout is nice on low-resolution displays, but for high resolutions, we can split the screen in thirds and have a more comfortable layout.

Note <December 15 2024> this layout seems to not work anymore. Need to fix it eventually.

(defun do.cpp.gdb/alt-layout ()
  "Layout the window pattern for option `gdb-many-windows'."
  ;; create 3 of the four buffers
  (gdb-get-buffer-create 'gdb-locals-buffer)
  (gdb-get-buffer-create 'gdb-stack-buffer)
  (gdb-get-buffer-create 'gdb-breakpoints-buffer)
  ;; comint buffer
  (set-window-dedicated-p (selected-window) nil)
  (switch-to-buffer gud-comint-buffer)
  (delete-other-windows)
  (let ((gdb-comint-window (selected-window))
        (gdb-src-window (split-window nil ( / (window-height) 4) 'above))
        (gdb-breakpoints-window (split-window-horizontally (/ (window-width) 3))))
    ;; source buffer
    (select-window gdb-src-window)
    (set-window-buffer
     gdb-src-window
     (if gud-last-last-frame
         (gud-find-file (car gud-last-last-frame))
       (if gdb-main-file
           (gud-find-file gdb-main-file)
         ;; scratch if we can't find a src file
         (get-buffer-create "*scratch*"))))
    (setq gdb-source-window (selected-window))
    ;; io buffer
    (let ((gdb-io-window (split-window-horizontally (/ (* 2 (window-width)) 3))))
      (gdb-set-window-buffer
       (gdb-get-buffer-create 'gdb-inferior-io) nil gdb-io-window)
      ;; stack buffer
      (select-window gdb-io-window)
      (let ((gdb-stack-window (split-window nil (/ (* 2 (window-height)) 3) 'above)))
        (gdb-set-window-buffer (gdb-stack-buffer-name) nil gdb-stack-window)))
    ;; breakpoints buffer
    (select-window gdb-breakpoints-window)
    (gdb-set-window-buffer (if gdb-show-threads-by-default
                                 (gdb-threads-buffer-name)
                               (gdb-breakpoints-buffer-name))
                           nil gdb-breakpoints-window)
    ;; locals buffer
    (let ((gdb-locals-window (split-window-right)))
      (gdb-set-window-buffer (gdb-locals-buffer-name) nil gdb-locals-window))
    ;; select the main window and bounce
    (select-window gdb-comint-window)))

Here is another layout, one that displays the instructions buffer instead of I/O by default. This is relatively useful for embedded programming

(defun do.cpp.gdb/disassembly-layout ()
  "Lay out the window pattern for option `gdb-many-windows'.

This is replacing the I/O buffer with a disassembly buffer."
  (if gdb-default-window-configuration-file
      (gdb-load-window-configuration
       (if (file-name-absolute-p gdb-default-window-configuration-file)
           gdb-default-window-configuration-file
         (expand-file-name gdb-default-window-configuration-file
                           gdb-window-configuration-directory)))
    ;; Create default layout as before.
    ;; Make sure that local values are updated before locals.
    (gdb-get-buffer-create 'gdb-locals-values-buffer)
    (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-list (list (selected-window)))
      (let ((win4 (split-window-right)))
        (gdb-set-window-buffer
         ;; (gdb-get-buffer-create 'gdb-inferior-io) nil win4
         (gdb-get-buffer-create 'gdb-disassembly-buffer) 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))))

We choose the alternate layout as default. If we wanted to change this during runtime, we can just change the variable below.

(defvar do.cpp.gdb/layout 'disassembly
  "Specifies the GDB layout to use. ALTERNATE is optimzed for
  higher-resolution displays, whereas DEFAULT is better for lower
  resolutions. DISASSEMBLY is like DEFAULT, but it replaces the
  I/O buffer with the disassembly buffer. Add more options as
  needed.")

(defun do.cpp.gdb/setup-windows (original-gdb-setup-windows)
  "Call the appropriate function for `gdb-setup-windows'. Add more options as needed."
  (cond ((eq do.cpp.gdb/layout 'alternate) (funcall #'do.cpp.gdb/alt-layout))
        ((eq do.cpp.gdb/layout 'disassembly) (funcall #'do.cpp.gdb/disassembly-layout))
        (t (funcall original-gdb-setup-windows))))

TODO Advice

Debug: for some reason, the advice does not get activated inside of the use-package statement…

(add-function :around (symbol-function #'gdb-setup-windows)
              #'do.cpp.gdb/setup-windows)

Keybindings

(("<f5>" . gud-next)
 ("<f6>" . gud-step)
 ("<f7>" . gud-cont)
 ("<f8>" . gud-finish))

Simpler gdb

(add-hook 'gud-mode-hook 'no-trailing-whitespace)
(add-hook 'gdb-disassembly-mode-hook 'no-trailing-whitespace)
(add-hook 'gud-mode-hook '(lambda () (setq-local ml-interactive? t)))

C++ style comments in C

(defun do.cpp/c-hooks ()
  (setq comment-start "//"
        comment-end   ""))
(add-hook 'c-mode-hook #'do.cpp/c-hooks)

Preprocessor macros

Those can always look a little sharper than the default…

(use-package preproc-font-lock
  :ensure t
  :init
  (preproc-font-lock-global-mode 1)
  :config
  (make-face 'do.cpp/preproc-macro-face)
  (cond
   ((eq do.theme/enabled-theme 'dark)
    (set-face-attribute 'do.cpp/preproc-macro-face nil
                        :background (face-attribute 'default :background)
                        :foreground (face-attribute 'font-lock-constant-face :foreground)
                        :underline nil
                        :slant (if (display-graphic-p) 'italic 'normal)
                        :weight 'bold))
   ((eq do.theme/enabled-theme 'light)
    (set-face-attribute 'do.cpp/preproc-macro-face nil
                        :background (face-attribute 'default :background)
                        :foreground (face-attribute 'font-lock-constant-face :foreground)
                        :underline nil
                        :slant (if (display-graphic-p) 'italic 'normal)
                        :weight 'bold)))
  (setq preproc-font-lock-preprocessor-background-face 'do.cpp/preproc-macro-face))

Font-locking for modern c++

(use-package modern-cpp-font-lock
  :ensure t
  :diminish modern-c++-font-lock-mode
  :init
  (modern-c++-font-lock-global-mode t))

CMake

The packages cmake-mode and cmake-font-lock help when editing CMakeLists.txt source files.

(use-package cmake-font-lock
  :ensure t
  :commands cmake-font-lock-activate)

(use-package cmake-mode
  :ensure t
  :mode ("CMakeLists\\.txt\\'" "\\.cmake\\'")
  :init
  (add-hook 'cmake-mode-hook 'cmake-font-lock-activate))

LSP

Over the years, I went from CEDET -> rtags + cmake-ide -> LSP. The configuration is still a little tricky, but much easier than before.

TODO: break up and document properly

(use-package lsp-clangd
  :after lsp-mode
  :demand t)

(defvar do.cpp.lsp/clangd-executable nil
  "path to the clangd executable")

(defun do.cpp.lsp/start-lsp ()
  "Start LSP in C++ and C mode. Have to hack this a little bit."
  (when (or (equal major-mode 'c++-mode) (equal major-mode 'c-mode))
    ;; only start LSP when clangd is installed. otherwise LSP will ask to
    ;; download clangd and we do not want that!
    (if (not do.cpp.lsp/clangd-executable)
        ;; only start LSP when the executable is set
        (display-warning :warning "`do.cpp.lsp/clangd-executable' is not set. not starting LSP.")
      ;; it is not clear to me why I have to do this here. this seems
      ;; to be the only place that this works. we make a few copies
      ;; of clangd so we can run multiple instances of it. to force
      ;; a particular project to use e.g. clangd-3, we do the
      ;; equivalent of the following in .dir-locals.el:
      ;;
      ;; (setq lsp-enabled-clients '(clangd-3))
      (unless (gethash 'clangd-1 lsp-clients)
        (dolist (id '(clangd-1 clangd-2 clangd-3 clangd-4 clangd-5 clangd-6))
          (let ((c (copy-lsp--client (gethash 'clangd lsp-clients))))
            (setf (lsp--client-server-id c) id)
            (setf (lsp--client-priority c) -5)
            (lsp-register-client c))))
      ;; point clangd to the compile commands file and the query driver
      (setq lsp-clients-clangd-executable do.cpp.lsp/clangd-executable)
      (setq lsp-clients-clangd-args `(;; compile-commands.json needs to be in `do-cpp-build-dir'.
                                      ,@(if (boundp 'do-cpp-build-dir)
                                            (list "--compile-commands-dir" do-cpp-build-dir)
                                          (message (concat "`do-cpp-build-dir' not defined." " "
                                                           "calling clangd without --compile-commands-dir argument."))
                                          nil)
                                      ;; query-driver is especially important when the compiler
                                      ;; is NOT from the same distribution as the clangd executable
                                      ,@(when (boundp 'do-cpp-clangd-query-driver)
                                          (list "--query-driver" do-cpp-clangd-query-driver))
                                      ;; we allow to use more or less threads, default is 4.
                                      ,@(if (boundp 'do-cpp-clangd-threads)
                                            (list (format "-j=%d" do-cpp-clangd-threads))
                                          '("-j=4"))
                                      "-background-index"))
      ;; start lsp-mode
      (lsp))))

(defun do.cpp/add-compile-bindings ()
  (local-set-key (kbd "C-c c") #'do.cpp/try-compile)
  (local-set-key (kbd "C-c t") #'do.cpp/try-test))

;; call this sometime directly from init.el
(defun do.cpp.lsp/setup ()
  ;; we need to hook in after the local variables are set. This affects every
  ;; buffer open, but should be ok
  (add-hook 'hack-local-variables-hook #'do.cpp.lsp/start-lsp)
  ;; bind the compile commands to the major mode
  (add-hook 'c++-mode-hook #'do.cpp/add-compile-bindings)
  (add-hook 'c-mode-hook #'do.cpp/add-compile-bindings))

(do.cpp.lsp/setup)

;; try-compile
(defun do.cpp/is-cmake-build-dir (dir)
  (file-exists-p (expand-file-name "CMakeCache.txt" dir)))

(defun do.cpp/is-makefile-build-dir (dir)
  (or (file-exists-p (expand-file-name "Makefile" dir))
(file-exists-p (expand-file-name "makefile" dir))
(file-exists-p (expand-file-name "GNUmakefile" dir))))

(defun do.cpp/is-ninja-build-dir (dir)
  (file-exists-p (expand-file-name "build.ninja" dir)))

(defun do.cpp/get-cmake-compile-command (dir)
  (cond ((do.cpp/is-ninja-build-dir dir) (concat (executable-find "ninja") " -C " (shell-quote-argument dir)))
  ((do.cpp/is-makefile-build-dir dir)
   (let* ((nprocbin (executable-find "nproc"))
    (nprocs (if nprocbin (replace-regexp-in-string "\n$" "" (shell-command-to-string nprocbin)) "1")))
     (concat (executable-find "make") " -j " nprocs " -C " (shell-quote-argument dir))))
  (t nil)))

;; get-build-dir
(defun do.cpp/get-build-dir ()
  "Get the location of the build directory."
  (expand-file-name
   (if (boundp 'do-cpp-build-dir) do-cpp-build-dir
     ;; TODO: find a way to silently save this dir-local variable.
     ;; check the code of "add-dir-local-variable"
     (read-directory-name "Couldn't find build directory. Select: "))))

(defun do.cpp/try-compile-command (command)
  (let ((build-dir (do.cpp/get-build-dir))
        (compilation-read-command nil)
        (compilation-scroll-output t)
        (cmd (or command "")))
    (cond
     ;; custom compile command
     ((bound-and-true-p do-cpp-build-cmd) (compile do-cpp-build-cmd))
     ;; CMake build
     ((do.cpp/is-cmake-build-dir build-dir)
      (let ((compile-cmd (do.cpp/get-cmake-compile-command build-dir)))
        (compile (concat compile-cmd " " cmd))))
     ;; Makefile build
     ((do.cpp/is-makefile-build-dir build-dir)
      (let* ((nprocbin (executable-find "nproc"))
             (nprocs (if nprocbin (replace-regexp-in-string "\n$" "" (shell-command-to-string nprocbin)) "1")))
        (compile (concat (executable-find "make") " -j " nprocs " -C " (shell-quote-argument build-dir) " " cmd))))
     ;; we give up
     (t (error "Could not find compile command")))))

(defun do.cpp/try-compile () (interactive) (do.cpp/try-compile-command nil))
(defun do.cpp/try-test () () (interactive) (do.cpp/try-compile-command "test"))

.dir-locals.el

To make all of this work, we set a directory-local variable do-cpp-build-dir at the root of the project directory.

((nil . ((do-cpp-build-dir . "<PATH_TO_PROJECT_BUILD_DIRECTORY>"))))