lsp-clangd.el (13503B)
1 ;;; lsp-clangd.el --- LSP clients for the C Languages Family -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2020 Daniel Martin & emacs-lsp maintainers 4 ;; URL: https://github.com/emacs-lsp/lsp-mode 5 ;; Keywords: languages, c, cpp, clang 6 7 ;; This program is free software: you can redistribute it and/or modify 8 ;; it under the terms of the GNU General Public License as published by 9 ;; the Free Software Foundation, either version 3 of the License, or 10 ;; (at your option) any later version. 11 12 ;; This program is distributed in the hope that it will be useful, 13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 ;; GNU General Public License for more details. 16 17 ;; You should have received a copy of the GNU General Public License 18 ;; along with this program. If not, see <http://www.gnu.org/licenses/>. 19 20 ;;; Commentary: 21 22 ;; LSP clients for the C Languages Family. 23 24 ;; ** Clang-tidy Flycheck integration (Clangd) ** 25 ;; 26 ;; If you invoke `flycheck-display-error-explanation' on a 27 ;; `clang-tidy' error (if Clangd is configured to show `clang-tidy' 28 ;; diagnostics), Emacs will open a detailed explanation about the 29 ;; message by querying the LLVM website. As an embedded web browser is 30 ;; used to show the documentation, this feature requires that Emacs is 31 ;; compiled with libxml2 support. 32 33 ;;; Code: 34 35 (require 'lsp-mode) 36 (require 'cl-lib) 37 (require 'rx) 38 (require 'seq) 39 (require 'dom) 40 (eval-when-compile (require 'subr-x)) 41 42 (require 'dash) 43 (require 's) 44 45 (defvar flycheck-explain-error-buffer) 46 (declare-function flycheck-error-id "ext:flycheck" (err) t) 47 (declare-function flycheck-error-group "ext:flycheck" (err) t) 48 (declare-function flycheck-error-message "ext:flycheck" (err) t) 49 50 (defcustom lsp-clangd-version "15.0.6" 51 "Clangd version to download. 52 It has to be set before `lsp-clangd.el' is loaded and it has to 53 be available here: https://github.com/clangd/clangd/releases/" 54 :type 'string 55 :group 'lsp-clangd 56 :package-version '(lsp-mode . "8.0.0")) 57 58 (defcustom lsp-clangd-download-url 59 (format (pcase system-type 60 ('darwin "https://github.com/clangd/clangd/releases/download/%s/clangd-mac-%s.zip") 61 ('windows-nt "https://github.com/clangd/clangd/releases/download/%s/clangd-windows-%s.zip") 62 (_ "https://github.com/clangd/clangd/releases/download/%s/clangd-linux-%s.zip")) 63 lsp-clangd-version 64 lsp-clangd-version) 65 "Automatic download url for clangd" 66 :type 'string 67 :group 'lsp-clangd 68 :package-version '(lsp-mode . "8.0.0")) 69 70 (defcustom lsp-clangd-binary-path 71 (f-join lsp-server-install-dir (format "clangd/clangd_%s/bin" 72 lsp-clangd-version) 73 (pcase system-type 74 ('windows-nt "clangd.exe") 75 (_ "clangd"))) 76 "The path to `clangd' binary." 77 :type 'file 78 :group 'lsp-clangd 79 :package-version '(lsp-mode . "8.0.0")) 80 81 (lsp-dependency 82 'clangd 83 `(:download :url lsp-clangd-download-url 84 :decompress :zip 85 :store-path ,(f-join lsp-server-install-dir "clangd" "clangd.zip") 86 :binary-path lsp-clangd-binary-path 87 :set-executable? t)) 88 89 (defun lsp-cpp-flycheck-clang-tidy--skip-http-headers () 90 "Position point just after HTTP headers." 91 (re-search-forward "^$")) 92 93 (defun lsp-cpp-flycheck-clang-tidy--narrow-to-http-body () 94 "Narrow the current buffer to contain the body of an HTTP response." 95 (lsp-cpp-flycheck-clang-tidy--skip-http-headers) 96 (narrow-to-region (point) (point-max))) 97 98 (defun lsp-cpp-flycheck-clang-tidy--decode-region-as-utf8 (start end) 99 "Decode a region from START to END in UTF-8." 100 (condition-case nil 101 (decode-coding-region start end 'utf-8) 102 (coding-system-error nil))) 103 104 (defun lsp-cpp-flycheck-clang-tidy--remove-crlf () 105 "Remove carriage return and line feeds from the current buffer." 106 (save-excursion 107 (while (re-search-forward "\r$" nil t) 108 (replace-match "" t t)))) 109 110 (defun lsp-cpp-flycheck-clang-tidy--extract-relevant-doc-section () 111 "Extract the parts of the LLVM clang-tidy documentation that are relevant. 112 113 This function assumes that the current buffer contains the result 114 of browsing `clang.llvm.org', as returned by `url-retrieve'. 115 More concretely, this function returns the main <div> element 116 with class `section', and also removes `headerlinks'." 117 (goto-char (point-min)) 118 (lsp-cpp-flycheck-clang-tidy--narrow-to-http-body) 119 (lsp-cpp-flycheck-clang-tidy--decode-region-as-utf8 (point-min) (point-max)) 120 (lsp-cpp-flycheck-clang-tidy--remove-crlf) 121 (let* ((dom (libxml-parse-html-region (point-min) (point-max))) 122 (section (dom-by-class dom "section"))) 123 (dolist (headerlink (dom-by-class section "headerlink")) 124 (dom-remove-node section headerlink)) 125 section)) 126 127 (defun lsp-cpp-flycheck-clang-tidy--explain-error (explanation &rest args) 128 "Explain an error in the Flycheck error explanation buffer using EXPLANATION. 129 130 EXPLANATION is a function with optional ARGS that, when 131 evaluated, inserts the content in the appropriate Flycheck 132 buffer." 133 (with-current-buffer flycheck-explain-error-buffer 134 (let ((inhibit-read-only t) 135 (inhibit-modification-hooks t)) 136 (erase-buffer) 137 (apply explanation args) 138 (goto-char (point-min))))) 139 140 (defun lsp-cpp-flycheck-clang-tidy--show-loading-status () 141 "Show a loading string while clang-tidy documentation is fetched from llvm.org. 142 Recent versions of `flycheck' call `display-message-or-buffer' to 143 display error explanations. `display-message-or-buffer' displays 144 the documentation string either in the echo area or in a separate 145 window, depending on the string's height. This function forces to 146 always display it in a separate window by appending the required 147 number of newlines." 148 (let* ((num-lines-threshold 149 (round (if resize-mini-windows 150 (cond ((floatp max-mini-window-height) 151 (* (frame-height) 152 max-mini-window-height)) 153 ((integerp max-mini-window-height) 154 max-mini-window-height) 155 (t 156 1)) 157 1))) 158 (extra-new-lines (make-string (1+ num-lines-threshold) ?\n))) 159 (concat "Loading documentation..." extra-new-lines))) 160 161 (defun lsp-cpp-flycheck-clang-tidy--show-documentation (error-id) 162 "Show clang-tidy documentation about ERROR-ID. 163 164 Information comes from the clang.llvm.org website." 165 ;; Example error-id: modernize-loop-convert 166 ;; Example url: https://clang.llvm.org/extra/clang-tidy/checks/modernize/loop-convert.html 167 (setq error-id (s-join "/" (s-split-up-to "-" error-id 1 t))) 168 (url-retrieve (format 169 "https://clang.llvm.org/extra/clang-tidy/checks/%s.html" error-id) 170 (lambda (status) 171 (if-let* ((error-status (plist-get status :error))) 172 (lsp-cpp-flycheck-clang-tidy--explain-error 173 #'insert 174 (format 175 "Error accessing clang-tidy documentation: %s" 176 (error-message-string error-status))) 177 (let ((doc-contents 178 (lsp-cpp-flycheck-clang-tidy--extract-relevant-doc-section))) 179 (lsp-cpp-flycheck-clang-tidy--explain-error 180 #'shr-insert-document doc-contents))))) 181 (lsp-cpp-flycheck-clang-tidy--show-loading-status)) 182 183 ;;;###autoload 184 (defun lsp-cpp-flycheck-clang-tidy-error-explainer (error) 185 "Explain a clang-tidy ERROR by scraping documentation from llvm.org." 186 (unless (fboundp 'libxml-parse-html-region) 187 (error "This function requires Emacs to be compiled with libxml2")) 188 (if-let* ((clang-tidy-error-id (flycheck-error-id error))) 189 (condition-case err 190 (lsp-cpp-flycheck-clang-tidy--show-documentation clang-tidy-error-id) 191 (error 192 (format 193 "Error accessing clang-tidy documentation: %s" 194 (error-message-string err)))) 195 (error "The clang-tidy error message does not contain an [error-id]"))) 196 197 198 ;;; lsp-clangd 199 (defgroup lsp-clangd nil 200 "LSP support for C-family languages (C, C++, Objective-C, Objective-C++, CUDA), using clangd." 201 :group 'lsp-mode 202 :link '(url-link "https://clang.llvm.org/extra/clangd")) 203 204 (defcustom lsp-clients-clangd-executable nil 205 "The clangd executable to use. 206 When `'non-nil' use the name of the clangd executable file 207 available in your path to use. Otherwise the system will try to 208 find a suitable one. Set this variable before loading lsp." 209 :group 'lsp-clangd 210 :risky t 211 :type '(choice (file :tag "Path") 212 (const :tag "Auto" nil))) 213 214 (defvar lsp-clients--clangd-default-executable nil 215 "Clang default executable full path when found. 216 This must be set only once after loading the clang client.") 217 218 (defcustom lsp-clients-clangd-args '("--header-insertion-decorators=0") 219 "Extra arguments for the clangd executable." 220 :group 'lsp-clangd 221 :risky t 222 :type '(repeat string)) 223 224 (defcustom lsp-clients-clangd-library-directories '("/usr") 225 "List of directories which will be considered to be libraries." 226 :risky t 227 :type '(repeat string) 228 :group 'lsp-clangd 229 :package-version '(lsp-mode . "9.0.0")) 230 231 (defun lsp-clients--clangd-command () 232 "Generate the language server startup command." 233 (unless lsp-clients--clangd-default-executable 234 (setq lsp-clients--clangd-default-executable 235 (or (lsp-package-path 'clangd) 236 (-first #'executable-find 237 (-map (lambda (version) 238 (concat "clangd" version)) 239 ;; Prefer `clangd` without a version number appended. 240 (cl-list* "" (-map 241 (lambda (vernum) (format "-%d" vernum)) 242 (number-sequence 17 6 -1))))) 243 (lsp-clients-executable-find "xcodebuild" "-find-executable" "clangd") 244 (lsp-clients-executable-find "xcrun" "--find" "clangd")))) 245 246 `(,(or lsp-clients-clangd-executable lsp-clients--clangd-default-executable "clangd") 247 ,@lsp-clients-clangd-args)) 248 249 (lsp-register-client 250 (make-lsp-client :new-connection (lsp-stdio-connection 251 'lsp-clients--clangd-command) 252 :activation-fn (lsp-activate-on "c" "cpp" "objective-c" "cuda") 253 :priority -1 254 :server-id 'clangd 255 :library-folders-fn (lambda (_workspace) lsp-clients-clangd-library-directories) 256 :download-server-fn (lambda (_client callback error-callback _update?) 257 (lsp-package-ensure 'clangd callback error-callback)))) 258 259 (defun lsp-clangd-join-region (beg end) 260 "Apply join-line from BEG to END. 261 This function is useful when an indented function prototype needs 262 to be shown in a single line." 263 (save-excursion 264 (let ((end (copy-marker end))) 265 (goto-char beg) 266 (while (< (point) end) 267 (join-line 1))) 268 (s-trim (buffer-string)))) 269 270 (cl-defmethod lsp-clients-extract-signature-on-hover (contents (_server-id (eql clangd))) 271 "Extract a representative line from clangd's CONTENTS, to show in the echo area. 272 This function tries to extract the type signature from CONTENTS, 273 or the first line if it cannot do so. A single line is always 274 returned to avoid that the echo area grows uncomfortably." 275 (with-temp-buffer 276 (-let [value (lsp:markup-content-value contents)] 277 (insert value) 278 (goto-char (point-min)) 279 (if (re-search-forward (rx (seq "```cpp\n" 280 (opt (group "//" 281 (zero-or-more nonl) 282 "\n")) 283 (group 284 (one-or-more 285 (not (any "`"))) 286 "\n") 287 "```")) nil t nil) 288 (progn (narrow-to-region (match-beginning 2) (match-end 2)) 289 (lsp--render-element (lsp-make-marked-string 290 :language "cpp" 291 :value (lsp-clangd-join-region (point-min) (point-max))))) 292 (car (s-lines (lsp--render-element contents))))))) 293 294 (cl-defmethod lsp-diagnostics-flycheck-error-explainer (e (_server-id (eql clangd))) 295 "Explain a `flycheck-error' E that was generated by the Clangd language server." 296 (cond ((string-equal "clang-tidy" (flycheck-error-group e)) 297 (lsp-cpp-flycheck-clang-tidy-error-explainer e)) 298 (t (flycheck-error-message e)))) 299 300 (defun lsp-clangd-find-other-file (&optional new-window) 301 "Switch between the corresponding C/C++ source and header file. 302 If NEW-WINDOW (interactively the prefix argument) is non-nil, 303 open in a new window. 304 305 Only works with clangd." 306 (interactive "P") 307 (let ((other (lsp-send-request (lsp-make-request 308 "textDocument/switchSourceHeader" 309 (lsp--text-document-identifier))))) 310 (unless (s-present? other) 311 (user-error "Could not find other file")) 312 (funcall (if new-window #'find-file-other-window #'find-file) 313 (lsp--uri-to-path other)))) 314 315 (lsp-consistency-check lsp-clangd) 316 317 (provide 'lsp-clangd) 318 ;;; lsp-clangd.el ends here