455 lines
20 KiB
EmacsLisp
455 lines
20 KiB
EmacsLisp
;;; lsp-eslint.el --- lsp-mode eslint integration -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2019 Ivan Yonchovski
|
|
|
|
;; Author: Ivan Yonchovski <yyoncho@gmail.com>
|
|
;; Keywords: languages
|
|
|
|
;; 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 <https://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;;
|
|
|
|
;;; Code:
|
|
|
|
(require 'lsp-protocol)
|
|
(require 'lsp-mode)
|
|
|
|
(defconst lsp-eslint/status-ok 1)
|
|
(defconst lsp-eslint/status-warn 2)
|
|
(defconst lsp-eslint/status-error 3)
|
|
|
|
(defgroup lsp-eslint nil
|
|
"ESLint language server group."
|
|
:group 'lsp-mode
|
|
:link '(url-link "https://github.com/microsoft/vscode-eslint"))
|
|
|
|
(defcustom lsp-eslint-unzipped-path (f-join lsp-server-install-dir "eslint/unzipped")
|
|
"The path to the file in which `eslint' will be stored."
|
|
:type 'file
|
|
:group 'lsp-eslint
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defcustom lsp-eslint-download-url "https://github.com/emacs-lsp/lsp-server-binaries/blob/master/dbaeumer.vscode-eslint-2.2.2.vsix?raw=true"
|
|
"ESLint language server download url."
|
|
:type 'string
|
|
:group 'lsp-eslint
|
|
:package-version '(lsp-mode . "8.0.1"))
|
|
|
|
(defcustom lsp-eslint-server-command `("node"
|
|
"~/server/out/eslintServer.js"
|
|
"--stdio")
|
|
"Command to start ESLint server."
|
|
:risky t
|
|
:type '(repeat string)
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-enable t
|
|
"Controls whether ESLint is enabled for JavaScript files or not."
|
|
:type 'boolean
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-package-manager "npm"
|
|
"The package manager you use to install node modules."
|
|
:type '(choice (const :tag "npm" "npm")
|
|
(const :tag "yarn" "yarn")
|
|
(const :tag "pnpm" "pnpm")
|
|
(string :tag "other"))
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-format t
|
|
"Whether to perform format."
|
|
:type 'boolean
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-node-path nil
|
|
"A path added to NODE_PATH when resolving the `eslint' module."
|
|
:type '(repeat string)
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-node "node"
|
|
"Path to Node.js."
|
|
:type 'file
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defcustom lsp-eslint-options nil
|
|
"The ESLint options object to provide args normally passed to
|
|
`eslint' when executed from a command line (see
|
|
https://eslint.org/docs/latest/integrate/nodejs-api)."
|
|
:type 'alist)
|
|
|
|
(defcustom lsp-eslint-experimental nil
|
|
"The eslint experimental configuration."
|
|
:type 'alist)
|
|
|
|
(defcustom lsp-eslint-config-problems nil
|
|
"The eslint problems configuration."
|
|
:type 'alist)
|
|
|
|
(defcustom lsp-eslint-time-budget nil
|
|
"The eslint config to inform you of slow validation times and
|
|
long ESLint runs when computing code fixes during save."
|
|
:type 'alist)
|
|
|
|
(defcustom lsp-eslint-trace-server "off"
|
|
"Traces the communication between VSCode and the ESLint linter service."
|
|
:type 'string)
|
|
|
|
(defcustom lsp-eslint-run "onType"
|
|
"Run the linter on save (onSave) or on type (onType)"
|
|
:type '(choice (const :tag "onSave" "onSave")
|
|
(const :tag "onType" "onType"))
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-auto-fix-on-save nil
|
|
"Turns auto fix on save on or off."
|
|
:type 'boolean
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-fix-all-problem-type "all"
|
|
"Determines which problems are fixed when running the
|
|
source.fixAll code action."
|
|
:type '(choice
|
|
(const "all")
|
|
(const "problems")
|
|
string)
|
|
:package-version '(lsp-mode . "7.0.1"))
|
|
|
|
(defcustom lsp-eslint-quiet nil
|
|
"Turns on quiet mode, which ignores warnings."
|
|
:type 'boolean
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-working-directories []
|
|
"A vector of working directory names to use. Can be a pattern, an absolute path
|
|
or a path relative to the workspace. Examples:
|
|
- \"/home/user/abc/\"
|
|
- \"abc/\"
|
|
- (directory \"abc\") which is equivalent to \"abc\" above
|
|
- (pattern \"abc/*\")
|
|
Note that the home directory reference ~/ is not currently supported, use
|
|
/home/[user]/ instead."
|
|
:type 'lsp-string-vector
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-validate '("svelte")
|
|
"An array of language ids which should always be validated by ESLint."
|
|
:type '(repeat string)
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defcustom lsp-eslint-provide-lint-task nil
|
|
"Controls whether a task for linting the whole workspace will be available."
|
|
:type 'boolean
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-lint-task-enable nil
|
|
"Controls whether a task for linting the whole workspace will be available."
|
|
:type 'boolean
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-lint-task-options "."
|
|
"Command line options applied when running the task for linting the whole
|
|
workspace (see https://eslint.org/docs/user-guide/command-line-interface)."
|
|
:type 'string
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-runtime nil
|
|
"The location of the node binary to run ESLint under."
|
|
:type '(repeat string)
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-code-action-disable-rule-comment t
|
|
"Controls whether code actions to add a rule-disabling comment should be shown."
|
|
:type 'bool
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-code-action-disable-rule-comment-location "separateLine"
|
|
"Controls where the disable rule code action places comments.
|
|
|
|
Accepts the following values:
|
|
- \"separateLine\": Add the comment above the line to be disabled (default).
|
|
- \"sameLine\": Add the comment on the same line that will be disabled."
|
|
:type '(choice
|
|
(const "separateLine")
|
|
(const "sameLine"))
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defcustom lsp-eslint-code-action-show-documentation t
|
|
"Controls whether code actions to show documentation for an ESLint rule should
|
|
be shown."
|
|
:type 'bool
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defcustom lsp-eslint-warn-on-ignored-files nil
|
|
"Controls whether a warning should be emitted when a file is ignored."
|
|
:type 'bool
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defcustom lsp-eslint-rules-customizations []
|
|
"Controls severity overrides for ESLint rules.
|
|
|
|
The value is a vector of alists, with each alist containing the following keys:
|
|
- rule - The rule to match. Can match wildcards with *, or be prefixed with !
|
|
to negate the match.
|
|
- severity - The severity to report this rule as. Can be one of the following:
|
|
- \"off\": Disable the rule.
|
|
- \"info\": Report as informational.
|
|
- \"warn\": Report as a warning.
|
|
- \"error\": Report as an error.
|
|
- \"upgrade\": Increase by 1 severity level (eg. warning -> error).
|
|
- \"downgrade\": Decrease by 1 severity level (eg. warning -> info).
|
|
- \"default\": Report as the same severity specified in the ESLint config."
|
|
:type '(lsp-repeatable-vector
|
|
(alist :options ((rule string)
|
|
(severity (choice
|
|
(const "off")
|
|
(const "info")
|
|
(const "warn")
|
|
(const "error")
|
|
(const "upgrade")
|
|
(const "downgrade")
|
|
(const "default"))))))
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defcustom lsp-eslint-experimental-incremental-sync t
|
|
"Controls whether the new incremental text document synchronization should
|
|
be used."
|
|
:type 'boolean
|
|
:package-version '(lsp-mode . "6.3"))
|
|
|
|
(defcustom lsp-eslint-save-library-choices t
|
|
"Controls whether to remember choices made to permit or deny ESLint libraries
|
|
from running."
|
|
:type 'boolean
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defcustom lsp-eslint-library-choices-file (expand-file-name (locate-user-emacs-file ".lsp-eslint-choices"))
|
|
"The file where choices to permit or deny ESLint libraries from running is
|
|
stored."
|
|
:type 'string
|
|
:package-version '(lsp-mode . "8.0.0"))
|
|
|
|
(defun lsp--find-eslint ()
|
|
(or
|
|
(when-let ((workspace-folder (lsp-find-session-folder (lsp-session) default-directory)))
|
|
(let ((eslint-local-path (f-join workspace-folder "node_modules" ".bin"
|
|
(if (eq system-type 'windows-nt) "eslint.cmd" "eslint"))))
|
|
(when (f-exists? eslint-local-path)
|
|
eslint-local-path)))
|
|
"eslint"))
|
|
|
|
(defun lsp-eslint-create-default-configuration ()
|
|
"Create default ESLint configuration."
|
|
(interactive)
|
|
(unless (lsp-session-folders (lsp-session))
|
|
(user-error "There are no workspace folders"))
|
|
(pcase (->> (lsp-session)
|
|
lsp-session-folders
|
|
(-filter (lambda (dir)
|
|
(-none?
|
|
(lambda (file) (f-exists? (f-join dir file)))
|
|
'(".eslintrc.js" ".eslintrc.yaml" ".eslintrc.yml" ".eslintrc" ".eslintrc.json")))))
|
|
(`nil (user-error "All workspace folders contain ESLint configuration"))
|
|
(folders (let ((default-directory (completing-read "Select project folder: " folders nil t)))
|
|
(async-shell-command (format "%s --init" (lsp--find-eslint)))))))
|
|
|
|
(lsp-defun lsp-eslint-status-handler (workspace (&eslint:StatusParams :state))
|
|
(setf (lsp--workspace-status-string workspace)
|
|
(propertize "ESLint"
|
|
'face (cond
|
|
((eq state lsp-eslint/status-error) 'error)
|
|
((eq state lsp-eslint/status-warn) 'warn)
|
|
(t 'success)))))
|
|
|
|
(lsp-defun lsp-eslint--configuration (_workspace (&ConfigurationParams :items))
|
|
(->> items
|
|
(seq-map (-lambda ((&ConfigurationItem :scope-uri?))
|
|
(-when-let* ((file (lsp--uri-to-path scope-uri?))
|
|
(buffer (find-buffer-visiting file))
|
|
(workspace-folder (lsp-find-session-folder (lsp-session) file)))
|
|
(with-current-buffer buffer
|
|
(let ((working-directory (lsp-eslint--working-directory workspace-folder file)))
|
|
(list :validate (if (member (lsp-buffer-language) lsp-eslint-validate) "on" "probe")
|
|
:packageManager lsp-eslint-package-manager
|
|
:codeAction (list
|
|
:disableRuleComment (list
|
|
:enable (lsp-json-bool lsp-eslint-code-action-disable-rule-comment)
|
|
:location lsp-eslint-code-action-disable-rule-comment-location)
|
|
:showDocumentation (list
|
|
:enable (lsp-json-bool lsp-eslint-code-action-show-documentation)))
|
|
:codeActionOnSave (list :enable (lsp-json-bool lsp-eslint-auto-fix-on-save)
|
|
:mode lsp-eslint-fix-all-problem-type)
|
|
:format (lsp-json-bool lsp-eslint-format)
|
|
:quiet (lsp-json-bool lsp-eslint-quiet)
|
|
:onIgnoredFiles (if lsp-eslint-warn-on-ignored-files "warn" "off")
|
|
:options (or lsp-eslint-options (ht))
|
|
:experimental (or lsp-eslint-experimental (ht))
|
|
:problems (or lsp-eslint-config-problems (ht))
|
|
:timeBudget (or lsp-eslint-time-budget (ht))
|
|
:rulesCustomizations lsp-eslint-rules-customizations
|
|
:run lsp-eslint-run
|
|
:nodePath lsp-eslint-node-path
|
|
:workingDirectory (when working-directory
|
|
(list
|
|
:directory working-directory
|
|
:!cwd :json-false))
|
|
:workspaceFolder (list :uri (lsp--path-to-uri workspace-folder)
|
|
:name (f-filename workspace-folder))))))))
|
|
(apply #'vector)))
|
|
|
|
(defun lsp-eslint--working-directory (workspace current-file)
|
|
"Find the first directory in the parameter config.workingDirectories which
|
|
contains the current file"
|
|
(let ((directories (-map (lambda (dir)
|
|
(when (and (listp dir) (plist-member dir 'directory))
|
|
(setq dir (plist-get dir 'directory)))
|
|
(if (and (listp dir) (plist-member dir 'pattern))
|
|
(progn
|
|
(setq dir (plist-get dir 'pattern))
|
|
(when (not (f-absolute? dir))
|
|
(setq dir (f-join workspace dir)))
|
|
(f-glob dir))
|
|
(if (f-absolute? dir)
|
|
dir
|
|
(f-join workspace dir))))
|
|
(append lsp-eslint-working-directories nil))))
|
|
(-first (lambda (dir) (f-ancestor-of-p dir current-file)) (-flatten directories))))
|
|
|
|
(lsp-defun lsp-eslint--open-doc (_workspace (&eslint:OpenESLintDocParams :url))
|
|
"Open documentation."
|
|
(browse-url url))
|
|
|
|
(defun lsp-eslint-apply-all-fixes ()
|
|
"Apply all autofixes in the current buffer."
|
|
(interactive)
|
|
(lsp-send-execute-command "eslint.applyAllFixes" (vector (lsp--versioned-text-document-identifier))))
|
|
|
|
;; XXX: replace with `lsp-make-interactive-code-action' macro
|
|
;; (lsp-make-interactive-code-action eslint-fix-all "source.fixAll.eslint")
|
|
|
|
(defun lsp-eslint-fix-all ()
|
|
"Perform the source.fixAll.eslint code action, if available."
|
|
(interactive)
|
|
(condition-case nil
|
|
(lsp-execute-code-action-by-kind "source.fixAll.eslint")
|
|
(lsp-no-code-actions
|
|
(when (called-interactively-p 'any)
|
|
(lsp--info "source.fixAll.eslint action not available")))))
|
|
|
|
(defun lsp-eslint-server-command ()
|
|
(if (lsp-eslint-server-exists? lsp-eslint-server-command)
|
|
lsp-eslint-server-command
|
|
`(,lsp-eslint-node ,(f-join lsp-eslint-unzipped-path
|
|
"extension/server/out/eslintServer.js")
|
|
"--stdio")))
|
|
|
|
(defun lsp-eslint-server-exists? (eslint-server-command)
|
|
(let* ((command-name (f-base (f-filename (cl-first eslint-server-command))))
|
|
(first-argument (cl-second eslint-server-command))
|
|
(first-argument-exist (and first-argument (file-exists-p first-argument))))
|
|
(if (equal command-name lsp-eslint-node)
|
|
first-argument-exist
|
|
(executable-find (cl-first eslint-server-command)))))
|
|
|
|
(defvar lsp-eslint--stored-libraries (ht)
|
|
"Hash table defining if a given path to an ESLint library is allowed to run.
|
|
If the value for a key is 4, it will be allowed. If it is 1, it will not. If a
|
|
value does not exist for the key, or the value is nil, the user will be prompted
|
|
to allow or deny it.")
|
|
|
|
(when (and (file-exists-p lsp-eslint-library-choices-file)
|
|
lsp-eslint-save-library-choices)
|
|
(setq lsp-eslint--stored-libraries (lsp--read-from-file lsp-eslint-library-choices-file)))
|
|
|
|
(lsp-defun lsp-eslint--confirm-local (_workspace (&eslint:ConfirmExecutionParams :library-path) callback)
|
|
(if-let ((option-alist '(("Always" 4 . t)
|
|
("Yes" 4 . nil)
|
|
("No" 1 . nil)
|
|
("Never" 1 . t)))
|
|
(remembered-answer (gethash library-path lsp-eslint--stored-libraries)))
|
|
(funcall callback remembered-answer)
|
|
(lsp-ask-question
|
|
(format
|
|
"Allow lsp-mode to execute %s? Note: The latest versions of the ESLint language server no longer create this prompt."
|
|
library-path)
|
|
(mapcar 'car option-alist)
|
|
(lambda (response)
|
|
(let ((option (cdr (assoc response option-alist))))
|
|
(when (cdr option)
|
|
(puthash library-path (car option) lsp-eslint--stored-libraries)
|
|
(when lsp-eslint-save-library-choices
|
|
(lsp--persist lsp-eslint-library-choices-file lsp-eslint--stored-libraries)))
|
|
(funcall callback (car option)))))))
|
|
|
|
(defun lsp-eslint--probe-failed (_workspace _message)
|
|
"Called when the server detects a misconfiguration in ESLint."
|
|
(lsp--error "ESLint is not configured correctly. Please ensure your eslintrc is set up for the languages you are using."))
|
|
|
|
(lsp-register-client
|
|
(make-lsp-client
|
|
:new-connection
|
|
(lsp-stdio-connection
|
|
(lambda () (lsp-eslint-server-command))
|
|
(lambda () (lsp-eslint-server-exists? (lsp-eslint-server-command))))
|
|
:activation-fn (lambda (filename &optional _)
|
|
(when lsp-eslint-enable
|
|
(or (string-match-p (rx (one-or-more anything) "."
|
|
(or "ts" "js" "jsx" "tsx" "html" "vue" "svelte")eos)
|
|
filename)
|
|
(and (derived-mode-p 'js-mode 'js2-mode 'typescript-mode 'typescript-ts-mode 'html-mode 'svelte-mode)
|
|
(not (string-match-p "\\.json\\'" filename))))))
|
|
:priority -1
|
|
:completion-in-comments? t
|
|
:add-on? t
|
|
:multi-root t
|
|
:notification-handlers (ht ("eslint/status" #'lsp-eslint-status-handler))
|
|
:request-handlers (ht ("workspace/configuration" #'lsp-eslint--configuration)
|
|
("eslint/openDoc" #'lsp-eslint--open-doc)
|
|
("eslint/probeFailed" #'lsp-eslint--probe-failed))
|
|
:async-request-handlers (ht ("eslint/confirmESLintExecution" #'lsp-eslint--confirm-local))
|
|
:server-id 'eslint
|
|
:initialized-fn (lambda (workspace)
|
|
(with-lsp-workspace workspace
|
|
(lsp--server-register-capability
|
|
(lsp-make-registration
|
|
:id "random-id"
|
|
:method "workspace/didChangeWatchedFiles"
|
|
:register-options? (lsp-make-did-change-watched-files-registration-options
|
|
:watchers
|
|
`[,(lsp-make-file-system-watcher
|
|
:glob-pattern "**/.eslintr{c.js,c.yaml,c.yml,c,c.json}")
|
|
,(lsp-make-file-system-watcher
|
|
:glob-pattern "**/.eslintignore")
|
|
,(lsp-make-file-system-watcher
|
|
:glob-pattern "**/package.json")])))))
|
|
:download-server-fn (lambda (_client callback error-callback _update?)
|
|
(let ((tmp-zip (make-temp-file "ext" nil ".zip")))
|
|
(delete-file tmp-zip)
|
|
(lsp-download-install
|
|
(lambda (&rest _)
|
|
(condition-case err
|
|
(progn
|
|
(lsp-unzip tmp-zip lsp-eslint-unzipped-path)
|
|
(funcall callback))
|
|
(error (funcall error-callback err))))
|
|
error-callback
|
|
:url lsp-eslint-download-url
|
|
:store-path tmp-zip)))))
|
|
|
|
(lsp-consistency-check lsp-eslint)
|
|
|
|
(provide 'lsp-eslint)
|
|
;;; lsp-eslint.el ends here
|