LSP
(use-package lsp-mode :ensure t :init (setq lsp-keymap-prefix "C-c C-l" lsp-prefer-flymake nil lsp-keep-workspace-alive nil) :commands (lsp make-lsp-client lsp-register-client) :hook (lsp-mode . lsp-enable-which-key-integration) :config ;; this is a particularly nasty bug ;; https://github.com/bbatsov/projectile/issues/1387 (defun do.lsp/dont-resolve-symlinks-projectile (fun &rest args) "Call FUN with ARGS but disable the effects of `file-truename'." (cl-letf (((symbol-function 'file-truename) #'identity)) (apply fun args))) (advice-add 'lsp--suggest-project-root :around #'do.lsp/dont-resolve-symlinks-projectile) <<do.lsp/docker>>) (use-package lsp-ui :ensure t :init (setq lsp-ui-flycheck-enable t) :commands lsp-ui-mode) (use-package dap-mode :ensure t :after lsp-mode :config (dap-mode t) (dap-ui-mode t))
clangd in Docker
This is the start of some code to allow for clangd in a docker container. But I can't get this to compile yet so I'll just park it here for now.
(require 'dash) (require 'lsp) (require 'lsp-clangd) (require 's) ;; ;; Helper functions. I copied some of them from https://github.com/emacs-lsp/lsp-docker/ ;; ;; stolen from lsp-docker (defun lsp-docker--uri->path (path-mappings docker-container-name uri) "Turn docker URI into host path. Argument PATH-MAPPINGS dotted pair of (host-path . container-path). Argument DOCKER-CONTAINER-NAME name to use when running container. Argument URI the uri to translate." (let ((path (lsp--uri-to-path-1 uri))) (-if-let ((local . remote) (-first (-lambda ((_ . docker-path)) (s-contains? docker-path path)) path-mappings)) (s-replace remote local path) (format "/docker:%s:%s" docker-container-name path)))) (defun lsp-docker--path->uri (path-mappings path) "Turn host PATH into docker uri. Argument PATH-MAPPINGS dotted pair of (host-path . container-path). Argument PATH the path to translate." (lsp--path-to-uri-1 (-if-let ((local . remote) (-first (-lambda ((local-path . _)) (s-contains? local-path path)) path-mappings)) (s-replace local remote path) (user-error "The path %s is not under path mappings" path)))) ;; my functions to use docker containers (defun do.cpp/format-docker-path-mappings (path-mappings) (->> path-mappings (-map (-lambda ((path . docker-path)) (format "-v %s:%s" path docker-path))) (s-join " "))) (defun do.cpp/launch-new-container (docker-container-name path-mappings docker-image-id server-command) "Return the docker command to be executed on host. Argument DOCKER-CONTAINER-NAME name to use for container. Argument PATH-MAPPINGS dotted pair of (host-path . container-path). Argument DOCKER-IMAGE-ID the docker container to run language servers with. Argument SERVER-COMMAND the language server command to run inside the container." (split-string (--doto (format "docker run --name %s --rm -i %s %s %s" docker-container-name (do.cpp/format-docker-path-mappings path-mappings) docker-image-id server-command)) " ")) (defun do.cpp/exec-in-running-container (docker-container-name command) "Return the docker command to be executed on host. The goal is to run inside of an already running container. Argument DOCKER-CONTAINER-NAME name to use for container. Argument COMMAND the language server command to run inside the container." (split-string (--doto (format "docker exec -i %s %s" docker-container-name command)) " ")) (defun do.cpp/is-docker-container-running? (container-name) "See if a container with the name CONTAINER-NAME is currently running." (let ((output (shell-command-to-string (format "docker container ls -q --filter \"name=%s\"" container-name)))) (not (string-empty-p output)))) ;; ;; these variables (in addition to `do-cpp-project-dir' and `do-cpp-build-dir') ;; need to be set in the .dir-locals.el file to make this work. ;; (defvar do-cpp-docker-image-name nil "The name of the development docker container image.") (defvar do-cpp-docker-container-name nil "The name of the development docker container") (defvar do-cpp-docker-work-dir nil "The path to the working directory of the dev docker container.") (defvar do-cpp-docker-work-dir-docker-path "/work" "The default path to the working directory of the dev docker container. (This is the path in the container)") (defvar do-cpp-docker-clangd-path "clangd" "The path to the clangd executable inside of the docker container.") ;; ;; define a language server client that connects to a clangd in a ;; project-specific docker container ;; (let ((client (copy-lsp--client (gethash 'clangd lsp-clients)))) (setf ;; higher priority wins, we want to try this one first (lsp--client-priority client) 20 ;; we use this activation function to determine whether the buffer should ;; use the remote clangd or not. `do-cpp-docker-container-name' needs to be ;; set as a dir-local variable and needs to contain the name of a docker ;; container, same as the others (lsp--client-activation-fn client) (lambda (filename major-mode) (and (bound-and-true-p do-cpp-build-dir) (bound-and-true-p do-cpp-project-dir) (bound-and-true-p do-cpp-docker-image-name) (bound-and-true-p do-cpp-docker-container-name) (bound-and-true-p do-cpp-docker-work-dir) (bound-and-true-p do-cpp-docker-clangd-path))) ;; we use the path conversion functions to convert between local and docker ;; paths. The convention is that I map exactly one directory ;; (`do-cpp-docker-work-dir') to `do-cpp-docker-work-dir-docker-path' in the ;; container. the LSP magic in the background translates between these ;; paths. (lsp--client-uri->path-fn client) (lambda (uri) (let ((path-mappings (list (cons (file-name-as-directory (file-truename do-cpp-docker-work-dir)) (file-name-as-directory do-cpp-docker-work-dir-docker-path))))) (lsp-docker--uri->path path-mappings do-cpp-docker-container-name uri))) (lsp--client-path->uri-fn client) (lambda (path) (let ((path-mappings (list (cons (file-name-as-directory (file-truename do-cpp-docker-work-dir)) (file-name-as-directory do-cpp-docker-work-dir-docker-path))))) (lsp-docker--path->uri path-mappings (file-truename path)))) ;; we create an stdio connection through docker (lsp--client-new-connection client) (lsp-stdio-connection (lambda () ;; point the clangd to the compile_commands.json file in the build directory (let ((server-command (s-join " " `(,do-cpp-docker-clangd-path "--compile-commands-dir" ,(s-replace (file-truename do-cpp-docker-work-dir) (file-name-as-directory do-cpp-docker-work-dir-docker-path) (file-name-as-directory (file-truename do-cpp-build-dir))) ,@(when (boundp 'do-cpp-clangd-query-driver) (list "--query-driver" do-cpp-clangd-query-driver)) "-j=4" "-background-index"))) (path-mappings (list (cons (file-name-as-directory (file-truename do-cpp-docker-work-dir)) (file-name-as-directory do-cpp-docker-work-dir-docker-path))))) ;; either start a new container or execute in one that is currently running (if (do.cpp/is-docker-container-running? do-cpp-docker-container-name) (do.cpp/exec-in-running-container do-cpp-docker-container-name server-command) (do.cpp/launch-new-container do-cpp-docker-container-name path-mappings do-cpp-docker-image-name server-command)))))) ;; just because we might need to run a few of these at the same time, we hack ;; it and register five of them and then use dir-locals to decide which one ;; to launch. it ain't pretty, but it works. (dolist (id '(docker-clangd docker-clangd-1 docker-clangd-2 docker-clangd-3 docker-clangd-4)) (let ((c (copy-lsp--client client))) (setf (lsp--client-server-id c) id) (unless (eq id 'docker-clangd) (setf (lsp--client-priority c) 10)) (lsp-register-client c)))) ;; hacketty hacckety: since I am using `file-truename' in the path to uri ;; conversions, this screws up the internal hash table for clangd ;; diagnostics. The issue can be reproduced by attaching ielm to a cpp buffer ;; (M-x ielm-change-working-buffer) and seeing the difference in outputs: ;; (lsp--get-buffer-diagnostics) and (json-encode (lsp-diagnostics)). The gist ;; is that the diagnostics are arriving from the checker, but a hashtable ;; lookup inserts them with the key (buffer-file-name) but then attempts to ;; read them with the key (file-truename (buffer-file-name)). This happens ;; because I am using `file-truename' in the uri->path converseion function. ;; ;; There are two options: 1. be consistent between the dirlocals paths and the ;; way I access the files in emacs (i.e., only use the linked paths when ;; accessing) or 2. try to hack LSP. There is an avenue to get this to work by ;; hacking the function `lsp--fix-path-casing'. This function is exclusively ;; called before inserting and looking up `buffer-file-name' in the diagnostics ;; hash table... So I can just advise it and splice in a call to ;; `file-truename'... lord have mercy. I will definitely have to fix this in a ;; later version... ;; (add-function :filter-return (symbol-function 'lsp--fix-path-casing) #'file-truename) ;; the problem with the above approach is that this might screw me up with ;; other checkers---once where I am not doing the strange translating bit. TBH ;; I should just stop using softlinks. ;; let us try this: (setq find-file-visit-truename t) ;; we also clone the regular clangd a few times so we can open multiple C and ;; C++ projects at the same time. (dolist (id '(clangd-1 clangd-2 clangd-3 clangd-4)) (let ((c (copy-lsp--client (gethash 'clangd lsp-clients)))) (setf (lsp--client-server-id c) id (lsp--client-priority c) -5) (lsp-register-client c)))