From c44920252475c26b283df3fec60c91a6af18106d Mon Sep 17 00:00:00 2001 From: Luckas Date: Sat, 10 Aug 2024 19:04:09 +0300 Subject: [PATCH] feat!: rplugin & telescope extension --- .gitignore | 1 + README.md | 24 +- lua/tailwind-tools/cmp.lua | 11 +- lua/tailwind-tools/config.lua | 7 + lua/tailwind-tools/init.lua | 3 + lua/tailwind-tools/utils.lua | 11 + lua/telescope/_extensions/tailwind.lua | 103 ++++++ rplugin/node/tailwind-tools/package-lock.json | 293 ++++++++++++++++++ rplugin/node/tailwind-tools/package.json | 23 ++ rplugin/node/tailwind-tools/src/index.js | 106 +++++++ rplugin/node/tailwind-tools/src/utils.js | 75 +++++ tests/init.lua | 7 +- 12 files changed, 652 insertions(+), 12 deletions(-) create mode 100644 .gitignore create mode 100644 lua/telescope/_extensions/tailwind.lua create mode 100644 rplugin/node/tailwind-tools/package-lock.json create mode 100644 rplugin/node/tailwind-tools/package.json create mode 100644 rplugin/node/tailwind-tools/src/index.js create mode 100644 rplugin/node/tailwind-tools/src/utils.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md index 4c394f7..adda5e8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ It currently provides the following features: - Class concealing - Class sorting (without [prettier-plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)) - Completion utilities (using [nvim-cmp](https://github.com/hrsh7th/nvim-cmp)) +- Class previewier (using [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)) - Class motions > [!NOTE] @@ -39,6 +40,7 @@ It currently provides the following features: - Neovim v0.9 or higher (v0.10 is recommended) - [tailwindcss-language-server](https://github.com/tailwindlabs/tailwindcss-intellisense/tree/master/packages/tailwindcss-language-server) >= `v0.0.14` (can be installed using [Mason](https://github.com/williamboman/mason.nvim)) - `html`, `css`, `tsx` and other language Treesitter grammars (using [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter)) +- Neovim [node-client](https://www.npmjs.com/package/neovim) (using npm) > [!TIP] > If you are not familiar with neovim LSP ecosystem check out [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) to learn how to setup the LSP. @@ -51,12 +53,14 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim): -- tailwind-tools.lua return { "luckasRanarison/tailwind-tools.nvim", + name = "tailwind-tools", + build = ":UpdateRemotePlugins", dependencies = { "nvim-treesitter/nvim-treesitter" }, opts = {} -- your configuration } ``` -If you are using other package managers you need to call `setup`: +If you are using other package managers you need register the remote plugin by running the `:UpdateRemotePlugins` command, then call `setup` to enable the lua plugin: ```lua require("tailwind-tools").setup({ @@ -88,6 +92,12 @@ Here is the default configuration: fg = "#38BDF8", }, }, + telescope = { + utilities = { + -- the function used when selecting an utility class in telescope + callback = function(name, class) end, + }, + }, -- see the extension section to learn more extension = { queries = {}, -- a list of filetypes having custom `class` queries @@ -117,6 +127,8 @@ Available commands: ## Utilities +### nvim-cmp + Utility function for highlighting colors in [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) using [lspkind.nvim](https://github.com/onsails/lspkind.nvim): ```lua @@ -144,6 +156,14 @@ return { > [!TIP] > You can extend it by calling the function and get the returned `vim_item`, see the nvim-cmp [wiki](https://github.com/hrsh7th/nvim-cmp/wiki/Menu-Appearance) to learn more. +### telescope.nvim + +The plugins registers by default a telescope extension that you can call using `:Telescope tailwind ` + +Available subcommands: + +- `utilities`: Lists all utility classes available in the current projects. + ## Extension The plugin already supports many languages, but requests for additional language support and PRs are welcome. You can also extend the language support in your configuration by using Treesitter queries or Lua patterns (or both). @@ -186,7 +206,7 @@ The `class.scm` file should contain a query used to extract the class values for (attribute_value) @tailwind)) ``` -Note that quantified captures (using `+` or `?`) cannot be captured using `@tailwind`. Instead, you must capture the parent node using `@Ƨailwind.inner`. +Note that quantified captures (using `+` or `?`) cannot be captured using `@tailwind`. Instead, you must capture the parent node using `@tailwind.inner`. ```scheme (arguments diff --git a/lua/tailwind-tools/cmp.lua b/lua/tailwind-tools/cmp.lua index b85ceec..47663c4 100644 --- a/lua/tailwind-tools/cmp.lua +++ b/lua/tailwind-tools/cmp.lua @@ -11,16 +11,9 @@ M.lspkind_format = function(entry, vim_item) if vim_item.kind == "Color" and doc then local content = type(doc) == "string" and doc or doc.value - local base, _, _, _r, _g, _b = 10, content:find("rgba?%((%d+), (%d+), (%d+)") + local r, g, b = utils.extract_color(content) - if not _r then - base, _, _, _r, _g, _b = 16, content:find("#(%x%x)(%x%x)(%x%x)") - end - - if _r then - local r, g, b = tonumber(_r, base), tonumber(_g, base), tonumber(_b, base) - vim_item.kind_hl_group = utils.set_hl_from(r, g, b, "foreground") - end + if r then vim_item.kind_hl_group = utils.set_hl_from(r, g, b, "foreground") end end return vim_item diff --git a/lua/tailwind-tools/config.lua b/lua/tailwind-tools/config.lua index 2898e76..d09a32a 100644 --- a/lua/tailwind-tools/config.lua +++ b/lua/tailwind-tools/config.lua @@ -1,3 +1,5 @@ +---@diagnostic disable: unused-local + local M = {} ---@alias TailwindTools.ColorHint "foreground" | "background" | "inline" @@ -19,6 +21,11 @@ M.options = { fg = "#38BDF8", }, }, + telescope = { + utilities = { + callback = function(_name, _css) end, + }, + }, extension = { queries = {}, patterns = {}, diff --git a/lua/tailwind-tools/init.lua b/lua/tailwind-tools/init.lua index add2ab5..464eb71 100644 --- a/lua/tailwind-tools/init.lua +++ b/lua/tailwind-tools/init.lua @@ -52,6 +52,9 @@ M.setup = function(options) if state.conceal.enabled then conceal.enable() end end, }) + + local has_telescope, telescope = pcall(require, "telescope") + if has_telescope then telescope.load_extension("tailwind") end end return M diff --git a/lua/tailwind-tools/utils.lua b/lua/tailwind-tools/utils.lua index 461c963..f61f339 100644 --- a/lua/tailwind-tools/utils.lua +++ b/lua/tailwind-tools/utils.lua @@ -24,4 +24,15 @@ M.set_hl_from = function(red, green, blue, style) return group end +---@param s string +M.extract_color = function(s) + local base, _, _, _r, _g, _b = 10, s:find("rgba?%((%d+), (%d+), (%d+)") + + if not _r then + base, _, _, _r, _g, _b = 16, s:find("#(%x%x)(%x%x)(%x%x)") + end + + if _r then return tonumber(_r, base), tonumber(_g, base), tonumber(_b, base) end +end + return M diff --git a/lua/telescope/_extensions/tailwind.lua b/lua/telescope/_extensions/tailwind.lua new file mode 100644 index 0000000..d9a170a --- /dev/null +++ b/lua/telescope/_extensions/tailwind.lua @@ -0,0 +1,103 @@ +local log = require("tailwind-tools.log") +local utils = require("tailwind-tools.utils") +local plugin_config = require("tailwind-tools.config") + +local actions = require("telescope.actions") +local action_state = require("telescope.actions.state") +local finders = require("telescope.finders") +local pickers = require("telescope.pickers") +local previewers = require("telescope.previewers") +local entry_display = require("telescope.pickers.entry_display") +local config = require("telescope.config").values + +---@class TailwindTools.ClassEntry +---@field name string +---@field value any + +local fg_prefixes = { "text", "border", "outline" } + +local function get_hl_kind(class_name) + for _, prefix in pairs(fg_prefixes) do + if vim.startswith(class_name, prefix) then return "foreground" end + end + + return "background" +end + +local function class_picker() + local classes = vim.fn.TailwindGetUtilities() --[[@as TailwindTools.ClassEntry[] | nil]] + + if not classes then return log.error("No project found") end + + local displayer = entry_display.create({ + separator = "", + items = { { remaining = true } }, + }) + + local finder = finders.new_table({ + results = classes, + ---@param entry TailwindTools.ClassEntry + entry_maker = function(entry) + local highlight = "Normal" + + if type(entry.value) == "string" then + local r, g, b = utils.extract_color(entry.value) + if r then + local kind = get_hl_kind(entry.name) + highlight = utils.set_hl_from(r, g, b, kind) + end + end + + return { + value = entry, + display = function() return displayer({ { entry.name, highlight } }) end, + ordinal = entry.name, + } + end, + }) + + local previewer = previewers.new_buffer_previewer({ + title = "Preview", + define_preview = function(self, entry) + local bufnr = self.state.bufnr + local css = vim.fn.TailwindExpandUtilities({ entry.value.name }) + + if type(css) == "string" then + vim.bo[bufnr].ft = "css" + vim.api.nvim_buf_set_text(bufnr, 0, -1, 0, -1, vim.split(css, "\n")) + entry.value.css = css + end + end, + }) + + local attach_mappings = function() + actions.select_default:replace(function(prompt_bufnr) + local selection = action_state.get_selected_entry() + local name = selection.value.name + local css = selection.value.css or "" + + actions.close(prompt_bufnr) + + plugin_config.options.telescope.utilities.callback(name, css) + end) + + return true + end + + pickers + .new({}, { + prompt_title = "Tailwind classes", + finder = finder, + sorter = config.generic_sorter(), + previewer = previewer, + attach_mappings = attach_mappings, + }) + :find() +end + +return require("telescope").register_extension({ + setup = function() end, + exports = { + utilities = class_picker, + }, +}) diff --git a/rplugin/node/tailwind-tools/package-lock.json b/rplugin/node/tailwind-tools/package-lock.json new file mode 100644 index 0000000..46836b7 --- /dev/null +++ b/rplugin/node/tailwind-tools/package-lock.json @@ -0,0 +1,293 @@ +{ + "name": "tailwind-tools", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tailwind-tools", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "neovim": "^5.1.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", + "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/logform": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/neovim": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/neovim/-/neovim-5.1.0.tgz", + "integrity": "sha512-9eTxqknziWkN8CBOx1SKdK+0Dfp1NHKHKyJaeOYu+x6qjaV9z3hB211wKhLaFGtyYmGZxVaIe1aLtvuTHmuZTA==", + "dependencies": { + "@msgpack/msgpack": "^2.8.0", + "winston": "3.11.0" + }, + "bin": { + "neovim-node-host": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/winston": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", + "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", + "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", + "dependencies": { + "logform": "^2.6.1", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + } + } +} diff --git a/rplugin/node/tailwind-tools/package.json b/rplugin/node/tailwind-tools/package.json new file mode 100644 index 0000000..8c3615c --- /dev/null +++ b/rplugin/node/tailwind-tools/package.json @@ -0,0 +1,23 @@ +{ + "name": "tailwind-tools", + "version": "1.0.0", + "description": "tailwind-tools.nvim NodeJS remote plugin", + "keywords": [ + "tailwindcss", + "neovim" + ], + "license": "MIT", + "author": "LIOKA Ranarison Fiderana", + "repository": { + "type": "git", + "url": "git+https://github.com/luckasRanarison/tailwind-tools.nvim.git" + }, + "bugs": { + "url": "https://github.com/luckasRanarison/tailwind-tools.nvim/issues" + }, + "homepage": "https://github.com/luckasRanarison/tailwind-tools.nvim#readme", + "main": "src/index.js", + "dependencies": { + "neovim": "^5.1.0" + } +} diff --git a/rplugin/node/tailwind-tools/src/index.js b/rplugin/node/tailwind-tools/src/index.js new file mode 100644 index 0000000..06a74bd --- /dev/null +++ b/rplugin/node/tailwind-tools/src/index.js @@ -0,0 +1,106 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const utils = require("./utils"); + +class Plugin { + /** + * @param {import("neovim").NvimPlugin} plugin + */ + constructor(plugin) { + this.nvim = plugin.nvim; + + plugin.registerFunction( + "TailwindGetConfig", + this.getTailwindConfig.bind(this), + { sync: true } + ); + + plugin.registerFunction( + "TailwindGetUtilities", + this.getUtilities.bind(this), + { sync: true } + ); + + plugin.registerFunction( + "TailwindExpandUtilities", + this.expandUtilities.bind(this), + { sync: true } + ); + } + + // TODO: Get the running language server root directory? + getProjectRoot() { + return this.nvim.call("getcwd"); + } + + async getTailwindConfig() { + const rootDir = await this.getProjectRoot(); + const tailwindPath = path.join(rootDir, "node_modules", "tailwindcss"); + + if (!fs.existsSync(tailwindPath)) return; + + const _require = utils.getNodeModuleResolver(rootDir); + const resolveConfig = _require("tailwindcss/resolveConfig"); + const loadConfig = _require("tailwindcss/lib/public/load-config"); + + const configExtensions = ["js", "ts", "cjs"]; + const configPath = configExtensions + .map((ext) => path.join(rootDir, `tailwind.config.${ext}`)) + .find((filePath) => fs.existsSync(filePath)); + + return resolveConfig(configPath ? loadConfig(configPath) : {}); + } + + /** + * @returns {{name: string, value: any}[]} + */ + async getUtilities() { + const config = await this.getTailwindConfig(); + + if (!config) return; + + const root = await this.getProjectRoot(); + const _require = utils.getNodeModuleResolver(root); + const flattenPalette = _require("tailwindcss/lib/util/flattenColorPalette"); + + const { theme } = config; + + for (const key in theme) { + if (utils.isColorClass(key)) theme[key] = flattenPalette(theme[key]); + } + + const entries = Object.entries(theme).flatMap(([className, values]) => { + const normalizedName = utils.normalizeClassName(className); + return Object.entries(values).map(([subName, value]) => ({ + name: utils.mergeClass(normalizedName, subName), + value: value, + })); + }); + + return entries; + } + + /** + * @param {string[]} classes + */ + async expandUtilities(classes) { + const config = await this.getTailwindConfig(); + + if (!config) return; + + config.content = [{ raw: classes.join(" ") }]; + + const rootDir = await this.getProjectRoot(); + const _require = utils.getNodeModuleResolver(rootDir); + const postcss = _require("postcss"); + const tailwind = _require("tailwindcss"); + const processor = postcss(tailwind(config)); + + return processor + .process("@tailwind utilities", { from: undefined }) + .async() + .then(({ css }) => css); + } +} + +module.exports = Plugin; diff --git a/rplugin/node/tailwind-tools/src/utils.js b/rplugin/node/tailwind-tools/src/utils.js new file mode 100644 index 0000000..b1d1d18 --- /dev/null +++ b/rplugin/node/tailwind-tools/src/utils.js @@ -0,0 +1,75 @@ +const path = require("node:path"); + +function getNodeModuleResolver(rootDir) { + return (modulePath) => { + const absolutePath = path.join(rootDir, "node_modules", modulePath); + const _module = require(absolutePath); + const { default: _default } = _module; + return _default ? _default : _module; + }; +} + +const classNameMap = { + fontSize: "text", + fontWeight: "font", + fontFamilly: "font", + lineHeight: "leading", + keyframes: "animate", + animation: "animate", + aspectRation: "aspect", + letterSpacing: "tracking", + backgroundSize: "bg", + backgroundImage: "bg", + backgroundPosition: "bg", + borderWidth: "border", + borderRadius: "rounded", + gridAutoRows: "auto-rows", + gridAutoColumns: "auto-cols", + gridTemplateRows: "grid-rows", + gridTemplateColumns: "grid-cols", + zIndex: "z", +}; + +const nameReplacements = [ + ["grid-column", "col"], + ["grid-row", "row"], + ["background", "bg"], + ["width", "w"], + ["height", "h"], + ["padding", "p"], + ["margin", "m"], + ["-color", ""], +]; + +function camelToKebabCase(camel) { + return camel.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); +} + +function normalizeClassName(name) { + const mapping = classNameMap[name]; + + if (mapping) return mapping; + + let kebabName = camelToKebabCase(name); + + for (const [name, replacement] of nameReplacements) { + kebabName = kebabName.replace(name, replacement); + } + + return kebabName; +} + +function mergeClass(key, value) { + return value === "DEFAULT" ? key : key + "-" + value; +} + +function isColorClass(name) { + return name === "fill" || name === "stroke" || name.match(/[cC]olor/); +} + +module.exports = { + getNodeModuleResolver, + normalizeClassName, + mergeClass, + isColorClass, +}; diff --git a/tests/init.lua b/tests/init.lua index 0ee2873..9544421 100644 --- a/tests/init.lua +++ b/tests/init.lua @@ -18,7 +18,12 @@ vim.o.swapfile = false require("lazy").setup({ { "neovim/nvim-lspconfig" }, { "nvim-lua/plenary.nvim", cmd = "PlenaryBustedDirectory" }, - { dir = "./", opts = {}, dependencies = { "nvim-treesitter/nvim-treesitter" } }, + { + dir = "./", + name = "tailwind-tools", + opts = {}, + dependencies = { "nvim-treesitter/nvim-treesitter" }, + }, }) require("lspconfig").tailwindcss.setup({})