Skip to content

Commit

Permalink
feat!: lua pattern support (#31)
Browse files Browse the repository at this point in the history
Breaking change: `custom_filetypes` has been removed and replaced by the `extension` field.

* wip: lua pattern support

* fix: pattern search & lsp request

* fix: tests

* update: pattern handling & add rust tests

* refactor: iniline spec

* fix: class query logic

* update!: config options

* fix(workflow): precise branches

Prevent double action triggers.

* update: README.md
  • Loading branch information
luckasRanarison authored Aug 1, 2024
1 parent 05c1f15 commit da8eee8
Show file tree
Hide file tree
Showing 24 changed files with 352 additions and 182 deletions.
9 changes: 3 additions & 6 deletions .luarc.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
{
"diagnostics": {
"runtime": {
"version": "LuaJIT"
}
},
"runtime.version": "LuaJIT",
"workspace.library": [
"/usr/local/share/nvim/runtime/lua",
"~/.local/share/nvim/lazy/nvim-cmp",
"${3rd}/luv/library",
"${3rd}/luassert/library"
"${3rd}/luassert/library",
"${3rd}/busted/library"
]
}
57 changes: 49 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# tailwind-tools.nvim

Unofficial [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) integration and tooling for [Neovim](https://github.com/neovim/neovim) using the built-in LSP client and treesitter, inspired by the official Visual Studio Code [extension](https://github.com/tailwindlabs/tailwindcss-intellisense).
Unofficial [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) integration and tooling for [Neovim](https://github.com/neovim/neovim) using the built-in LSP client and Treesitter, inspired by the official Visual Studio Code [extension](https://github.com/tailwindlabs/tailwindcss-intellisense).

![preview](https://github.com/luckasRanarison/tailwind-tools.nvim/assets/101930730/cb1c0508-8375-474f-9078-2842fb62e0b7)

Expand All @@ -21,7 +21,9 @@ Unofficial [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) integrati

## Features

The plugin works with all languages inheriting from html, css and tsx treesitter grammars (php, astro, vue, svelte, [...](./lua/tailwind-tools/filetypes.lua)) and provides the following features:
The plugin works with all languages inheriting from html, css and tsx treesitter grammars (php, astro, vue, svelte, [...](./lua/tailwind-tools/filetypes.lua)). Lua patterns can also be used as a fallback.

It currently provides the following features:

- Class color hints
- Class concealing
Expand All @@ -36,7 +38,7 @@ The plugin works with all languages inheriting from html, css and tsx treesitter

- 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 your other languages treesitter grammars (using [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter))
- `html`, `css`, `tsx` and other language Treesitter grammars (using [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter))

> [!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.
Expand Down Expand Up @@ -86,7 +88,15 @@ Here is the default configuration:
fg = "#38BDF8",
},
},
custom_filetypes = {} -- see the extension section to learn how it works
-- see the extension section to learn more
extension = {
queries = {}, -- a list of filetypes having custom `class` queries
patterns = { -- a map of filetypes to Lua pattern lists
-- exmaple:
-- rust = { "class=[\"']([^\"']+)[\"']" },
-- javascript = { "clsx%(([^)]+)%)" },
},
},
}
```

Expand Down Expand Up @@ -136,11 +146,23 @@ return {
## Extension

The plugin basically works with any language as long it has a treesitter parser and a `class` query. You can check the currently available queries and supported filetypes [here](./queries), feel free to request other languages support.
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).

But you can also create your own queries! If you are not familiar with treesitter queries you should check out the treesitter query documentation from [Neovim](https://neovim.io/doc/user/treesitter.html#treesitter-query) or [Treesitter](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax).
### Treesitter queries

To add a new filetype you first need to add it to your configuration then the plugin will search for a `class.scm` file (classexpr) associated to that filetype in your `runtimepath`. You could use your Neovim configuration folder to store queries in the following way:
Treesitter queries are recommended but can be harder to write, if you are not familiar with Treesitter queries, check out the documentation from [Neovim](https://neovim.io/doc/user/treesitter.html#treesitter-query) or [Treesitter](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax).

You can define custom queries for a filetype by adding the filetype to the `queries` list, like this:

```lua
{
extension = {
queries = { "myfiletype" },
}
}
```

The plugin will search for a `class.scm` file (classexpr) associated with that filetype in your `runtimepath`. You can use your Neovim configuration folder to store queries in the following way:

```
~/.config/nvim
Expand All @@ -153,7 +175,7 @@ To add a new filetype you first need to add it to your configuration then the pl
      └── class.scm
```

The `class.scm` file should contain a query used to extract the class values for a given filetype. The class value should be captured using `@tailwind` as shown in the follwing example:
The `class.scm` file should contain a query used to extract the class values for a given filetype. The class value should be captured using `@tailwind`, as shown in the follwing example:

```scheme
; queries/myfiletype/class.scm
Expand All @@ -167,6 +189,25 @@ The `class.scm` file should contain a query used to extract the class values for
> [!NOTE]
> Some class ranges cannot be precisely captured using queries alone and are handled in code. You can also check out the existing [queries](./queries) to see more examples.
### Lua patterns

[Lua patterns](https://www.lua.org/pil/20.2.html) are easier to write, but note that the underlying implementation is not completely efficient, although this inefficiency is likely negligible. Currently, there are no reliable APIs for performing pattern searches and retrieving information about capture positions.

You can define custom patterns by attaching a list of patterns to filetypes. Each pattern should have exactly **one** capture group representing the class value, as shown below:

```lua
{
extension = {
patterns = {
javascript = { "clsx%(([^)]+)%)" },
},
}
}
```

> [!TIP]
> Lua patterns can be combined with Treesitter queries. You can use both for a single filetype to get the combined results.
## Related projects

Here are some related projects:
Expand Down
28 changes: 28 additions & 0 deletions lua/tailwind-tools/classes.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
local M = {}

local patterns = require("tailwind-tools.patterns")
local filetypes = require("tailwind-tools.filetypes")
local tresitter = require("tailwind-tools.treesitter")
local config = require("tailwind-tools.config")

---@param bufnr number
---@return number[][]
M.get_ranges = function(bufnr)
local results = {}
local ft = vim.bo[bufnr].ft
local extension = config.options.extension
local query_list = vim.tbl_extend("force", filetypes.treesitter, extension.queries)
local pattern_list = vim.tbl_extend("force", filetypes.luapattern, extension.patterns)

for _, pattern in pairs(pattern_list[ft] or {}) do
vim.list_extend(results, patterns.find_class_ranges(bufnr, pattern))
end

if vim.tbl_contains(query_list, ft) then
vim.list_extend(results, tresitter.find_class_ranges(bufnr, ft) or {})
end

return results
end

return M
12 changes: 6 additions & 6 deletions lua/tailwind-tools/conceal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ local M = {}
local lsp = require("tailwind-tools.lsp")
local state = require("tailwind-tools.state")
local config = require("tailwind-tools.config")
local treesitter = require("tailwind-tools.treesitter")
local classes = require("tailwind-tools.classes")

---@param bufnr number
local function set_conceal(bufnr)
local class_nodes = treesitter.get_class_nodes(bufnr)
local class_ranges = classes.get_ranges(bufnr)

if not class_nodes then return end
if #class_ranges == 0 then return end

vim.wo.conceallevel = 2
vim.api.nvim_buf_clear_namespace(bufnr, vim.g.tailwind_tools.conceal_ns, 0, -1)
Expand All @@ -18,10 +18,10 @@ local function set_conceal(bufnr)

local opts = config.options.conceal

for _, node in pairs(class_nodes) do
local start_row, start_col, end_row, end_col = treesitter.get_class_range(node, bufnr)
for _, range in pairs(class_ranges) do
local start_row, start_col, end_row, end_col = unpack(range)

if not opts.min_length or node:byte_length() >= opts.min_length then
if not opts.min_length or end_row ~= start_row or end_col - start_col >= opts.min_length then
vim.api.nvim_buf_set_extmark(bufnr, vim.g.tailwind_tools.conceal_ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
Expand Down
5 changes: 4 additions & 1 deletion lua/tailwind-tools/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ M.options = {
fg = "#38BDF8",
},
},
custom_filetypes = {},
extension = {
queries = {},
patterns = {},
},
}

return M
29 changes: 17 additions & 12 deletions lua/tailwind-tools/filetypes.lua
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
return {
"html",
"css",
"php",
"twig",
"vue",
"svelte",
"astro",
"heex",
"elixir",
"htmldjango",
"javascriptreact",
"typescriptreact",
treesitter = {
"html",
"css",
"php",
"twig",
"vue",
"svelte",
"astro",
"heex",
"elixir",
"htmldjango",
"javascriptreact",
"typescriptreact",
},
luapattern = {
rust = { "class=[\"']([^\"']+)[\"']" },
},
}
13 changes: 6 additions & 7 deletions lua/tailwind-tools/lsp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ local log = require("tailwind-tools.log")
local utils = require("tailwind-tools.utils")
local state = require("tailwind-tools.state")
local config = require("tailwind-tools.config")
local treesitter = require("tailwind-tools.treesitter")
local classes = require("tailwind-tools.classes")

local color_events = {
"BufEnter",
Expand Down Expand Up @@ -176,22 +176,21 @@ M.sort_classes = function()

local bufnr = vim.api.nvim_get_current_buf()
local params = vim.lsp.util.make_text_document_params(bufnr)
local class_nodes = treesitter.get_class_nodes(bufnr, true)
local class_ranges = classes.get_ranges(bufnr)

if not class_nodes then return end
if #class_ranges == 0 then return end

local class_text = {}
local class_ranges = {}

for _, node in pairs(class_nodes) do
local start_row, start_col, end_row, end_col = treesitter.get_class_range(node, bufnr)
for _, range in pairs(class_ranges) do
local start_row, start_col, end_row, end_col = unpack(range)
local text = vim.api.nvim_buf_get_text(bufnr, start_row, start_col, end_row, end_col, {})

class_text[#class_text + 1] = table.concat(text, "\n")
class_ranges[#class_ranges + 1] = { start_row, start_col, end_row, end_col }
end

params.classLists = class_text

client.request("@/tailwindCSS/sortSelection", params, function(err, result, _, _)
if err then return log.error(err.message) end
if result.error then return log.error(result.error) end
Expand Down
18 changes: 9 additions & 9 deletions lua/tailwind-tools/motions.lua
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
local M = {}

local log = require("tailwind-tools.log")
local treesitter = require("tailwind-tools.treesitter")
local classes = require("tailwind-tools.classes")

---@param comp fun(a: number, b: number): boolean
local move_to_class = function(comp)
local nodes = treesitter.get_class_nodes(0, true)
local bufnr = vim.api.nvim_get_current_buf()
local class_ranges = classes.get_ranges(bufnr)

if not nodes then return end
if #nodes == 0 then return log.info("No classes") end
if #class_ranges == 0 then return log.info("No classes") end

local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0))

table.sort(nodes, function(a, b)
local a_row, a_col = treesitter.get_class_range(a, 0)
local b_row, b_col = treesitter.get_class_range(b, 0)
table.sort(class_ranges, function(a, b)
local a_row, a_col = unpack(a)
local b_row, b_col = unpack(b)
return a_row == b_row and comp(b_col, a_col) or comp(b_row, a_row)
end)

for _, node in ipairs(nodes) do
local node_row, node_col = treesitter.get_class_range(node, 0)
for _, range in ipairs(class_ranges) do
local node_row, node_col = unpack(range)
local row = cursor_row - 1

if comp(node_row, row) or (node_row == row and comp(node_col, cursor_col)) then
Expand Down
63 changes: 63 additions & 0 deletions lua/tailwind-tools/patterns.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
local M = {}

---@param b_start number
---@param b_end number
---@param bufnr number
local function byte_range_to_pos(b_start, b_end, bufnr)
local line_count = vim.api.nvim_buf_line_count(bufnr)
local line_offsets = {}

for i = 1, line_count do
line_offsets[i] = vim.api.nvim_buf_get_offset(bufnr, i - 1)
end

local start_row, start_col, end_row, end_col

for line, offset in pairs(line_offsets) do
local next_offset = line_offsets[line + 1]

if not next_offset or b_start >= offset and b_start < next_offset then
start_row = line - 1
start_col = b_start - offset
end

if not next_offset or b_end >= offset and b_end < next_offset then
end_row = line - 1
end_col = b_end - offset
break
end
end

return start_row, start_col, end_row, end_col
end

---@param bufnr number
---@param pattern string
M.find_class_ranges = function(bufnr, pattern)
local results = {}
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
local substr = table.concat(lines, "\n")
local offset = 0

while true do
local b_start, b_end, class = substr:find(pattern)

if b_start == nil then break end

substr = substr:sub(b_start)
offset = offset + b_start - 1

local match_len = b_end - b_start
local class_start = substr:find(class, 1, true) + offset - 1
local class_end = class_start + #class
local sr, sc, er, ec = byte_range_to_pos(class_start, class_end, bufnr)

results[#results + 1] = { sr, sc, er, ec }
substr = substr:sub(match_len)
offset = offset + match_len - 1
end

return results
end

return M
Loading

0 comments on commit da8eee8

Please sign in to comment.