Using LSP in ESP IDF projects

Post Contents

Language servers are very helpful for writing code. They allow fast completion and immediately show potential issues. They tend, however, to be quite tricky to set up. Especially so, when developing inside container or using custom compiler or cross-compiling. And ESP IDF projects use all three of those. The good news is, it is doable though.

What you need

ESP IDF

LLVM/Clang toolchain is still under active development in IDF, but it is available as a downloadable tool and it contains clangd language server, which can be used to properly assist with ESP32* (be it S3 or C6 or other) projects.

The cleanest, easiest (and most stable) way to get a working ESP IDF environment is with Docker images. Just pull it from Docker Hub:

docker pull espressif/idf:latest

This image has to be modified to contain the Clang toolchain, so start the container:

docker run -it espressif/idf:latest-clang

Then, inside the container:

idf_tools.py install esp-clang

This change to image needs to be committed, so, in another terminal, with a container still running, get the container ID:

docker ps -a
CONTAINER ID   IMAGE                        COMMAND                  CREATED          STATUS                       PORTS     NAMES
1c09ba994c0e   espressif/idf:latest-clang   "/opt/esp/entrypoint…"   40 minutes ago   Up 40 minutes                          suspicious_colden

and commit it:

docker commit 1c09ba994c0e espressif/idf:latest-clang

(-clang is added just to know that it's an image with Clang).

OrbStack is recommended as it allows mounting container/image files on host machine (it will come in handy during project setup).

LSP support in Sublime Text 4

Sublime Text 4 has plugins for LSP for large selection of languages. In this case we will use LSP-clangd. Unfortunately, it has an issue with using different clangd binaries per project. To overcome that, it's best to install LSP-clangd via Sublime's Package Control and then patch it by:

  1. Cloning LSP-clangd code into Sublime's Packages directory:
    git clone https://github.com/sublimelsp/LSP-clangd.git
  2. Adding support to use custom_command from project settings:
diff --git a/plugin.py b/plugin.py
index fd2b681..dbab05d 100644
--- a/plugin.py
+++ b/plugin.py
@@ -167,7 +167,7 @@ class Clangd(AbstractPlugin):
         configuration: ClientConfig,
     ) -> Optional[str]:

-        if get_settings().get("binary") == "custom":
+        if "custom" in [get_settings().get("binary"), configuration.init_options.get("binary")]:
             clangd_base_command = configuration.init_options.get("custom_command")  # type: List[str]
         else:
             clangd_path = cls.clangd_path()

Project setup

Create an ESP IDF project on your host, add its folder to Sublime Text, save the Sublime project and build code with clang:

  1. Start ESP IDF container (from root of ESP IDF project - here: /home/user/project):
    docker run --rm -v /home/user/project:/project -w /project -e HOME=/tmp -it espressif/idf:latest-clang
  2. Build with clang:
    IDF_TOOLCHAIN=clang idf.py -B clang.build build

    This will generate clang.build/compile_commands.json file that will be used by clangd language server. Now it's time to configure LSP-clangd in Sublime project. In the saved project.sublime-project file add settings, LSP, clangd sections, so project looks like this:

    {
    "folders":
    [
        {
            "path": "/home/user/project",
        },
    ],
    "settings": {
        "LSP": {
            "clangd": {
                "initializationOptions": {
                    "binary": "custom",
                    "custom_command": [
                        "docker", "run", "--rm", "-v", "/home/user/project:/project", "-v", "/home/user/idfsilence.sh:/opt/esp/entrypoint.sh", "-w", "/project", "-i",
                        "espressif/idf:latest-clang", "clangd"
                    ],
                    "clangd.all-scopes-completion": true,
                    "clangd.log": "verbose",
                    "clangd.compile-commands-dir": "/project/clang.build/",
                    "clangd.path-mappings": "/home/user/project=/project,/home/user/OrbStack/docker/images/espressif/idf:latest-clang/opt=/opt",
                },
                "enabled": true,
            },
        },
    },
    }

    What happened here? Quite a lot:

  3. "binary": "custom" is needed to trigger our plugin patch to use "custom_command"
  4. `"custom_command":
    • "-v", "/home/user/project:/project"- mounts IDF project under /home/user/projec into /project folder in container
    • "-w", "/project"- sets /project as the initial working directory
    • "--rm" - container will be deleted once it exits
    • "-i", "espressif/idf:latest-clang" - launches container in interactive mode
    • "clangd": starts language server
  5. "clangd.compile-commands-dir" - tells language server where to find compile_commands.json file we generated earlier (path is from container point of view)
  6. "clangd.path-mappings" - this is where OrbStack functionality comes in handy. If you mount espressif/idf:latest-clang image to your host, you will be able to get all ESP IDF internal code inside Sublime Text! LSP just needs to know how to translate paths in container to ones on your computer. /home/user/project is /project in container (this has to be the same as in "custom_command!), and whole ESP IDF is mapped from where OrbStack mounted it on computer (/home/user/OrbStack/docker/images/espressif/idf:latest-clang/opt) to default path in container (/opt)

There's one more trick here: "-v", "/home/user/idfsilence.sh:/opt/esp/entrypoint.sh". By default, ESP IDF containers output information about the environment in this container to console on startup. While it is useful when used by hand, it is not valid output from language server, so Sublime's LSP implementation will take it as error and will kill such container. The trick is to override container entry point (/opt/esp/entrypoint.sh as seen in COMMAND' column of docker ps -a output) with one that redirects it to /dev/null (it's basically default /opt/esp/entrypoint.sh with > /dev/null 2>&1 redirection added:

#!/usr/bin/env bash
set -e

# IDF_GIT_SAFE_DIR has the same format as system PATH environment variable.
# All path specified in IDF_GIT_SAFE_DIR will be added to user's
# global git config as safe.directory paths. For more information
# see git-config manual page.
if [ -n "${IDF_GIT_SAFE_DIR+x}" ]
then
    echo "Adding following directories into git's safe.directory"
    echo "$IDF_GIT_SAFE_DIR" | tr ':' '\n' | while read -r dir
    do
        git config --global --add safe.directory "$dir"
        echo "  $dir"
    done
fi

. $IDF_PATH/export.sh > /dev/null 2>&1

exec "$@"

If this file is saved to /home/user/idfsilence.sh the command will work as shown.

After that, clangd will continue working in background indexing and checking code in your ESP IDF project.

Share this Post:

Related posts