Configuring eglot for Rcpp and Its Friends

This note introduces how one can quickly configure eglot (a built-in language server protocol client in Emacs 29) for Rcpp users (or more specifically R package developers using Rcpp) so that code diagnostics, auto-completion, and jump-to-definition, etc. work smoothly when writing Rcpp code in Emacs.

For Emacs 29, it is straightforward to enable eglot for C/C++ as follows:

(require 'eglot)
(add-hook 'c-mode-hook 'eglot-ensure)
(add-hook 'c++-mode-hook 'eglot-ensure)

I have a few more configurations specific to eglot given below, where I set clangd as the eglot-server-programs for C/C++ and disabled the aggressive header insertion feature.

(add-to-list 'eglot-server-programs
             '((c++-mode c-mode)
               . ("clangd"

Next, we just need to inform clangd of where the R.h and Rcpp.h are placed through a plain text file named .clangd in the project/package root directory (e.g., ~/foo-pkg/). An example .clangd file is as follows:

  Add: [
-xc++, -std=c++11, -Wall,

The paths to the header files can vary and I use the following R script1 to automatically generate a .clangd file for me (under Linux or Mac OS).

#!/usr/bin/env Rscript

### include R and Rcpp headers in .clangd for eglot

## R include
r_path <- Sys.getenv("R_INCLUDE_DIR")

## Rcpp
rcpp_family <- c("Rcpp", "RcppArmadillo")
rcpp_paths <- sapply(rcpp_family, function(a) {
    file.path(find.package(a), "include")

## optional: add include path for mac
if (isTRUE(["sysname"] == "Darwin")) {
    tmp <- tempfile("dummy", fileext = ".cpp")
    tmp_cout <- sub("cpp$", "o", tmp)
    writeLines("#include<iostream>\nint main() { return 0; }", tmp)
    tmp_out <- system2("clang++", sprintf("-c %s -o %s -v", tmp, tmp_cout),
                       stdout = TRUE, stderr =  TRUE)
    start_idx <- which(grepl("^#include <\\.\\.\\.> search starts here:",
    end_idx <- which(grepl("^End of search list", tmp_out))[1L]
    include_paths <- sapply( + 1L, end_idx - 1L),
                            function(i) {
                                if (grepl("include", tmp_out[i]))
    include_paths <- trimws(include_paths[!])
} else {
    include_paths <- NULL

## create flags
flags0 <- "-xc++, -std=c++11, -Wall,\n"
flags <- paste0("-I", c(include_paths, r_path, rcpp_paths))
flags <- paste0(flags0, paste(flags, collapse = ",\n"))

## add project include path
proj_include <- "inst/include"
if (dir.exists(proj_include)) {
    proj_include <- normalizePath(proj_include)
    flags <- paste(flags, paste0("-I", proj_include), sep = ",\n")

## constants
prefix <- "CompileFlags:\n  Add: [\n"
suffix <- "]\n"

## write .clang_complete
writeLines(c(prefix, flags, suffix), sep = "", con = ".clangd")
message("Generated '.clangd'")

I made this script executable and included the directory containing it in $PATH so that I can run it under the project/package root directory in bash conveniently.

With the generated .clangd, eglot should work out of the box when one edits Rcpp code under ~/foo-pkg/.

Although the configuration looks straightforward, it took me a while to go through the documentation of eglot and clangd to figure things out. I hope this note can be of help to my future self or other Emacs + R + Rcpp users like me.

  1. I likely took some of the code (probably the chunk locating the additional header files for Mac OS) from Stack Overflow. But I feel sorry that I can no longer recall the details. ↩︎

comments powered by Disqus