359 lines
14 KiB
EmacsLisp
359 lines
14 KiB
EmacsLisp
|
|
;;; lsp-tailwindcss.el --- A lsp-mode client for Tailwind CSS -*- lexical-binding: t; -*-
|
||
|
|
|
||
|
|
;; Copyright (C) 2020 Merrick Luo
|
||
|
|
;; Copyright (C) 2024-2026 emacs-lsp maintainers
|
||
|
|
|
||
|
|
;; Author: Merrick Luo <merrick@luois.me>
|
||
|
|
;; Keywords: language tools
|
||
|
|
|
||
|
|
;; 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:
|
||
|
|
|
||
|
|
;; LSP client for the Tailwind CSS language server.
|
||
|
|
|
||
|
|
;;; Code:
|
||
|
|
|
||
|
|
(require 'lsp-mode)
|
||
|
|
(require 'f)
|
||
|
|
|
||
|
|
(defgroup lsp-tailwindcss nil
|
||
|
|
"Adds lsp-mode support for Tailwind CSS."
|
||
|
|
:group 'lsp-mode
|
||
|
|
:link '(url-link "https://github.com/tailwindlabs/tailwindcss-intellisense"))
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-add-on-mode t
|
||
|
|
"Specify lsp-tailwindcss as add-on so it can work with other language servers."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-server-version "0.14.7"
|
||
|
|
"Specify the version of tailwindcss intellisense."
|
||
|
|
:type 'string
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-server-path ""
|
||
|
|
"Path to the tailwindcss lsp server binary (node.js executable).
|
||
|
|
|
||
|
|
Leave empty to use the managed installation."
|
||
|
|
:type 'string
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-major-modes '(rjsx-mode web-mode html-mode css-mode typescript-mode typescript-tsx-mode tsx-ts-mode)
|
||
|
|
"Major modes that lsp-tailwindcss should activate."
|
||
|
|
:type '(repeat symbol)
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-rustywind-command (executable-find "rustywind")
|
||
|
|
"[Experimental] Command used for sorting the tailwindcss classes."
|
||
|
|
:type 'string
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-rustywind-extra-args nil
|
||
|
|
"[Experimental] Extra arguments to use when invoking rustywind."
|
||
|
|
:type '(repeat string)
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-skip-config-check nil
|
||
|
|
"Force skip config file check.
|
||
|
|
Only use it when your config file is in an unconventional location
|
||
|
|
and you are sure tailwindcss language server can find it."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
;;;###autoload
|
||
|
|
(defun lsp-tailwindcss-rustywind ()
|
||
|
|
"[Experimental] Sort tailwindcss class name using rustywind."
|
||
|
|
(interactive)
|
||
|
|
(if (and lsp-tailwindcss-rustywind-command
|
||
|
|
(f-executable-p lsp-tailwindcss-rustywind-command))
|
||
|
|
(let ((tmpfile (make-nearby-temp-file "rustywind" nil nil))
|
||
|
|
(coding-system-for-read 'utf-8)
|
||
|
|
(coding-system-for-write 'utf-8))
|
||
|
|
|
||
|
|
(unwind-protect
|
||
|
|
(save-restriction
|
||
|
|
(widen)
|
||
|
|
(write-region nil nil tmpfile)
|
||
|
|
|
||
|
|
(let ((rustywind-args (append (list "--write" (file-local-name tmpfile)) lsp-tailwindcss-rustywind-extra-args)))
|
||
|
|
(when (zerop (apply #'process-file lsp-tailwindcss-rustywind-command nil nil nil rustywind-args))
|
||
|
|
(insert-file-contents tmpfile nil nil nil t))))
|
||
|
|
|
||
|
|
(delete-file tmpfile)))
|
||
|
|
(error "Can't find rustywind executable at %s" lsp-tailwindcss-rustywind-command)))
|
||
|
|
|
||
|
|
;;;###autoload
|
||
|
|
(defun lsp-tailwindcss-rustywind-before-save ()
|
||
|
|
"[Experimental] Run rustywind when saving buffer.
|
||
|
|
By adding this to `before-save-hook', it only runs when
|
||
|
|
lsp-tailwindcss can be activated, see `lsp-tailwindcss--activate-p'."
|
||
|
|
(when (lsp-tailwindcss--activate-p)
|
||
|
|
(lsp-tailwindcss-rustywind)))
|
||
|
|
|
||
|
|
;;;###autoload
|
||
|
|
(defun lsp-tailwindcss-installed-server-version ()
|
||
|
|
"Get the installed version of tailwindcss language server."
|
||
|
|
(interactive)
|
||
|
|
(let ((package-json-file (f-join lsp-server-install-dir "tailwindcss/extension/package.json")))
|
||
|
|
(if (f-exists? package-json-file)
|
||
|
|
(let ((json-object-type 'hash-table)
|
||
|
|
(json-array-type 'list)
|
||
|
|
(json-key-type 'string))
|
||
|
|
(message "%s" (gethash "version" (json-read-file package-json-file))))
|
||
|
|
(error "Can't find package.json file at %s" package-json-file))))
|
||
|
|
|
||
|
|
;;; Language server global settings:
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-emmet-completions nil
|
||
|
|
"Enable completions when using Emmet-style syntax.
|
||
|
|
For example div.bg-red-500.uppercase."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-show-pixel-equivalents t
|
||
|
|
"Show px equivalents for rem CSS values in completions and hovers."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-root-font-size 16
|
||
|
|
"Root font size in pixels.
|
||
|
|
Used to convert rem CSS values to their px equivalents.
|
||
|
|
See `lsp-tailwindcss-show-pixel-equivalents'."
|
||
|
|
:type 'number
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-validate t
|
||
|
|
"Enable linting.
|
||
|
|
Rules can be configured individually using the lsp-tailwindcss-lint-* settings:
|
||
|
|
ignore: disable lint rule entirely
|
||
|
|
warning: rule violations will be considered \"warnings\"
|
||
|
|
error: rule violations will be considered \"errors\""
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-hovers t
|
||
|
|
"Enable hovers."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-suggestions t
|
||
|
|
"Enable autocomplete suggestions."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-code-actions t
|
||
|
|
"Enable code actions."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-lint-invalid-screen "error"
|
||
|
|
"Unknown screen name used with the @screen directive."
|
||
|
|
:type '(choice (const "ignore")
|
||
|
|
(const "warning")
|
||
|
|
(const "error"))
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-lint-invalid-variant "error"
|
||
|
|
"Unknown variant name used with the @variants directive."
|
||
|
|
:type '(choice (const "ignore")
|
||
|
|
(const "warning")
|
||
|
|
(const "error"))
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-lint-invalid-tailwind-directive "error"
|
||
|
|
"Unknown value used with the @tailwind directive."
|
||
|
|
:type '(choice (const "ignore")
|
||
|
|
(const "warning")
|
||
|
|
(const "error"))
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-lint-invalid-apply "error"
|
||
|
|
"Unsupported use of the @apply directive."
|
||
|
|
:type '(choice (const "ignore")
|
||
|
|
(const "warning")
|
||
|
|
(const "error"))
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-lint-invalid-config-path "error"
|
||
|
|
"Unknown or invalid path used with the theme helper."
|
||
|
|
:type '(choice (const "ignore")
|
||
|
|
(const "warning")
|
||
|
|
(const "error"))
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-lint-css-conflict "warning"
|
||
|
|
"Class names on the same HTML element which apply the same CSS property or properties."
|
||
|
|
:type '(choice (const "ignore")
|
||
|
|
(const "warning")
|
||
|
|
(const "error"))
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-lint-recommended-variant-order "warning"
|
||
|
|
"Class variants not in the recommended order (applies in JIT mode only)."
|
||
|
|
:type '(choice (const "ignore")
|
||
|
|
(const "warning")
|
||
|
|
(const "error"))
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-experimental-class-regex ""
|
||
|
|
"Custom regex to match tailwindcss classes."
|
||
|
|
:type 'string
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-class-attributes ["class" "className" "ngClass" "class:list"]
|
||
|
|
"The HTML attributes to provide class completions, hover previews, linting etc."
|
||
|
|
:type 'lsp-string-vector
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-class-functions nil
|
||
|
|
"Functions in which to provide completions, hover previews, linting etc.
|
||
|
|
Currently, this works for both function calls and tagged template literals
|
||
|
|
in JavaScript / TypeScript."
|
||
|
|
:type 'lsp-string-vector
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(defcustom lsp-tailwindcss-experimental-config-file nil
|
||
|
|
"Manually specify the Tailwind config file or files.
|
||
|
|
It can be a single string or a hash map. See
|
||
|
|
https://github.com/tailwindlabs/tailwindcss-intellisense#tailwindcssexperimentalconfigfile
|
||
|
|
|
||
|
|
Example:
|
||
|
|
(setq lsp-tailwindcss-experimental-config-file \"config/tailwindcss.conf.js\")
|
||
|
|
|
||
|
|
(setq lsp-tailwindcss-experimental-config-file
|
||
|
|
(ht
|
||
|
|
(\"themes/simple/tailwind.config.js\" \"themes/simple/**\")
|
||
|
|
(\"themes/neon/tailwind.config.js\" \"themes/neon/**\")))"
|
||
|
|
:type 'sexp
|
||
|
|
:group 'lsp-tailwindcss)
|
||
|
|
|
||
|
|
(lsp-register-custom-settings
|
||
|
|
'(("tailwindCSS.emmetCompletions" lsp-tailwindcss-emmet-completions t)
|
||
|
|
("tailwindCSS.showPixelEquivalents" lsp-tailwindcss-show-pixel-equivalents t)
|
||
|
|
("tailwindCSS.rootFontSize" lsp-tailwindcss-root-font-size)
|
||
|
|
("tailwindCSS.validate" lsp-tailwindcss-validate t)
|
||
|
|
("tailwindCSS.hovers" lsp-tailwindcss-hovers t)
|
||
|
|
("tailwindCSS.suggestions" lsp-tailwindcss-suggestions t)
|
||
|
|
("tailwindCSS.codeActions" lsp-tailwindcss-code-actions t)
|
||
|
|
("tailwindCSS.lint.invalidScreen" lsp-tailwindcss-lint-invalid-screen)
|
||
|
|
("tailwindCSS.lint.invalidVariant" lsp-tailwindcss-lint-invalid-variant)
|
||
|
|
("tailwindCSS.lint.invalidTailwindDirective" lsp-tailwindcss-lint-invalid-tailwind-directive)
|
||
|
|
("tailwindCSS.lint.invalidApply" lsp-tailwindcss-lint-invalid-apply)
|
||
|
|
("tailwindCSS.lint.invalidConfigPath" lsp-tailwindcss-lint-invalid-config-path)
|
||
|
|
("tailwindCSS.lint.cssConflict" lsp-tailwindcss-lint-css-conflict)
|
||
|
|
("tailwindCSS.lint.recommendedVariantOrder" lsp-tailwindcss-lint-recommended-variant-order)
|
||
|
|
("tailwindCSS.experimental.classRegex" lsp-tailwindcss-experimental-class-regex)
|
||
|
|
("tailwindCSS.experimental.configFile" lsp-tailwindcss-experimental-config-file)
|
||
|
|
("tailwindCSS.classAttributes" lsp-tailwindcss-class-attributes)
|
||
|
|
("tailwindCSS.classFunctions" lsp-tailwindcss-class-functions)))
|
||
|
|
|
||
|
|
;;; Server setup:
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss--download-url ()
|
||
|
|
"Build language server download url from version."
|
||
|
|
(let ((version lsp-tailwindcss-server-version))
|
||
|
|
(lsp-vscode-extension-url "bradlc" "vscode-tailwindcss" version)))
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss-server-command ()
|
||
|
|
"Return the path to the server command when installed from the vscode extension."
|
||
|
|
(f-join lsp-server-install-dir "tailwindcss/extension/dist/tailwindServer.js"))
|
||
|
|
|
||
|
|
(lsp-dependency 'tailwindcss-language-server
|
||
|
|
`(:download
|
||
|
|
:url lsp-tailwindcss--download-url
|
||
|
|
:decompress :zip
|
||
|
|
:store-path ,(f-join lsp-server-install-dir "tailwindcss" "server")
|
||
|
|
:binary-path lsp-tailwindcss-server-command))
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss--has-config-file ()
|
||
|
|
"Check if there is a tailwindcss config file.
|
||
|
|
To keep it simple and performant, only check for conventional locations.
|
||
|
|
See `lsp-tailwindcss-skip-config-check'."
|
||
|
|
(or lsp-tailwindcss-skip-config-check
|
||
|
|
lsp-tailwindcss-experimental-config-file
|
||
|
|
(f-glob "tailwind.config.*" (lsp-workspace-root))
|
||
|
|
(f-glob "**/tailwind.config.*" (lsp-workspace-root))))
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss--package-version (&optional file)
|
||
|
|
"Return tailwindcss version from package.json closest to FILE or current buffer.
|
||
|
|
Checks both \"dependencies\" and \"devDependencies\"."
|
||
|
|
(when-let* ((start-file (or file (buffer-file-name)))
|
||
|
|
(package-root (locate-dominating-file start-file "package.json"))
|
||
|
|
(package-file (expand-file-name "package.json" package-root)))
|
||
|
|
(let* ((json-object-type 'hash-table)
|
||
|
|
(json-array-type 'list)
|
||
|
|
(json-key-type 'string)
|
||
|
|
(data (json-read-file package-file)))
|
||
|
|
(or (gethash "tailwindcss" (gethash "dependencies" data (make-hash-table :test 'equal)))
|
||
|
|
(gethash "tailwindcss" (gethash "devDependencies" data (make-hash-table :test 'equal)))))))
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss--version-v4-p (&optional version)
|
||
|
|
"Check if tailwindcss package VERSION is v4 or above.
|
||
|
|
Will try to lookup the version if not provided."
|
||
|
|
(when-let* ((version (or version (lsp-tailwindcss--package-version))))
|
||
|
|
(string-match-p "^[~^]?\\([4-9]\\|[0-9]\\{2\\}\\)\\.?" version)))
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss--activate-p (&rest _args)
|
||
|
|
"Check if tailwindcss language server can/should start."
|
||
|
|
(and (lsp-workspace-root)
|
||
|
|
(apply #'provided-mode-derived-p major-mode lsp-tailwindcss-major-modes)
|
||
|
|
(or (lsp-tailwindcss--has-config-file) (lsp-tailwindcss--version-v4-p))))
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss--company-dash-hack (workspace)
|
||
|
|
"Append \"-\" to the lsp completion trigger characters for WORKSPACE.
|
||
|
|
Workaround for company-mode completion not working when typing \"-\" in classname."
|
||
|
|
(with-lsp-workspace workspace
|
||
|
|
(let* ((caps (lsp--workspace-server-capabilities workspace))
|
||
|
|
(comp (lsp:server-capabilities-completion-provider? caps))
|
||
|
|
(trigger-chars (append (lsp:completion-options-trigger-characters? comp) nil)))
|
||
|
|
(lsp:set-completion-options-trigger-characters?
|
||
|
|
comp
|
||
|
|
(vconcat
|
||
|
|
(cl-pushnew "-" trigger-chars :test #'string=))))))
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss--server-path ()
|
||
|
|
"Return path to the tailwindcss language server."
|
||
|
|
(if (string-empty-p lsp-tailwindcss-server-path)
|
||
|
|
(lsp-package-path 'tailwindcss-language-server)
|
||
|
|
lsp-tailwindcss-server-path))
|
||
|
|
|
||
|
|
(defun lsp-tailwindcss--initialization-options ()
|
||
|
|
"Return initialization options for the tailwindcss language server."
|
||
|
|
(ht ("configuration" (lsp-configuration-section "tailwindcss"))))
|
||
|
|
|
||
|
|
(lsp-register-client
|
||
|
|
(make-lsp-client
|
||
|
|
:new-connection (lsp-stdio-connection
|
||
|
|
(lambda ()
|
||
|
|
`("node" ,(lsp-tailwindcss--server-path) "--stdio"))
|
||
|
|
(lambda ()
|
||
|
|
(when-let* ((server-path (lsp-tailwindcss--server-path)))
|
||
|
|
(file-exists-p server-path))))
|
||
|
|
:activation-fn #'lsp-tailwindcss--activate-p
|
||
|
|
:server-id 'tailwindcss
|
||
|
|
:priority -1
|
||
|
|
:add-on? lsp-tailwindcss-add-on-mode
|
||
|
|
:initialization-options #'lsp-tailwindcss--initialization-options
|
||
|
|
:initialized-fn #'lsp-tailwindcss--company-dash-hack
|
||
|
|
:notification-handlers (ht ("@/tailwindCSS/projectInitialized" #'ignore)
|
||
|
|
("@/tailwindCSS/projectsDestroyed" #'ignore))
|
||
|
|
:download-server-fn (lambda (_client callback error-callback _update?)
|
||
|
|
(lsp-package-ensure 'tailwindcss-language-server callback error-callback))))
|
||
|
|
|
||
|
|
(lsp-consistency-check lsp-tailwindcss)
|
||
|
|
|
||
|
|
(provide 'lsp-tailwindcss)
|
||
|
|
;;; lsp-tailwindcss.el ends here
|