lsp-eslint.el (20525B)
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. 137 Can be a pattern, an absolute path, a path relative to the workspace, 138 or a supported mode such as \"auto\" or \"location\". 139 Examples: 140 - \"/home/user/abc/\" 141 - \"abc/\" 142 - (directory \"abc\") which is equivalent to \"abc\" above 143 - (pattern \"abc/*\") 144 - (mode \"auto\") 145 - (mode \"location\") 146 Note that the home directory reference ~/ is not currently supported, use 147 /home/[user]/ instead." 148 :type '(lsp-repeatable-vector (choice string (plist mode string))) 149 :package-version '(lsp-mode . "6.3")) 150 151 (defcustom lsp-eslint-validate '("svelte") 152 "An array of language ids which should always be validated by ESLint." 153 :type '(repeat string) 154 :package-version '(lsp-mode . "8.0.0")) 155 156 (defcustom lsp-eslint-provide-lint-task nil 157 "Controls whether a task for linting the whole workspace will be available." 158 :type 'boolean 159 :package-version '(lsp-mode . "6.3")) 160 161 (defcustom lsp-eslint-lint-task-enable nil 162 "Controls whether a task for linting the whole workspace will be available." 163 :type 'boolean 164 :package-version '(lsp-mode . "6.3")) 165 166 (defcustom lsp-eslint-lint-task-options "." 167 "Command line options applied when running the task for linting the whole 168 workspace (see https://eslint.org/docs/user-guide/command-line-interface)." 169 :type 'string 170 :package-version '(lsp-mode . "6.3")) 171 172 (defcustom lsp-eslint-runtime nil 173 "The location of the node binary to run ESLint under." 174 :type '(repeat string) 175 :package-version '(lsp-mode . "6.3")) 176 177 (defcustom lsp-eslint-code-action-disable-rule-comment t 178 "Controls whether code actions to add a rule-disabling comment should be shown." 179 :type 'bool 180 :package-version '(lsp-mode . "6.3")) 181 182 (defcustom lsp-eslint-code-action-disable-rule-comment-location "separateLine" 183 "Controls where the disable rule code action places comments. 184 185 Accepts the following values: 186 - \"separateLine\": Add the comment above the line to be disabled (default). 187 - \"sameLine\": Add the comment on the same line that will be disabled." 188 :type '(choice 189 (const "separateLine") 190 (const "sameLine")) 191 :package-version '(lsp-mode . "8.0.0")) 192 193 (defcustom lsp-eslint-code-action-show-documentation t 194 "Controls whether code actions to show documentation for an ESLint rule should 195 be shown." 196 :type 'bool 197 :package-version '(lsp-mode . "8.0.0")) 198 199 (defcustom lsp-eslint-warn-on-ignored-files nil 200 "Controls whether a warning should be emitted when a file is ignored." 201 :type 'bool 202 :package-version '(lsp-mode . "8.0.0")) 203 204 (defcustom lsp-eslint-rules-customizations [] 205 "Controls severity overrides for ESLint rules. 206 207 The value is a vector of alists, with each alist containing the following keys: 208 - rule - The rule to match. Can match wildcards with *, or be prefixed with ! 209 to negate the match. 210 - severity - The severity to report this rule as. Can be one of the following: 211 - \"off\": Disable the rule. 212 - \"info\": Report as informational. 213 - \"warn\": Report as a warning. 214 - \"error\": Report as an error. 215 - \"upgrade\": Increase by 1 severity level (eg. warning -> error). 216 - \"downgrade\": Decrease by 1 severity level (eg. warning -> info). 217 - \"default\": Report as the same severity specified in the ESLint config." 218 :type '(lsp-repeatable-vector 219 (alist :options ((rule string) 220 (severity (choice 221 (const "off") 222 (const "info") 223 (const "warn") 224 (const "error") 225 (const "upgrade") 226 (const "downgrade") 227 (const "default")))))) 228 :package-version '(lsp-mode . "8.0.0")) 229 230 (defcustom lsp-eslint-experimental-incremental-sync t 231 "Controls whether the new incremental text document synchronization should 232 be used." 233 :type 'boolean 234 :package-version '(lsp-mode . "6.3")) 235 236 (defcustom lsp-eslint-save-library-choices t 237 "Controls whether to remember choices made to permit or deny ESLint libraries 238 from running." 239 :type 'boolean 240 :package-version '(lsp-mode . "8.0.0")) 241 242 (defcustom lsp-eslint-library-choices-file (expand-file-name (locate-user-emacs-file ".lsp-eslint-choices")) 243 "The file where choices to permit or deny ESLint libraries from running is 244 stored." 245 :type 'string 246 :package-version '(lsp-mode . "8.0.0")) 247 248 (defun lsp--find-eslint () 249 (or 250 (when-let* ((workspace-folder (lsp-find-session-folder (lsp-session) default-directory))) 251 (let ((eslint-local-path (f-join workspace-folder "node_modules" ".bin" 252 (if (eq system-type 'windows-nt) "eslint.cmd" "eslint")))) 253 (when (f-exists? eslint-local-path) 254 eslint-local-path))) 255 "eslint")) 256 257 (defun lsp-eslint-create-default-configuration () 258 "Create default ESLint configuration." 259 (interactive) 260 (unless (lsp-session-folders (lsp-session)) 261 (user-error "There are no workspace folders")) 262 (pcase (->> (lsp-session) 263 lsp-session-folders 264 (-filter (lambda (dir) 265 (-none? 266 (lambda (file) (f-exists? (f-join dir file))) 267 '(".eslintrc.js" ".eslintrc.yaml" ".eslintrc.yml" ".eslintrc" ".eslintrc.json"))))) 268 (`nil (user-error "All workspace folders contain ESLint configuration")) 269 (folders (let ((default-directory (completing-read "Select project folder: " folders nil t))) 270 (async-shell-command (format "%s --init" (lsp--find-eslint))))))) 271 272 (lsp-defun lsp-eslint-status-handler (workspace (&eslint:StatusParams :state)) 273 (setf (lsp--workspace-status-string workspace) 274 (propertize "ESLint" 275 'face (cond 276 ((eq state lsp-eslint/status-error) 'error) 277 ((eq state lsp-eslint/status-warn) 'warn) 278 (t 'success))))) 279 280 (lsp-defun lsp-eslint--configuration (_workspace (&ConfigurationParams :items)) 281 (->> items 282 (seq-map (-lambda ((&ConfigurationItem :scope-uri?)) 283 (-when-let* ((file (lsp--uri-to-path scope-uri?)) 284 (buffer (find-buffer-visiting file)) 285 (workspace-folder (lsp-find-session-folder (lsp-session) file))) 286 (with-current-buffer buffer 287 (list :validate (if (member (lsp-buffer-language) lsp-eslint-validate) "on" "probe") 288 :packageManager lsp-eslint-package-manager 289 :codeAction (list 290 :disableRuleComment (list 291 :enable (lsp-json-bool lsp-eslint-code-action-disable-rule-comment) 292 :location lsp-eslint-code-action-disable-rule-comment-location) 293 :showDocumentation (list 294 :enable (lsp-json-bool lsp-eslint-code-action-show-documentation))) 295 :codeActionOnSave (list :enable (lsp-json-bool lsp-eslint-auto-fix-on-save) 296 :mode lsp-eslint-fix-all-problem-type) 297 :format (lsp-json-bool lsp-eslint-format) 298 :quiet (lsp-json-bool lsp-eslint-quiet) 299 :onIgnoredFiles (if lsp-eslint-warn-on-ignored-files "warn" "off") 300 :options (or lsp-eslint-options (ht)) 301 :experimental (or lsp-eslint-experimental (ht)) 302 :problems (or lsp-eslint-config-problems (ht)) 303 :timeBudget (or lsp-eslint-time-budget (ht)) 304 :rulesCustomizations lsp-eslint-rules-customizations 305 :run lsp-eslint-run 306 :nodePath lsp-eslint-node-path 307 :workingDirectory (lsp-eslint--working-directory workspace-folder file) 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 (cond 319 ((not (listp dir)) 320 (if (f-absolute? dir) dir (f-join workspace dir))) 321 ((plist-member dir 'pattern) 322 (setq dir (plist-get dir 'pattern)) 323 (when (not (f-absolute? dir)) 324 (setq dir (f-join workspace dir))) 325 (f-glob dir)) 326 ((plist-member dir 'mode) 327 ;; we don't want this setting to get flattened by -flatten 328 `(mode . ,(plist-get dir 'mode))))) 329 (append lsp-eslint-working-directories nil))) 330 (working-directory (-first (lambda (dir) 331 (if (stringp dir) 332 (f-ancestor-of-p dir current-file) 333 dir)) 334 (-flatten directories)))) 335 (cond 336 ((consp working-directory) `(:mode ,(cdr working-directory))) 337 ((stringp working-directory) (list :directory working-directory :!cwd :json-false))))) 338 339 (lsp-defun lsp-eslint--open-doc (_workspace (&eslint:OpenESLintDocParams :url)) 340 "Open documentation." 341 (browse-url url)) 342 343 (defun lsp-eslint-apply-all-fixes () 344 "Apply all autofixes in the current buffer." 345 (interactive) 346 (lsp-send-execute-command "eslint.applyAllFixes" (vector (lsp--versioned-text-document-identifier)))) 347 348 ;; XXX: replace with `lsp-make-interactive-code-action' macro 349 ;; (lsp-make-interactive-code-action eslint-fix-all "source.fixAll.eslint") 350 351 (defun lsp-eslint-fix-all () 352 "Perform the source.fixAll.eslint code action, if available." 353 (interactive) 354 (condition-case nil 355 (lsp-execute-code-action-by-kind "source.fixAll.eslint") 356 (lsp-no-code-actions 357 (when (called-interactively-p 'any) 358 (lsp--info "source.fixAll.eslint action not available"))))) 359 360 (defun lsp-eslint-server-command () 361 (if (lsp-eslint-server-exists? lsp-eslint-server-command) 362 lsp-eslint-server-command 363 `(,lsp-eslint-node ,(f-join lsp-eslint-unzipped-path 364 "extension/server/out/eslintServer.js") 365 "--stdio"))) 366 367 (defun lsp-eslint-server-exists? (eslint-server-command) 368 (let* ((command-name (f-base (f-filename (cl-first eslint-server-command)))) 369 (first-argument (cl-second eslint-server-command)) 370 (first-argument-exist (and first-argument (file-exists-p first-argument)))) 371 (if (equal command-name lsp-eslint-node) 372 first-argument-exist 373 (executable-find (cl-first eslint-server-command))))) 374 375 (defvar lsp-eslint--stored-libraries (ht) 376 "Hash table defining if a given path to an ESLint library is allowed to run. 377 If the value for a key is 4, it will be allowed. If it is 1, it will not. If a 378 value does not exist for the key, or the value is nil, the user will be prompted 379 to allow or deny it.") 380 381 (when (and (file-exists-p lsp-eslint-library-choices-file) 382 lsp-eslint-save-library-choices) 383 (setq lsp-eslint--stored-libraries (lsp--read-from-file lsp-eslint-library-choices-file))) 384 385 (lsp-defun lsp-eslint--confirm-local (_workspace (&eslint:ConfirmExecutionParams :library-path) callback) 386 (if-let* ((option-alist '(("Always" 4 . t) 387 ("Yes" 4 . nil) 388 ("No" 1 . nil) 389 ("Never" 1 . t))) 390 (remembered-answer (gethash library-path lsp-eslint--stored-libraries))) 391 (funcall callback remembered-answer) 392 (lsp-ask-question 393 (format 394 "Allow lsp-mode to execute %s? Note: The latest versions of the ESLint language server no longer create this prompt." 395 library-path) 396 (mapcar 'car option-alist) 397 (lambda (response) 398 (let ((option (cdr (assoc response option-alist)))) 399 (when (cdr option) 400 (puthash library-path (car option) lsp-eslint--stored-libraries) 401 (when lsp-eslint-save-library-choices 402 (lsp--persist lsp-eslint-library-choices-file lsp-eslint--stored-libraries))) 403 (funcall callback (car option))))))) 404 405 (defun lsp-eslint--probe-failed (_workspace _message) 406 "Called when the server detects a misconfiguration in ESLint." 407 (lsp--error "ESLint is not configured correctly. Please ensure your eslintrc is set up for the languages you are using.")) 408 409 (lsp-register-client 410 (make-lsp-client 411 :new-connection 412 (lsp-stdio-connection 413 (lambda () (lsp-eslint-server-command)) 414 (lambda () (lsp-eslint-server-exists? (lsp-eslint-server-command)))) 415 :activation-fn (lambda (filename &optional _) 416 (when lsp-eslint-enable 417 (or (string-match-p (rx (one-or-more anything) "." 418 (or "ts" "js" "jsx" "tsx" "html" "vue" "svelte")eos) 419 filename) 420 (and (derived-mode-p 'js-mode 'js2-mode 'typescript-mode 'typescript-ts-mode 'html-mode 'svelte-mode) 421 (not (string-match-p "\\.json\\'" filename)))))) 422 :priority -1 423 :completion-in-comments? t 424 :add-on? t 425 :multi-root t 426 :notification-handlers (ht ("eslint/status" #'lsp-eslint-status-handler)) 427 :request-handlers (ht ("workspace/configuration" #'lsp-eslint--configuration) 428 ("eslint/openDoc" #'lsp-eslint--open-doc) 429 ("eslint/probeFailed" #'lsp-eslint--probe-failed)) 430 :async-request-handlers (ht ("eslint/confirmESLintExecution" #'lsp-eslint--confirm-local)) 431 :server-id 'eslint 432 :initialized-fn (lambda (workspace) 433 (with-lsp-workspace workspace 434 (lsp--server-register-capability 435 (lsp-make-registration 436 :id "random-id" 437 :method "workspace/didChangeWatchedFiles" 438 :register-options? (lsp-make-did-change-watched-files-registration-options 439 :watchers 440 `[,(lsp-make-file-system-watcher 441 :glob-pattern "**/.eslintr{c.js,c.yaml,c.yml,c,c.json}") 442 ,(lsp-make-file-system-watcher 443 :glob-pattern "**/.eslintignore") 444 ,(lsp-make-file-system-watcher 445 :glob-pattern "**/package.json")]))))) 446 :download-server-fn (lambda (_client callback error-callback _update?) 447 (let ((tmp-zip (make-temp-file "ext" nil ".zip"))) 448 (delete-file tmp-zip) 449 (lsp-download-install 450 (lambda (&rest _) 451 (condition-case err 452 (progn 453 (lsp-unzip tmp-zip lsp-eslint-unzipped-path) 454 (funcall callback)) 455 (error (funcall error-callback err)))) 456 error-callback 457 :url lsp-eslint-download-url 458 :store-path tmp-zip))))) 459 460 (lsp-consistency-check lsp-eslint) 461 462 (provide 'lsp-eslint) 463 ;;; lsp-eslint.el ends here