C and C++

UPDATE <2019-11-16 Sat> 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

(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 (concat initel-directory "llvm/bin/clang-format")))

GDB

<2020-04-23 Thu> 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/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)

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.

(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)))

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 'alternate
  "Specifies the GDB layout to use. ALTERNATE is optimzed for
  higher-resolution displays, whereas DEFAULT is better for lower
  resolutions. 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 ((equalp do.cpp.gdb/layout 'alternate) (funcall #'do.cpp.gdb/alt-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 '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

;; need to execute this hook before the lsp hook!
(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))
    ;; (1) configure clangd
    (setq
     lsp-clients-clangd-executable (concat (file-name-as-directory initel-directory) "llvm/bin/clangd")
     lsp-clients-clangd-args `("--compile-commands-dir" ,(do.cpp/get-build-dir)
                               "-j=4" "-background-index"))
    ;; (3) start lsp
    (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)
        (cmd (or command "")))
    (cond
     ;; 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))))
     ;; either a) custom command (set `do-cpp-compile-command' in .dir-locals.el
     ;; or b) we give up
     (t (if (boundp 'do-cpp-compile-command)
            (compile do-cpp-compile-command cmd)
          (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>"))))