config

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

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