;;; 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 ;; 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 . ;;; 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