I've been spending some quality time with a C compiler these days and decided to gather little elements of my workflow here. I should preface this by saying I'm not a C expert by any means. Also, these are probably going to be interesting to those who program C in a specific style:
- Optimizing for C89 or C99
- No language servers and other such distractions
- Emacs (obviously)
Simpler C mode
The built in Emacs C mode is not very good. It's incredibly slow, bloated, and even though I've forgotten them now, there were little behavioural inconveniences that made me loathe it. There's a mode called simpc-mode
1 that provides a simple yet capable C programming mode, and even though it performed really well, it had lack-luster indentation configuration. For instance, it hard-codes 4-space indentation, and hooks into Emacs' built in indentation system which overrides the TAB
and RET
keybindings to do indentation situationally. This is all very convenient but I am trying to follow the linux style guide and that requires tabs. After cloning simpc-mode
and making changes for it to insert tabs instead, I still found the "smart" atuo indentation behavior to be annoying and janky so I decided to just gut out the indent system completely.
The result is the same simpc-mode
but with certain C++ related stuff removed, and indentation disabled by actively overriding TAB
and RET
to simply insert themselves. The Emacs manual says to avoid doing that and to blindly accept what you're given, thanking the gods in the process, but turns out this is the only way to get Emacs to behave the way I want. Anyway, here's dsc-mode
2.
(require 'subr-x) (defvar dsc-mode-syntax-table (let ((table (make-syntax-table))) (modify-syntax-entry ?/ ". 124b" table) (modify-syntax-entry ?* ". 23" table) (modify-syntax-entry ?\n "> b" table) (modify-syntax-entry ?# "." table) (modify-syntax-entry ?' "\"" table) (modify-syntax-entry ?& "." table) (modify-syntax-entry ?% "." table) table)) (defun dsc-types () '("char" "int" "long" "short" "void" "bool" "float" "double" "signed" "unsigned" "char16_t" "char32_t" "char8_t" "int8_t" "uint8_t" "int16_t" "uint16_t" "int32_t" "uint32_t" "int64_t" "uint64_t" "uintptr_t" "size_t")) (defun dsc-keywords () '("auto" "break" "case" "char" "const" "continue" "default" "do" "double" "else" "enum" "extern" "float" "for" "goto" "if" "int" "long" "register" "return" "short" "signed" "sizeof" "static" "struct" "switch" "typedef" "union" "unsigned" "void" "volatile" "while")) (defun dsc-font-lock-keywords () (list `("# *[#a-zA-Z0-9_]+" . font-lock-preprocessor-face) `("#.*include \\(\\(<\\|\"\\).*\\(>\\|\"\\)\\)" . (1 font-lock-string-face)) `(,(regexp-opt (dsc-keywords) 'symbols) . font-lock-keyword-face) `(,(regexp-opt (dsc-types) 'symbols) . font-lock-type-face))) (define-derived-mode dsc-mode fundamental-mode "DSC" "Simpler major mode for editing C files." :syntax-table dsc-mode-syntax-table (setq-local font-lock-defaults '(dsc-font-lock-keywords)) (setq-local comment-start "/* ") (setq-local comment-end " */")) (provide 'dsc-mode)
Oh and here's how I set it up, keybinds and all:
(use-package dsc-mode :load-path "~/.config/emacs/no-package-manager/dsc-mode" :mode "\\.[ch]\\'" :hook ((dsc-mode . hs-minor-mode) (dsc-mode . indent-tabs-mode) (dsc-mode . apheleia-mode)) :bind (:map dsc-mode-map ;;([C-tab] . hs-toggle-hiding) ("C-c f" . apheleia-format-buffer) ("C-c p" . personal/printf-symbol) ("TAB" . self-insert-command) ("RET" . newline)))
You'll notice a couple of interesting things here that i'll touch on in the rest of the post, namely the hooked modes, and the keybinds.
hs-minor-mode
hs-minor-mode
does code folding. It basically uses outline-mode
as its backend. Very useful for when you just wanna look at some declarations to get an overview of what's happening, or for when you have an implementation .c
file, and you wanted to quickly dump some declarations into a corresponding .h
file, which shines further if you're using the linux style guide or similar.
/* blah.c */ int foo1(const char* bar, int baz) { /* implementation detail */ /* implementation detail */ /* implementation detail */ } int foo2(const char* bar, int baz) { /* implementation detail */ /* implementation detail */ /* implementation detail */ } /* blah.c but folded */ int foo1(const char* bar, int baz) {...} int foo2(const char* bar, int baz) {...}
apheleia-mode
apheleia-mode
is a mode for code-formatting. Since I'm not using eglot, i thought to facilitate code formatting by shelling out to clang-format
but that had a janky jarring buffer-scrolling and cursor-repositioning byproduct. apheleia-mode
fixes that.
Utility functions
personal/printf-symbol
Another thing you'll notice from the config is the personal/printf-symbol
function.
(defun personal/printf-symbol (arg) (interactive "cFormat(s,d,etc): ") (save-excursion (let* ((end (point)) (beg (progn (back-to-indentation) (point))) (symbol (buffer-substring beg end))) (kill-region beg end) (insert (format "printf(\"%s: %%%c\\n\", %s);" symbol (or arg "s") symbol)))))
It allows this:
int foo(void) { int bar; bar = baz(); bar| /* | = cursor */ }
To turn into this:
int foo(void) { int bar; bar = baz(); |printf("bar: %d\n", bar); }
Just helps with debugging.
personal/make-c-header-file-template
Oh and I have another function that populates empty .h
files with the ifndef
boilerplate.
(defun personal/make-c-header-file-template () (interactive) (let ((parts (split-string (buffer-name) "\\." t nil))) (save-excursion (beginning-of-buffer) (insert (format "#ifndef %s_H_\n#define %s_H_\n" (upcase (car parts)) (upcase (car parts)))) (end-of-buffer) (insert (format "#endif //%s_H_\n" (upcase (car parts)))))))
I should use some sort of templating package for these, but hey.
personal/disable-things
Sometimes I want to use eglot for code navigation and huge refactors, but I hate how it comes with distractions like inlay hints, and diagnostics and code-completion, so I do this:
(defun personal/disable-things () (eglot-inlay-hints-mode -1) (eldoc-mode -1) (flymake-mode-off)) (add-hook 'eglot-managed-mode-hook #'personal/disable-things)
Other workflow notes
Now that I can code C peacefully, I do the grunt work like compilation and tinkering in a separate place. Specifically in other tabs. I usually run tab-bar-mode
and have:
- a coding tab
- a run-and-see-results tab
- in some cases a man pages tab
- and a separate tab for other side-stuff like tinkering with my emacs config or scratch buffer stuff.
I am using the built-in project.el and usually have a makefile in my project somewhere so i sometimes do compilation via project-compile
in my coding tab.
I also have some special commenting tricks. I'm using Hyperbole which allows me to put file locations anywhere and to jump to them. I also make use of the fact that I can execute E-lisp from any text buffer in Emacs.
/* An iovec is a region of memory with a base address (iov_base) and size (iov_len) System calls use arrays of iovecs. (man "iovec(3type)") <-- just jump straight into the manpage */ struct iovec *iovecs; /* I've documented this further at blah.c:5 <-- jump straight to the location via hyperbole */ weird_thing = weird_operation();
And that's it. Thanks for sticking around.
Footnotes:
Its called dsc-mode
because its a dead simple c mode.