config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

lsp-clangd.el (13501B)


      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