Introduction

It is no secret that Emacs, being an extensible, customizable, free/libre text editor (or simply the great operating system lacking only a decent editor (though not true anymore with evil mode emulating Vim keybindings)), is capable of handling many different tasks greatly. For example, Emacs can be configured as a development environment for many programming languages. As a matter of fact, many new programming languages have their corresponding emacs modes first before they are integrated to other IDEs. For "old" languages like C/C++, there are many options available in Emacs which require different levels of configuration.

If you search C/C++ development in emacs in Google, chances are you will come across tuhdo's post which uses gtags and other commonly used libraries like company and projectile. When I first read the post, it totally blew my mind: on one hand I was amazed by how powerful Emacs can be customized as a development environment; on the other hand the extent of the post together with all the gif images were not very encouraging for new Emacs users like I was back then. After several tries, I did finish reading the whole post and had a better understanding about how the individual components cooperate together in Emacs to provide the code navigation, auto completion, syntax checking and documentation as commonly seen in many other IDEs. Since then I have tried irony, rtags before finally settled on lsp. Below I will briefly describe my experience about irony and rtags first and then show the detailed configuration for lsp mode with some demos.

irony

As the github page says, irony-mode is an Emacs minor mode that improves C/C++/Objective-C languages editing experience. It does not require installation of external programs like rtags and the irony-server is launched within in Emacs. Configuration for irony-mode is straightforward following the instructions and you can still find my setup in here. I was pretty happy with irony until it broke down when I worked on a Qt based C++ project (can't find the Qt header files) and then I moved to rtags.

rtags

rtags is much more powerful compared to irony but also requires more configurations including installing external tools. There are better posts describing the setup for rtags (and comparison/integration with irony) so I will not repeat the details here. Following the github instruction to building rtags from source, start the rtags daemon rdm and index the C/C++ project with rc. It's also helpful to start rdm automatically e.g. by integrating it with systemd on Linux. My rtags configuration in Emacs can be found here To be fair, rtags works perfectly fine for all the C/C++ projects I worked on and the only reason (at least back then) that I moved to lsp is I want to have a consistent front end user interface experience when working with other programming languages like Python or VHDL.

lsp + ccls setup

lsp stands for language server protocol and was originally developed by Microsoft for its Visual Studio Code editor. To quote from Wikipedia:

The Language Server Protocol (LSP) is an open, JSON-RPC-based protocol for use between source code editors or integrated development environments (IDEs) and servers that provide programming language-specific features. The goal of the protocol is to allow programming language support to be implemented and distributed independently of any given editor or IDE.[1]

And lsp-mode provides the lsp support with multiple languages for Emacs. I personally consider that lsp/lsp-mode is one of the best things happened to Emacs in the recent years as it really lowers the setup requirements for a decent development environments for various languages. The gif below shows the jumping between the declaration and definition of a member function:

./emacs-ccpp-jump.gif
Jumping between declaration and definition

The refactor part in lsp-mode currently only supports organize imports and rename. To auto the generation of function definition, I use emacs srefactor instead:

./emacs-ccpp-refactor.gif
emacs srefactor

lsp setup

My setup for lsp, company and flycheck is shown below. They are mostly copied directly from the github page and I have not done any fancy customization yet the usability is already good enough:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
;; lsp
(use-package lsp-mode
  :ensure t
  :hook ((c-mode . lsp)
	 (c++-mode . lsp)
         (lsp-mode . lsp-enable-which-key-integration))
  :commands lsp
  :config
  (setq lsp-keymap-prefix "C-c l")
  (define-key lsp-mode-map (kbd "C-c l") lsp-command-map)
  (setq lsp-file-watch-threshold 15000))

(use-package lsp-ui
  :ensure t
  :commands (lsp-ui-mode)
  :config
  (setq lsp-ui-doc-enable nil)
  (setq lsp-ui-doc-delay 0.5)
  (define-key lsp-ui-mode-map [remap xref-find-definitions] #'lsp-ui-peek-find-definitions)
  (define-key lsp-ui-mode-map [remap xref-find-references] #'lsp-ui-peek-find-references)
  )

(use-package lsp-ivy
  :ensure t
  :commands lsp-ivy-workspace-symbol)

(use-package lsp-treemacs
  :ensure t
  :commands lsp-treemacs-errors-list)

;; company
(use-package company
  :ensure t
  :bind ("M-/" . company-complete-common-or-cycle) ;; overwritten by flyspell
  :init (add-hook 'after-init-hook 'global-company-mode)
  :config
  (setq company-show-numbers            t
	company-minimum-prefix-length   1
	company-idle-delay              0.5
	company-backends
	'((company-files          ; files & directory
	   company-keywords       ; keywords
	   company-capf           ; what is this?
	   company-yasnippet)
	  (company-abbrev company-dabbrev))))

(use-package company-box
  :ensure t
  :after company
  :hook (company-mode . company-box-mode))

;; flycheck
(use-package flycheck
  :ensure t
  :init (global-flycheck-mode)
  :config
  (setq flycheck-display-errors-function
	#'flycheck-display-error-messages-unless-error-list)

  (setq flycheck-indication-mode nil))

(use-package flycheck-pos-tip
  :ensure t
  :after flycheck
  :config
  (flycheck-pos-tip-mode))

ccls setup

lsp-mode needs a language server for each supported language and the default language server bundled with C/C++ is clangd. However I have heard (watched) good words about ccls and therefore gave it a go first. It works surprisingly well with my projects and I therefore stuck with it so far. Of course I have to go through the installation of external tool (ccls) but fortunately after the experience with rtags, it seems quite straightforward to me. Below is my emacs-ccls setup:

1
2
3
4
5
6
7
8
9
(use-package ccls
  :ensure t
  :config
  :hook ((c-mode c++-mode objc-mode cuda-mode) .
         (lambda () (require 'ccls) (lsp)))
  (setq ccls-executable "/usr/local/bin/ccls")
  (setq ccls-initialization-options
	'(:index (:comments 2) :completion (:detailedLabel t)))
  )

Refactor

As mentioned above, I use emacs-srefactor as an addition to lsp:

1
2
3
4
5
6
7
(use-package srefactor
  :ensure t
  :config
  (semantic-mode 1)
  (define-key c-mode-map (kbd "M-RET") 'srefactor-refactor-at-point)
  (define-key c++-mode-map (kbd "M-RET") 'srefactor-refactor-at-point)
  )

Setup a build system

Working in Emacs for C/C++ development means I am stuck with a text file based build system (for a good reason) like Make/CMake. With a bit of elisp magic, I am able to call various CMake and other build system like Premake commands from emacs without even dropping to the terminal (in Emacs I mean):

./emacs-ccpp-build.gif

Following elisp defines some custom functions to call Bear make, CMake and Premake:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
;;; Build system
(defun kong-bearmake-compile-command ()
  "Bear make compile command."
  (interactive)
  (set (make-local-variable 'compile-command)
       (concat "bear make "
  	       (if buffer-file-name
  		   (shell-quote-argument
  		    (file-name-sans-extension buffer-file-name)))))
  (call-interactively 'compile))

(defun kong-make-compile-command ()
  "Make compile command."
  (interactive)
  (set (make-local-variable 'compile-command)
       (concat "make "
  	       (if buffer-file-name
  		   (shell-quote-argument
  		    (file-name-sans-extension buffer-file-name)))))
  (call-interactively 'compile))

;; Premake
(defun kong-premake-compile-command ()
  "Premake compile command."
  (interactive)
  (set (make-local-variable 'compile-command)
       (concat "premake4 gmake "
  	       (if buffer-file-name
  		   (shell-quote-argument
  		    (file-name-sans-extension buffer-file-name)))))
  (call-interactively 'compile))


(defun kong-premake-build-command ()
  "Premake build command."
  (interactive)
  (set (make-local-variable 'compile-command)
       (concat "cd build && bear make "
  	       (if buffer-file-name
  		   (shell-quote-argument
  		    (file-name-sans-extension buffer-file-name)))))
  (call-interactively 'compile))

;; CMake
(defun kong-cmake-compile-command ()
  "CMake compile command."
  (interactive)
  (set (make-local-variable 'compile-command)
       (concat "cmake -H. -Bbuild -DCMAKE_EXPORT_COMPILE_COMMANDS=1 "
  	       (if buffer-file-name
  		   (shell-quote-argument
  		    (file-name-sans-extension buffer-file-name)))))
  (call-interactively 'compile))

(defun kong-cmake-build-command ()
  "CMake build command."
  (interactive)
  (set (make-local-variable 'compile-command)
       (concat "cmake --build build "
  	       (if buffer-file-name
  		   (shell-quote-argument
  		    (file-name-sans-extension buffer-file-name)))))
  (call-interactively 'compile))

Adding these functions to a hydra menu gives the convenience of executing the build procedures shown in the previous gif with several key strokes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;; Hydra / Build
(pretty-hydra-define hydra-build
  (:hint nil :color teal :quit-key "q" :title (with-material "build" "Build (in root)" 1 -0.05))
  ("CMake"
   (("c" kong-cmake-compile-command "CMake compile")
    ("b" kong-cmake-build-command "CMake build")
    ("i" kong-cmake-install-command "CMake install"))
   "Premake"
   (("M-c" kong-premake-compile-command "Premake compile")
    ("M-b" kong-premake-build-command "Premake build"))
   "Others"
   (("B" kong-bearmake-compile-command "Bear Make")
    ("M" kong-make-compile-command "Make"))))