lsp-eslint.el (20164B)
1 ;;; lsp-eslint.el --- lsp-mode eslint integration -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2019 Ivan Yonchovski 4 5 ;; Author: Ivan Yonchovski <yyoncho@gmail.com> 6 ;; Keywords: languages 7 8 ;; This program is free software; you can redistribute it and/or modify 9 ;; it under the terms of the GNU General Public License as published by 10 ;; the Free Software Foundation, either version 3 of the License, or 11 ;; (at your option) any later version. 12 13 ;; This program is distributed in the hope that it will be useful, 14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 ;; GNU General Public License for more details. 17 18 ;; You should have received a copy of the GNU General Public License 19 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 20 21 ;;; Commentary: 22 23 ;; 24 25 ;;; Code: 26 27 (require 'lsp-protocol) 28 (require 'lsp-mode) 29 30 (defconst lsp-eslint/status-ok 1) 31 (defconst lsp-eslint/status-warn 2) 32 (defconst lsp-eslint/status-error 3) 33 34 (defgroup lsp-eslint nil 35 "ESLint language server group." 36 :group 'lsp-mode 37 :link '(url-link "https://github.com/microsoft/vscode-eslint")) 38 39 (defcustom lsp-eslint-unzipped-path (f-join lsp-server-install-dir "eslint/unzipped") 40 "The path to the file in which `eslint' will be stored." 41 :type 'file 42 :group 'lsp-eslint 43 :package-version '(lsp-mode . "8.0.0")) 44 45 (defcustom lsp-eslint-download-url "https://github.com/emacs-lsp/lsp-server-binaries/blob/master/dbaeumer.vscode-eslint-3.0.10.vsix?raw=true" 46 "ESLint language server download url." 47 :type 'string 48 :group 'lsp-eslint 49 :package-version '(lsp-mode . "9.0.0")) 50 51 (defcustom lsp-eslint-server-command `("node" 52 "~/server/out/eslintServer.js" 53 "--stdio") 54 "Command to start ESLint server." 55 :risky t 56 :type '(repeat string) 57 :package-version '(lsp-mode . "6.3")) 58 59 (defcustom lsp-eslint-enable t 60 "Controls whether ESLint is enabled for JavaScript files or not." 61 :type 'boolean 62 :package-version '(lsp-mode . "6.3")) 63 64 (defcustom lsp-eslint-package-manager "npm" 65 "The package manager you use to install node modules." 66 :type '(choice (const :tag "npm" "npm") 67 (const :tag "yarn" "yarn") 68 (const :tag "pnpm" "pnpm") 69 (string :tag "other")) 70 :package-version '(lsp-mode . "6.3")) 71 72 (defcustom lsp-eslint-format t 73 "Whether to perform format." 74 :type 'boolean 75 :package-version '(lsp-mode . "6.3")) 76 77 (defcustom lsp-eslint-node-path nil 78 "A path added to NODE_PATH when resolving the `eslint' module." 79 :type '(repeat string) 80 :package-version '(lsp-mode . "6.3")) 81 82 (defcustom lsp-eslint-node "node" 83 "Path to Node.js." 84 :type 'file 85 :package-version '(lsp-mode . "8.0.0")) 86 87 (defcustom lsp-eslint-options nil 88 "The ESLint options object to provide args normally passed to 89 `eslint' when executed from a command line (see 90 https://eslint.org/docs/latest/integrate/nodejs-api)." 91 :type 'alist) 92 93 (defcustom lsp-eslint-experimental nil 94 "The eslint experimental configuration." 95 :type 'alist) 96 97 (defcustom lsp-eslint-config-problems nil 98 "The eslint problems configuration." 99 :type 'alist) 100 101 (defcustom lsp-eslint-time-budget nil 102 "The eslint config to inform you of slow validation times and 103 long ESLint runs when computing code fixes during save." 104 :type 'alist) 105 106 (defcustom lsp-eslint-trace-server "off" 107 "Traces the communication between VSCode and the ESLint linter service." 108 :type 'string) 109 110 (defcustom lsp-eslint-run "onType" 111 "Run the linter on save (onSave) or on type (onType)" 112 :type '(choice (const :tag "onSave" "onSave") 113 (const :tag "onType" "onType")) 114 :package-version '(lsp-mode . "6.3")) 115 116 (defcustom lsp-eslint-auto-fix-on-save nil 117 "Turns auto fix on save on or off." 118 :type 'boolean 119 :package-version '(lsp-mode . "6.3")) 120 121 (defcustom lsp-eslint-fix-all-problem-type "all" 122 "Determines which problems are fixed when running the 123 source.fixAll code action." 124 :type '(choice 125 (const "all") 126 (const "problems") 127 string) 128 :package-version '(lsp-mode . "7.0.1")) 129 130 (defcustom lsp-eslint-quiet nil 131 "Turns on quiet mode, which ignores warnings." 132 :type 'boolean 133 :package-version '(lsp-mode . "6.3")) 134 135 (defcustom lsp-eslint-working-directories [] 136 "A vector of working directory names to use. Can be a pattern, an absolute path 137 or a path relative to the workspace. Examples: 138 - \"/home/user/abc/\" 139 - \"abc/\" 140 - (directory \"abc\") which is equivalent to \"abc\" above 141 - (pattern \"abc/*\") 142 Note that the home directory reference ~/ is not currently supported, use 143 /home/[user]/ instead." 144 :type 'lsp-string-vector 145 :package-version '(lsp-mode . "6.3")) 146 147 (defcustom lsp-eslint-validate '("svelte") 148 "An array of language ids which should always be validated by ESLint." 149 :type '(repeat string) 150 :package-version '(lsp-mode . "8.0.0")) 151 152 (defcustom lsp-eslint-provide-lint-task nil 153 "Controls whether a task for linting the whole workspace will be available." 154 :type 'boolean 155 :package-version '(lsp-mode . "6.3")) 156 157 (defcustom lsp-eslint-lint-task-enable nil 158 "Controls whether a task for linting the whole workspace will be available." 159 :type 'boolean 160 :package-version '(lsp-mode . "6.3")) 161 162 (defcustom lsp-eslint-lint-task-options "." 163 "Command line options applied when running the task for linting the whole 164 workspace (see https://eslint.org/docs/user-guide/command-line-interface)." 165 :type 'string 166 :package-version '(lsp-mode . "6.3")) 167 168 (defcustom lsp-eslint-runtime nil 169 "The location of the node binary to run ESLint under." 170 :type '(repeat string) 171 :package-version '(lsp-mode . "6.3")) 172 173 (defcustom lsp-eslint-code-action-disable-rule-comment t 174 "Controls whether code actions to add a rule-disabling comment should be shown." 175 :type 'bool 176 :package-version '(lsp-mode . "6.3")) 177 178 (defcustom lsp-eslint-code-action-disable-rule-comment-location "separateLine" 179 "Controls where the disable rule code action places comments. 180 181 Accepts the following values: 182 - \"separateLine\": Add the comment above the line to be disabled (default). 183 - \"sameLine\": Add the comment on the same line that will be disabled." 184 :type '(choice 185 (const "separateLine") 186 (const "sameLine")) 187 :package-version '(lsp-mode . "8.0.0")) 188 189 (defcustom lsp-eslint-code-action-show-documentation t 190 "Controls whether code actions to show documentation for an ESLint rule should 191 be shown." 192 :type 'bool 193 :package-version '(lsp-mode . "8.0.0")) 194 195 (defcustom lsp-eslint-warn-on-ignored-files nil 196 "Controls whether a warning should be emitted when a file is ignored." 197 :type 'bool 198 :package-version '(lsp-mode . "8.0.0")) 199 200 (defcustom lsp-eslint-rules-customizations [] 201 "Controls severity overrides for ESLint rules. 202 203 The value is a vector of alists, with each alist containing the following keys: 204 - rule - The rule to match. Can match wildcards with *, or be prefixed with ! 205 to negate the match. 206 - severity - The severity to report this rule as. Can be one of the following: 207 - \"off\": Disable the rule. 208 - \"info\": Report as informational. 209 - \"warn\": Report as a warning. 210 - \"error\": Report as an error. 211 - \"upgrade\": Increase by 1 severity level (eg. warning -> error). 212 - \"downgrade\": Decrease by 1 severity level (eg. warning -> info). 213 - \"default\": Report as the same severity specified in the ESLint config." 214 :type '(lsp-repeatable-vector 215 (alist :options ((rule string) 216 (severity (choice 217 (const "off") 218 (const "info") 219 (const "warn") 220 (const "error") 221 (const "upgrade") 222 (const "downgrade") 223 (const "default")))))) 224 :package-version '(lsp-mode . "8.0.0")) 225 226 (defcustom lsp-eslint-experimental-incremental-sync t 227 "Controls whether the new incremental text document synchronization should 228 be used." 229 :type 'boolean 230 :package-version '(lsp-mode . "6.3")) 231 232 (defcustom lsp-eslint-save-library-choices t 233 "Controls whether to remember choices made to permit or deny ESLint libraries 234 from running." 235 :type 'boolean 236 :package-version '(lsp-mode . "8.0.0")) 237 238 (defcustom lsp-eslint-library-choices-file (expand-file-name (locate-user-emacs-file ".lsp-eslint-choices")) 239 "The file where choices to permit or deny ESLint libraries from running is 240 stored." 241 :type 'string 242 :package-version '(lsp-mode . "8.0.0")) 243 244 (defun lsp--find-eslint () 245 (or 246 (when-let ((workspace-folder (lsp-find-session-folder (lsp-session) default-directory))) 247 (let ((eslint-local-path (f-join workspace-folder "node_modules" ".bin" 248 (if (eq system-type 'windows-nt) "eslint.cmd" "eslint")))) 249 (when (f-exists? eslint-local-path) 250 eslint-local-path))) 251 "eslint")) 252 253 (defun lsp-eslint-create-default-configuration () 254 "Create default ESLint configuration." 255 (interactive) 256 (unless (lsp-session-folders (lsp-session)) 257 (user-error "There are no workspace folders")) 258 (pcase (->> (lsp-session) 259 lsp-session-folders 260 (-filter (lambda (dir) 261 (-none? 262 (lambda (file) (f-exists? (f-join dir file))) 263 '(".eslintrc.js" ".eslintrc.yaml" ".eslintrc.yml" ".eslintrc" ".eslintrc.json"))))) 264 (`nil (user-error "All workspace folders contain ESLint configuration")) 265 (folders (let ((default-directory (completing-read "Select project folder: " folders nil t))) 266 (async-shell-command (format "%s --init" (lsp--find-eslint))))))) 267 268 (lsp-defun lsp-eslint-status-handler (workspace (&eslint:StatusParams :state)) 269 (setf (lsp--workspace-status-string workspace) 270 (propertize "ESLint" 271 'face (cond 272 ((eq state lsp-eslint/status-error) 'error) 273 ((eq state lsp-eslint/status-warn) 'warn) 274 (t 'success))))) 275 276 (lsp-defun lsp-eslint--configuration (_workspace (&ConfigurationParams :items)) 277 (->> items 278 (seq-map (-lambda ((&ConfigurationItem :scope-uri?)) 279 (-when-let* ((file (lsp--uri-to-path scope-uri?)) 280 (buffer (find-buffer-visiting file)) 281 (workspace-folder (lsp-find-session-folder (lsp-session) file))) 282 (with-current-buffer buffer 283 (let ((working-directory (lsp-eslint--working-directory workspace-folder file))) 284 (list :validate (if (member (lsp-buffer-language) lsp-eslint-validate) "on" "probe") 285 :packageManager lsp-eslint-package-manager 286 :codeAction (list 287 :disableRuleComment (list 288 :enable (lsp-json-bool lsp-eslint-code-action-disable-rule-comment) 289 :location lsp-eslint-code-action-disable-rule-comment-location) 290 :showDocumentation (list 291 :enable (lsp-json-bool lsp-eslint-code-action-show-documentation))) 292 :codeActionOnSave (list :enable (lsp-json-bool lsp-eslint-auto-fix-on-save) 293 :mode lsp-eslint-fix-all-problem-type) 294 :format (lsp-json-bool lsp-eslint-format) 295 :quiet (lsp-json-bool lsp-eslint-quiet) 296 :onIgnoredFiles (if lsp-eslint-warn-on-ignored-files "warn" "off") 297 :options (or lsp-eslint-options (ht)) 298 :experimental (or lsp-eslint-experimental (ht)) 299 :problems (or lsp-eslint-config-problems (ht)) 300 :timeBudget (or lsp-eslint-time-budget (ht)) 301 :rulesCustomizations lsp-eslint-rules-customizations 302 :run lsp-eslint-run 303 :nodePath lsp-eslint-node-path 304 :workingDirectory (when working-directory 305 (list 306 :directory working-directory 307 :!cwd :json-false)) 308 :workspaceFolder (list :uri (lsp--path-to-uri workspace-folder) 309 :name (f-filename workspace-folder)))))))) 310 (apply #'vector))) 311 312 (defun lsp-eslint--working-directory (workspace current-file) 313 "Find the first directory in the parameter config.workingDirectories which 314 contains the current file" 315 (let ((directories (-map (lambda (dir) 316 (when (and (listp dir) (plist-member dir 'directory)) 317 (setq dir (plist-get dir 'directory))) 318 (if (and (listp dir) (plist-member dir 'pattern)) 319 (progn 320 (setq dir (plist-get dir 'pattern)) 321 (when (not (f-absolute? dir)) 322 (setq dir (f-join workspace dir))) 323 (f-glob dir)) 324 (if (f-absolute? dir) 325 dir 326 (f-join workspace dir)))) 327 (append lsp-eslint-working-directories nil)))) 328 (-first (lambda (dir) (f-ancestor-of-p dir current-file)) (-flatten directories)))) 329 330 (lsp-defun lsp-eslint--open-doc (_workspace (&eslint:OpenESLintDocParams :url)) 331 "Open documentation." 332 (browse-url url)) 333 334 (defun lsp-eslint-apply-all-fixes () 335 "Apply all autofixes in the current buffer." 336 (interactive) 337 (lsp-send-execute-command "eslint.applyAllFixes" (vector (lsp--versioned-text-document-identifier)))) 338 339 ;; XXX: replace with `lsp-make-interactive-code-action' macro 340 ;; (lsp-make-interactive-code-action eslint-fix-all "source.fixAll.eslint") 341 342 (defun lsp-eslint-fix-all () 343 "Perform the source.fixAll.eslint code action, if available." 344 (interactive) 345 (condition-case nil 346 (lsp-execute-code-action-by-kind "source.fixAll.eslint") 347 (lsp-no-code-actions 348 (when (called-interactively-p 'any) 349 (lsp--info "source.fixAll.eslint action not available"))))) 350 351 (defun lsp-eslint-server-command () 352 (if (lsp-eslint-server-exists? lsp-eslint-server-command) 353 lsp-eslint-server-command 354 `(,lsp-eslint-node ,(f-join lsp-eslint-unzipped-path 355 "extension/server/out/eslintServer.js") 356 "--stdio"))) 357 358 (defun lsp-eslint-server-exists? (eslint-server-command) 359 (let* ((command-name (f-base (f-filename (cl-first eslint-server-command)))) 360 (first-argument (cl-second eslint-server-command)) 361 (first-argument-exist (and first-argument (file-exists-p first-argument)))) 362 (if (equal command-name lsp-eslint-node) 363 first-argument-exist 364 (executable-find (cl-first eslint-server-command))))) 365 366 (defvar lsp-eslint--stored-libraries (ht) 367 "Hash table defining if a given path to an ESLint library is allowed to run. 368 If the value for a key is 4, it will be allowed. If it is 1, it will not. If a 369 value does not exist for the key, or the value is nil, the user will be prompted 370 to allow or deny it.") 371 372 (when (and (file-exists-p lsp-eslint-library-choices-file) 373 lsp-eslint-save-library-choices) 374 (setq lsp-eslint--stored-libraries (lsp--read-from-file lsp-eslint-library-choices-file))) 375 376 (lsp-defun lsp-eslint--confirm-local (_workspace (&eslint:ConfirmExecutionParams :library-path) callback) 377 (if-let ((option-alist '(("Always" 4 . t) 378 ("Yes" 4 . nil) 379 ("No" 1 . nil) 380 ("Never" 1 . t))) 381 (remembered-answer (gethash library-path lsp-eslint--stored-libraries))) 382 (funcall callback remembered-answer) 383 (lsp-ask-question 384 (format 385 "Allow lsp-mode to execute %s? Note: The latest versions of the ESLint language server no longer create this prompt." 386 library-path) 387 (mapcar 'car option-alist) 388 (lambda (response) 389 (let ((option (cdr (assoc response option-alist)))) 390 (when (cdr option) 391 (puthash library-path (car option) lsp-eslint--stored-libraries) 392 (when lsp-eslint-save-library-choices 393 (lsp--persist lsp-eslint-library-choices-file lsp-eslint--stored-libraries))) 394 (funcall callback (car option))))))) 395 396 (defun lsp-eslint--probe-failed (_workspace _message) 397 "Called when the server detects a misconfiguration in ESLint." 398 (lsp--error "ESLint is not configured correctly. Please ensure your eslintrc is set up for the languages you are using.")) 399 400 (lsp-register-client 401 (make-lsp-client 402 :new-connection 403 (lsp-stdio-connection 404 (lambda () (lsp-eslint-server-command)) 405 (lambda () (lsp-eslint-server-exists? (lsp-eslint-server-command)))) 406 :activation-fn (lambda (filename &optional _) 407 (when lsp-eslint-enable 408 (or (string-match-p (rx (one-or-more anything) "." 409 (or "ts" "js" "jsx" "tsx" "html" "vue" "svelte")eos) 410 filename) 411 (and (derived-mode-p 'js-mode 'js2-mode 'typescript-mode 'typescript-ts-mode 'html-mode 'svelte-mode) 412 (not (string-match-p "\\.json\\'" filename)))))) 413 :priority -1 414 :completion-in-comments? t 415 :add-on? t 416 :multi-root t 417 :notification-handlers (ht ("eslint/status" #'lsp-eslint-status-handler)) 418 :request-handlers (ht ("workspace/configuration" #'lsp-eslint--configuration) 419 ("eslint/openDoc" #'lsp-eslint--open-doc) 420 ("eslint/probeFailed" #'lsp-eslint--probe-failed)) 421 :async-request-handlers (ht ("eslint/confirmESLintExecution" #'lsp-eslint--confirm-local)) 422 :server-id 'eslint 423 :initialized-fn (lambda (workspace) 424 (with-lsp-workspace workspace 425 (lsp--server-register-capability 426 (lsp-make-registration 427 :id "random-id" 428 :method "workspace/didChangeWatchedFiles" 429 :register-options? (lsp-make-did-change-watched-files-registration-options 430 :watchers 431 `[,(lsp-make-file-system-watcher 432 :glob-pattern "**/.eslintr{c.js,c.yaml,c.yml,c,c.json}") 433 ,(lsp-make-file-system-watcher 434 :glob-pattern "**/.eslintignore") 435 ,(lsp-make-file-system-watcher 436 :glob-pattern "**/package.json")]))))) 437 :download-server-fn (lambda (_client callback error-callback _update?) 438 (let ((tmp-zip (make-temp-file "ext" nil ".zip"))) 439 (delete-file tmp-zip) 440 (lsp-download-install 441 (lambda (&rest _) 442 (condition-case err 443 (progn 444 (lsp-unzip tmp-zip lsp-eslint-unzipped-path) 445 (funcall callback)) 446 (error (funcall error-callback err)))) 447 error-callback 448 :url lsp-eslint-download-url 449 :store-path tmp-zip))))) 450 451 (lsp-consistency-check lsp-eslint) 452 453 (provide 'lsp-eslint) 454 ;;; lsp-eslint.el ends here