aboutsummaryrefslogtreecommitdiffstats
path: root/elpa/lsp-mode-20220505.630/lsp-clangd.el
blob: 1a88877319e87d31d4bd50801036ef5bc2e86348 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
;;; lsp-clangd.el --- LSP clients for the C Languages Family -*- lexical-binding: t; -*-

;; Copyright (C) 2020 Daniel Mart�n & emacs-lsp maintainers
;; URL: https://github.com/emacs-lsp/lsp-mode
;; Keywords: languages, c, cpp, clang

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; LSP clients for the C Languages Family.

;; ** Clang-tidy Flycheck integration (Clangd) **
;;
;; If you invoke `flycheck-display-error-explanation' on a
;; `clang-tidy' error (if Clangd is configured to show `clang-tidy'
;; diagnostics), Emacs will open a detailed explanation about the
;; message by querying the LLVM website. As an embedded web browser is
;; used to show the documentation, this feature requires that Emacs is
;; compiled with libxml2 support.

;;; Code:

(require 'lsp-mode)
(require 'cl-lib)
(require 'rx)
(require 'seq)
(require 'dom)
(eval-when-compile (require 'subr-x))

(require 'dash)
(require 's)

(defvar flycheck-explain-error-buffer)
(declare-function flycheck-error-id "ext:flycheck" (err) t)
(declare-function flycheck-error-group "ext:flycheck" (err) t)
(declare-function flycheck-error-message "ext:flycheck" (err) t)

(defcustom lsp-clangd-version "13.0.0"
  "Clangd version to download.
It has to be set before `lsp-clangd.el' is loaded and it has to
be available here: https://github.com/clangd/clangd/releases/"
  :type 'string
  :group 'lsp-clangd
  :package-version '(lsp-mode . "8.0.0"))

(defcustom lsp-clangd-download-url
  (format (pcase system-type
            ('darwin "https://github.com/clangd/clangd/releases/download/%s/clangd-mac-%s.zip")
            ('windows-nt "https://github.com/clangd/clangd/releases/download/%s/clangd-windows-%s.zip")
            (_ "https://github.com/clangd/clangd/releases/download/%s/clangd-linux-%s.zip"))
          lsp-clangd-version
          lsp-clangd-version)
  "Automatic download url for clangd"
  :type 'string
  :group 'lsp-clangd
  :package-version '(lsp-mode . "8.0.0"))

(defcustom lsp-clangd-binary-path
  (f-join lsp-server-install-dir (format "clangd/clangd_%s/bin"
                                         lsp-clangd-version)
          (pcase system-type
            ('windows-nt "clangd.exe")
            (_ "clangd")))
  "The path to `clangd' binary."
  :type 'file
  :group 'lsp-clangd
  :package-version '(lsp-mode . "8.0.0"))

(lsp-dependency
 'clangd
 `(:download :url lsp-clangd-download-url
             :decompress :zip
             :store-path ,(f-join lsp-server-install-dir "clangd" "clangd.zip")
             :binary-path lsp-clangd-binary-path
             :set-executable? t))

(defun lsp-cpp-flycheck-clang-tidy--skip-http-headers ()
  "Position point just after HTTP headers."
  (re-search-forward "^$"))

(defun lsp-cpp-flycheck-clang-tidy--narrow-to-http-body ()
  "Narrow the current buffer to contain the body of an HTTP response."
  (lsp-cpp-flycheck-clang-tidy--skip-http-headers)
  (narrow-to-region (point) (point-max)))

(defun lsp-cpp-flycheck-clang-tidy--decode-region-as-utf8 (start end)
  "Decode a region from START to END in UTF-8."
  (condition-case nil
      (decode-coding-region start end 'utf-8)
    (coding-system-error nil)))

(defun lsp-cpp-flycheck-clang-tidy--remove-crlf ()
  "Remove carriage return and line feeds from the current buffer."
  (save-excursion
    (while (re-search-forward "\r$" nil t)
      (replace-match "" t t))))

(defun lsp-cpp-flycheck-clang-tidy--extract-relevant-doc-section ()
  "Extract the parts of the LLVM clang-tidy documentation that are relevant.

This function assumes that the current buffer contains the result
of browsing 'clang.llvm.org', as returned by `url-retrieve'.
More concretely, this function returns the main <div> element
with class 'section', and also removes 'headerlinks'."
  (goto-char (point-min))
  (lsp-cpp-flycheck-clang-tidy--narrow-to-http-body)
  (lsp-cpp-flycheck-clang-tidy--decode-region-as-utf8 (point-min) (point-max))
  (lsp-cpp-flycheck-clang-tidy--remove-crlf)
  (let* ((dom (libxml-parse-html-region (point-min) (point-max)))
         (section (dom-by-class dom "section")))
    (dolist (headerlink (dom-by-class section "headerlink"))
      (dom-remove-node section headerlink))
    section))

(defun lsp-cpp-flycheck-clang-tidy--explain-error (explanation &rest args)
  "Explain an error in the Flycheck error explanation buffer using EXPLANATION.

EXPLANATION is a function with optional ARGS that, when
evaluated, inserts the content in the appropriate Flycheck
buffer."
  (with-current-buffer flycheck-explain-error-buffer
    (let ((inhibit-read-only t)
          (inhibit-modification-hooks t))
      (erase-buffer)
      (apply explanation args)
      (goto-char (point-min)))))

(defun lsp-cpp-flycheck-clang-tidy--show-loading-status ()
  "Show a loading string while clang-tidy documentation is fetched from llvm.org.
Recent versions of `flycheck' call `display-message-or-buffer' to
display error explanations. `display-message-or-buffer' displays
the documentation string either in the echo area or in a separate
window, depending on the string's height. This function forces to
always display it in a separate window by appending the required
number of newlines."
  (let* ((num-lines-threshold
          (round (if resize-mini-windows
                     (cond ((floatp max-mini-window-height)
                            (* (frame-height)
                               max-mini-window-height))
                           ((integerp max-mini-window-height)
                            max-mini-window-height)
                           (t
                            1))
                   1)))
         (extra-new-lines (make-string (1+ num-lines-threshold) ?\n)))
    (concat "Loading documentation..." extra-new-lines)))

(defun lsp-cpp-flycheck-clang-tidy--show-documentation (error-id)
  "Show clang-tidy documentation about ERROR-ID.

Information comes from the clang.llvm.org website."
  (url-retrieve (format
                 "https://clang.llvm.org/extra/clang-tidy/checks/%s.html" error-id)
                (lambda (status)
                  (if-let ((error-status (plist-get status :error)))
                      (lsp-cpp-flycheck-clang-tidy--explain-error
                       #'insert
                       (format
                        "Error accessing clang-tidy documentation: %s"
                        (error-message-string error-status)))
                    (let ((doc-contents
                           (lsp-cpp-flycheck-clang-tidy--extract-relevant-doc-section)))
                      (lsp-cpp-flycheck-clang-tidy--explain-error
                       #'shr-insert-document doc-contents)))))
  (lsp-cpp-flycheck-clang-tidy--show-loading-status))

;;;###autoload
(defun lsp-cpp-flycheck-clang-tidy-error-explainer (error)
  "Explain a clang-tidy ERROR by scraping documentation from llvm.org."
  (unless (fboundp 'libxml-parse-html-region)
    (error "This function requires Emacs to be compiled with libxml2"))
  (if-let ((clang-tidy-error-id (flycheck-error-id error)))
      (condition-case err
          (lsp-cpp-flycheck-clang-tidy--show-documentation clang-tidy-error-id)
        (error
         (format
          "Error accessing clang-tidy documentation: %s"
          (error-message-string err))))
    (error "The clang-tidy error message does not contain an [error-id]")))


;;; lsp-clangd
(defgroup lsp-clangd nil
  "LSP support for C-family languages (C, C++, Objective-C, Objective-C++), using clangd."
  :group 'lsp-mode
  :link '(url-link "https://clang.llvm.org/extra/clangd"))

(defcustom lsp-clients-clangd-executable nil
  "The clangd executable to use.
When `'non-nil' use the name of the clangd executable file
available in your path to use. Otherwise the system will try to
find a suitable one. Set this variable before loading lsp."
  :group 'lsp-clangd
  :risky t
  :type '(choice (file :tag "Path")
                 (const :tag "Auto" nil)))

(defvar lsp-clients--clangd-default-executable nil
  "Clang default executable full path when found.
This must be set only once after loading the clang client.")

(defcustom lsp-clients-clangd-args '("--header-insertion-decorators=0")
  "Extra arguments for the clangd executable."
  :group 'lsp-clangd
  :risky t
  :type '(repeat string))

(defcustom lsp-clients-clangd-library-directories '("/usr")
  "List of directories which will be considered to be libraries."
  :risky t
  :type '(repeat string)
  :group 'lsp-clangd
  :package-version '(lsp-mode . "8.0.1"))

(defun lsp-clients--clangd-command ()
  "Generate the language server startup command."
  (unless lsp-clients--clangd-default-executable
    (setq lsp-clients--clangd-default-executable
          (or (lsp-package-path 'clangd)
              (-first #'executable-find
                      (-map (lambda (version)
                              (concat "clangd" version))
                            ;; Prefer `clangd` without a version number appended.
                            (cl-list* "" (-map
                                          (lambda (vernum) (format "-%d" vernum))
                                          (number-sequence 14 6 -1)))))
              (lsp-clients-executable-find "xcodebuild" "-find-executable" "clangd")
              (lsp-clients-executable-find "xcrun" "--find" "clangd"))))

  `(,(or lsp-clients-clangd-executable lsp-clients--clangd-default-executable "clangd")
    ,@lsp-clients-clangd-args))

(lsp-register-client
 (make-lsp-client :new-connection (lsp-stdio-connection
                                   'lsp-clients--clangd-command)
                  :activation-fn (lsp-activate-on "c" "cpp" "objective-c")
                  :priority -1
                  :server-id 'clangd
                  :library-folders-fn (lambda (_workspace) lsp-clients-clangd-library-directories)
                  :download-server-fn (lambda (_client callback error-callback _update?)
                                        (lsp-package-ensure 'clangd callback error-callback))))

(defun lsp-clangd-join-region (beg end)
  "Apply join-line from BEG to END.
This function is useful when an indented function prototype needs
to be shown in a single line."
  (save-excursion
    (let ((end (copy-marker end)))
      (goto-char beg)
      (while (< (point) end)
        (join-line 1)))
    (s-trim (buffer-string))))

(cl-defmethod lsp-clients-extract-signature-on-hover (contents (_server-id (eql clangd)))
  "Extract a representative line from clangd's CONTENTS, to show in the echo area.
This function tries to extract the type signature from CONTENTS,
or the first line if it cannot do so. A single line is always
returned to avoid that the echo area grows uncomfortably."
  (with-temp-buffer
    (-let [value (lsp:markup-content-value contents)]
      (insert value)
      (goto-char (point-min))
      (if (re-search-forward (rx (seq "```cpp\n"
                                      (opt (group "//"
                                                  (zero-or-more nonl)
                                                  "\n"))
                                      (group
                                       (one-or-more
                                        (not (any "`")))
                                       "\n")
                                      "```")) nil t nil)
          (progn (narrow-to-region (match-beginning 2) (match-end 2))
                 (lsp--render-element (lsp-make-marked-string
                                       :language "cpp"
                                       :value (lsp-clangd-join-region (point-min) (point-max)))))
        (car (s-lines (lsp--render-element contents)))))))

(cl-defmethod lsp-diagnostics-flycheck-error-explainer (e (_server-id (eql clangd)))
  "Explain a `flycheck-error' E that was generated by the Clangd language server."
  (cond ((string-equal "clang-tidy" (flycheck-error-group e))
         (lsp-cpp-flycheck-clang-tidy-error-explainer e))
        (t (flycheck-error-message e))))

(defun lsp-clangd-find-other-file (&optional new-window)
  "Switch between the corresponding C/C++ source and header file.
If NEW-WINDOW (interactively the prefix argument) is non-nil,
open in a new window.

Only works with clangd."
  (interactive "P")
  (let ((other (lsp-send-request (lsp-make-request
                                  "textDocument/switchSourceHeader"
                                  (lsp--text-document-identifier)))))
    (unless (s-present? other)
      (user-error "Could not find other file"))
    (funcall (if new-window #'find-file-other-window #'find-file)
             (lsp--uri-to-path other))))

(lsp-consistency-check lsp-clangd)

(provide 'lsp-clangd)
;;; lsp-clangd.el ends here