├── .github └── workflows │ ├── autolog.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README-KO.md ├── README.md ├── after └── plugin │ └── cmp-tw2css.lua ├── lua └── cmp-tw2css │ ├── generate.lua │ ├── init.lua │ ├── items.lua │ └── tw.json └── stylua.toml /.github/workflows/autolog.yml: -------------------------------------------------------------------------------- 1 | name: Autolog 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_call: 8 | 9 | jobs: 10 | generate-changelog: 11 | name: Generate Changelog 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Changelog 19 | uses: ardalanamini/auto-changelog@v3 20 | id: changelog 21 | with: 22 | github-token: ${{ secrets.GITHUB_TOKEN }} 23 | commit-types: | 24 | breaking: Breaking Changes 25 | feat: New Features 26 | fix: Bug Fixes 27 | revert: Reverts 28 | perf: Performance Improvements 29 | refactor: Refactors 30 | deps: Dependencies 31 | docs: Documentation Changes 32 | style: Code Style Changes 33 | build: Build System 34 | ci: Continuous Integration 35 | test: Tests 36 | chore: Chores 37 | other: Other Changes 38 | default-commit-type: Default Type 39 | mention-authors: false 40 | mention-new-contributors: false 41 | include-compare: false 42 | semver: false 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | autolog: 10 | uses: ./.github/workflows/autolog.yml 11 | 12 | release: 13 | name: Create Release 14 | 15 | concurrency: github-release 16 | 17 | environment: 18 | name: release 19 | url: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }} 20 | 21 | runs-on: ubuntu-latest 22 | 23 | needs: 24 | - autolog 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | 30 | - name: Changelog 31 | uses: ardalanamini/auto-changelog@master 32 | id: changelog 33 | 34 | - name: Create Release 35 | uses: softprops/action-gh-release@v1 36 | with: 37 | body: | 38 | ${{ steps.changelog.outputs.changelog }} 39 | prerelease: ${{ steps.changelog.outputs.prerelease }} 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.CHANGELOG_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joohoon Cha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-KO.md: -------------------------------------------------------------------------------- 1 | # cmp-tw2css 2 | 3 | `cmp-tw2css`는 [`nvim-cmp`](https://github.com/hrsh7th/nvim-cmp) 엔진을 사용하여 [tailwindcss](https://tailwindcss.com) 클래스를 css 코드로 완성해주는 플러그인입니다. 4 | Neovim에 내장된 treesitter 라이브러리를 이용하여 파일 내의 css 코드 블록 여부를 확인하고 그에 따라 자동 완성 소스를 로드합니다. 5 | 6 | ## 기본 설정하기 7 | 8 | ### 플러그인을 설치하기 전에... 9 | 10 | `cmp-tw2css`는 `nvim-cmp` 엔진을 통해 코드를 제공합니다. 플러그인 사용을 위해서는 `nvim-cmp`가 neovim에 설치된 상태여야 합니다. `nvim-cmp`를 설치하는 방법은 [nvim-cmp Github repo](https://github.com/hrsh7th/nvim-cmp)를 참고해주세요. 11 | 12 | `cmp-tw2css`는 버전 1.0.0부터 css 파서(parser)를 이용하여 커서가 css 코드 블록 안에 있는지 확인합니다. 따라서 이 기능을 이용하기 위해서는 css 파서를 필요로 합니다. 더 자세한 내용은 [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter)에서 찾을 수 있습니다. 13 | 14 | ### 설치 방법 15 | 16 | 선호하는 플러그인 매니저를 사용하여 간편하게 설치할 수 있습니다. 이 페이지의 예시는 가장 대중적으로 사용되는 [`packer.nvim`](https://github.com/wbthomason/packer.nvim)를 통해 설치하는 방법을 서술하고 있습니다. 17 | 18 | ```lua 19 | use({ 20 | "hrsh7th/nvim-cmp", 21 | requires = { 22 | "jcha0713/cmp-tw2css", 23 | }, 24 | }) 25 | ``` 26 | 27 | `packer.nvim`을 통해 설치가 끝난 후에는 다음의 설정값을 `nvim-cmp`에 추가해주어야 합니다. 28 | 29 | ```lua 30 | require('cmp').setup { 31 | -- ... 32 | sources = { 33 | { name = 'cmp-tw2css' }, 34 | -- other sources ... 35 | }, 36 | -- ... 37 | } 38 | ``` 39 | 40 | ## 어떻게 사용하나요? 41 | 42 | 여기까지 설치를 무사히 완료했다면 `cmp-tw2css` 사용은 쉽습니다. CSS 블록을 포함하고 있는 파일을 열고 원하는 tailwindcss 클래스를 입력하면 CSS 코드로 변환이 가능합니다. 43 | 44 | ### 예시 45 | 46 | ![demo](https://user-images.githubusercontent.com/29053796/222915836-9b2e19d5-3ace-4419-b492-eb1b00b572ac.gif) 47 | 48 | - `*.css`: 49 | 50 | ```css 51 | body { 52 | /* flex -> display: flex; */ 53 | /* p-6 -> padding: 1.5rem; */ 54 | /* ... */ 55 | } 56 | ``` 57 | 58 | - `*.html`: 59 | 60 | ```html 61 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | ``` 75 | 76 | ## 변수 설정 77 | 78 | ```lua 79 | require('cmp-tw2css').setup({ ... }) 80 | ``` 81 | 82 | ### `fallback` 83 | 84 | **type**: `boolean` 85 | **default**: `true` 86 | 87 | ```lua 88 | { 89 | fallback = true 90 | } 91 | ``` 92 | 93 | treesitter 파서가 없을 때 자동 완성 소스를 로드할지 말지 정합니다. `true`로 설정하는 경우에는 css 파서 없이도 소스를 로드하지만 커서 위치에 관계없이 모든 상황에서 소스를 불러오게 됩니다. `false`로 설정하면 css 파서가 없는 상황에서는 완성 소스를 불러오지 않습니다. 94 | 95 | ## 사용 전에 알아야 할 주의 사항 96 | 97 | `cmp-tw2css`는 tailwindcss 공식 문서를 스크래핑하여 만든 파일을 통해 완성 소스를 제공합니다. 따라서 공식 문서에 내용이 추가되거나 제외되는 부분을 바로 반영하기가 힘듭니다. 또한 미리 만들어진 파일을 통해 제공되는 소스이기 때문에 사용자가 원하는대로 값을 수정하는 게 불가능합니다. Tailwindcss 문서가 제공하는 내용과 다른 부분을 찾게 된다면 언제든지 issue를 생성해주세요. 98 | 99 | 추가적으로 같은 줄에 `:`이 두 번 이상 나올 경우 완성 소스를 로드하지 못하는 한계가 있으며 입력하는 css 코드가 두 줄 이상일 경우에 탭을 자동으로 삽입하지 않는 소소한 단점이 있습니다. 100 | 101 | ## 추가될 기능들 102 | 103 | - [x] 커서가 css 코드 블록 안에 있을 때만 소스를 로드하기 104 | - [x] treesitter 기능을 사용자가 제어할 수 있게 하기 105 | - [x] 완성 소스를 고를 때 documentation 보여주기 106 | - [ ] LSP를 이용하여 웹 스크래핑 없이 소스 제공하기 107 | 108 | ## 참고한 프로젝트 109 | 110 | - [`cmp-emoji`](https://github.com/hrsh7th/cmp-emoji) 111 | - [`cmp-npm`](https://github.com/David-Kunz/cmp-npm) 112 | - [`nvim-treesitter`](https://github.com/nvim-treesitter/nvim-treesitter) 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmp-tw2css 2 | 3 | - [한국어 문서 읽으러 가기](README-KO.md) 4 | 5 | A source for [`nvim-cmp`](https://github.com/hrsh7th/nvim-cmp) to convert [tailwindcss](https://tailwindcss.com) classes to pure css codes. 6 | It uses treesitter to find out whether any css code block exists in the code and loads the completion source. 7 | 8 | ## Setup 9 | 10 | ### Prerequisites: 11 | 12 | `cmp-tw2css` uses `nvim-cmp` to provide the code. You first need to have `nvim-cmp` installed in your neovim. To install `nvim-cmp`, please visit the [nvim-cmp Github repo](https://github.com/hrsh7th/nvim-cmp). 13 | 14 | Since version 1.0.0, `cmp-tw2css` uses css parser to detect if the cursor is inside the css code block. To fully use this feature, you need to install the css parser through `nvim-treesitter`. For more information, please refer to [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter). 15 | 16 | ### Installation 17 | 18 | To install `cmp-tw2css`, I recommend using [`packer.nvim`](https://github.com/wbthomason/packer.nvim). 19 | 20 | ```lua 21 | use({ 22 | "hrsh7th/nvim-cmp", 23 | requires = { 24 | "jcha0713/cmp-tw2css", 25 | }, 26 | }) 27 | ``` 28 | 29 | And to add source, go to `nvim-cmp` configuration and add the following 30 | 31 | ```lua 32 | require('cmp').setup { 33 | -- ... 34 | sources = { 35 | { name = 'cmp-tw2css' }, 36 | -- other sources ... 37 | }, 38 | -- ... 39 | } 40 | ``` 41 | 42 | ## Usage 43 | 44 | Using `cmp-tw2css` is simple. Open any file that contains css code blocks and start typing tailwindcss classes that you want to convert into css codes. 45 | 46 | ### Example 47 | 48 | ![demo](https://user-images.githubusercontent.com/29053796/222915836-9b2e19d5-3ace-4419-b492-eb1b00b572ac.gif) 49 | 50 | - `*.css`: 51 | 52 | ```css 53 | body { 54 | /* flex -> display: flex; */ 55 | /* p-6 -> padding: 1.5rem; */ 56 | /* ... */ 57 | } 58 | ``` 59 | 60 | - `*.html`: 61 | 62 | ```html 63 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 75 | 76 | ``` 77 | 78 | ## Configuration 79 | 80 | ```lua 81 | require('cmp-tw2css').setup({ ... }) 82 | ``` 83 | 84 | ### `fallback` 85 | 86 | **type**: `boolean` 87 | **default**: `true` 88 | 89 | ```lua 90 | { 91 | fallback = true 92 | } 93 | ``` 94 | 95 | Determines whether to load the completion items when there is no treesitter parser. If this is set to `true`, you are allowing `cmp-tw2css` to load the completion items regardless of your cursor position. If it is set to `false`, then it simply does not load anything when there is no css parser. 96 | 97 | ## Limitation 98 | 99 | There are a number of limitations to `cmp-tw2css`. First, the source of this plugin is a result of web scraping. This means that you might find some items are missing while using. If this happens to you, please let me know by submitting an issue so that I can update the source accordingly. Another downside is that it can't be dynamically generated and only provides the code from the official website. 100 | 101 | Currently `cmp-tw2css` does not automatically add tabs for the additional lines when the insert text is more than one line. And also when there are more than one colon(`:`) in a line, `cmp-tw2css` cannot properly load its completion source. 102 | 103 | ## Roadmap 104 | 105 | - [x] Load the source only when the cursor is inside the code block 106 | - [x] Provide ways to configure with treesitter 107 | - [x] Show documentation when selecting an item 108 | - [ ] Provide the completion source using LSP functionality 109 | 110 | ## Credit 111 | 112 | - [`cmp-emoji`](https://github.com/hrsh7th/cmp-emoji) 113 | - [`cmp-npm`](https://github.com/David-Kunz/cmp-npm) 114 | - [`nvim-treesitter`](https://github.com/nvim-treesitter/nvim-treesitter) 115 | -------------------------------------------------------------------------------- /after/plugin/cmp-tw2css.lua: -------------------------------------------------------------------------------- 1 | local ok, cmp = pcall(require, "cmp") 2 | if ok then 3 | cmp.register_source("cmp-tw2css", require("cmp-tw2css").new()) 4 | end 5 | -------------------------------------------------------------------------------- /lua/cmp-tw2css/generate.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M._read = function(path) 4 | return vim.fn.json_decode(vim.fn.readfile(path)) 5 | end 6 | 7 | M._write = function(path, data) 8 | local writer = io.open(path, "w") 9 | writer:write(data) 10 | io.close(writer) 11 | end 12 | 13 | M.to_item = function(key, value) 14 | return ( 15 | "{ word = '%s', label = '%s', insertText = [[%s]], filterText = '%s', detail = [[%s]] };\n" 16 | ):format(key, key, value, key, value) 17 | end 18 | 19 | M.update = function() 20 | -- load json file 21 | local classTable = vim.fn.json_decode( 22 | vim.fn.readfile(vim.fn.expand("%:p:h") .. "/tw.json") 23 | ) 24 | 25 | local items = "" 26 | for key, value in pairs(classTable) do 27 | items = items .. M.to_item(key, value) 28 | end 29 | M._write("./items.lua", ("return function() return {\n%s} end"):format(items)) 30 | end 31 | 32 | M.update() 33 | 34 | return M 35 | -------------------------------------------------------------------------------- /lua/cmp-tw2css/init.lua: -------------------------------------------------------------------------------- 1 | local source = {} 2 | local opt = { 3 | fallback = true, 4 | } 5 | 6 | ---@class cmp-tw2css.source 7 | ---@field public is_sorted boolean 8 | ---@field public items table 9 | source.new = function() 10 | local self = setmetatable({}, { __index = source }) 11 | self.is_sorted = false 12 | self.items = {} 13 | return self 14 | end 15 | 16 | ---@param user_opt table 17 | source.setup = function(user_opt) 18 | if user_opt then 19 | vim.validate({ fallback = { user_opt.fallback, "boolean" } }) 20 | opt = vim.tbl_deep_extend("force", opt, user_opt) 21 | end 22 | end 23 | 24 | --- Checks if the lang is css or scss 25 | ---@param lang string 26 | ---@return boolean 27 | local function is_stylesheet(lang) 28 | return lang == "css" or lang == "scss" 29 | end 30 | 31 | --- Get the language set to the buffer 32 | ---@param bufnr number 33 | local function get_buf_lang(bufnr) 34 | bufnr = bufnr or vim.api.nvim_get_current_buf() 35 | return vim.api.nvim_buf_get_option(bufnr, "ft") 36 | end 37 | 38 | ---@return boolean 39 | local function iter_tree(tree) 40 | local lang = tree:lang() 41 | 42 | if is_stylesheet(lang) then 43 | return true 44 | end 45 | 46 | if tree:children() ~= nil then 47 | for child_lang, child_tree in pairs(tree:children()) do 48 | if is_stylesheet(child_lang) then 49 | return true 50 | else 51 | return iter_tree(child_tree) 52 | end 53 | end 54 | end 55 | 56 | return false 57 | end 58 | 59 | --- Return whether this source is available in the current context or not. 60 | ---@return boolean 61 | function source:is_available() 62 | local bufnr = vim.api.nvim_get_current_buf() or 0 63 | local buf_lang = get_buf_lang(bufnr) 64 | local ok, tree = pcall(vim.treesitter.get_parser, bufnr, buf_lang) 65 | 66 | -- if there is no treesitter parser then look at the file extension 67 | if not ok then 68 | local filename = vim.fn.expand("%:e") 69 | if not filename then 70 | return false 71 | end 72 | return is_stylesheet(filename) 73 | end 74 | 75 | return iter_tree(tree) 76 | end 77 | 78 | --- Return the debug name of this source. 79 | ---@return string 80 | function source:get_debug_name() 81 | return "cmp-tw2css" 82 | end 83 | 84 | --- Copied from nvim-treesitter.ts_utils 85 | --- Determines whether (line, col) position is in node range 86 | -- @param node Node defining the range 87 | -- @param line A line (0-based) 88 | -- @param col A column (0-based) 89 | local function is_in_node_range(node, line, col) 90 | local start_line, start_col, end_line, end_col = node:range() 91 | if line >= start_line and line <= end_line then 92 | if line == start_line and line == end_line then 93 | return col >= start_col and col < end_col 94 | elseif line == start_line then 95 | return col >= start_col 96 | elseif line == end_line then 97 | return col < end_col 98 | else 99 | return true 100 | end 101 | else 102 | return false 103 | end 104 | end 105 | 106 | --- Copied from nvim-treesitter.ts_utils 107 | --- Get the root node of the tree 108 | local function get_root_for_position(line, col, root_lang_tree) 109 | local lang_tree = root_lang_tree:language_for_range({ line, col, line, col }) 110 | 111 | for _, tree in ipairs(lang_tree:trees()) do 112 | local root = tree:root() 113 | 114 | if root and is_in_node_range(root, line, col) then 115 | return root, tree, lang_tree 116 | end 117 | end 118 | 119 | return nil, nil, lang_tree 120 | end 121 | 122 | -- Add kind to the items 123 | function source:add_kind() 124 | local KIND = require("cmp").lsp.CompletionItemKind 125 | for _, item in ipairs(self.items) do 126 | local kind = KIND.Constant 127 | for _, val in ipairs({ "color:", "stroke:", "fill:" }) do 128 | if (item.insertText):match(val) then 129 | kind = KIND.Color 130 | break 131 | end 132 | end 133 | item.kind = kind 134 | end 135 | end 136 | 137 | --- Get the items table and sort the items 138 | --- If the table is already sorted, then simply return it 139 | ---@return table items 140 | function source:get_sorted_items() 141 | if not self.is_sorted then 142 | -- if items table is not sorted, then sort it 143 | self.items = require("cmp-tw2css.items")() 144 | table.sort(self.items, function(a, b) 145 | return a.label < b.label 146 | end) 147 | 148 | source:add_kind() 149 | 150 | -- the table is now sorted 151 | self.is_sorted = true 152 | end 153 | 154 | return self.items 155 | end 156 | 157 | --- Add documentation before an item is displayed 158 | --@param completion_item CompletionItem 159 | --@param callback function 160 | function source:resolve(completion_item, callback) 161 | completion_item.detail = nil 162 | 163 | -- local indent = vim.api.nvim_buf_get_option(0, "shiftwidth") 164 | -- local newline_with_indent = "" 165 | -- for i = 1, indent do 166 | -- newline_with_indent = newline_with_indent .. " " 167 | -- end 168 | 169 | -- just use 2 spaces for indent 170 | completion_item.documentation = { 171 | kind = require("cmp").lsp.MarkupKind.Markdown, 172 | value = ("```css\n.%s {\n %s\n}\n```"):format( 173 | completion_item.word, 174 | string.gsub(completion_item.insertText, "\n", "\n ") 175 | ), 176 | } 177 | 178 | callback(completion_item) 179 | end 180 | 181 | --- Invoke completion (required). 182 | ---@param params cmp.SourceCompletionApiParams 183 | ---@param callback fun(response: lsp.CompletionResponse|nil) 184 | function source:complete(params, callback) 185 | local bufnr = vim.api.nvim_get_current_buf() or 0 186 | local buf_lang = get_buf_lang(bufnr) 187 | local ok, root_lang_tree = pcall(vim.treesitter.get_parser, bufnr, buf_lang) 188 | 189 | if not ok then 190 | if opt.fallback then 191 | local items = source:get_sorted_items() 192 | callback(items) 193 | else 194 | callback() 195 | end 196 | return 197 | end 198 | 199 | local cursor = params.context.cursor 200 | local cur_line = params.context.cursor_line 201 | 202 | local root = get_root_for_position(cursor.row, cursor.col, root_lang_tree) 203 | 204 | if not root then 205 | callback() 206 | return 207 | end 208 | 209 | local node_at_cursor = root:named_descendant_for_range( 210 | cursor.row - 1, 211 | cursor.col - 1, 212 | cursor.row - 1, 213 | cursor.col - 1 214 | ) 215 | 216 | -- prevent loading items when the cursor is after the colon 217 | --- TODO: Allow users to write single line css 218 | local in_property_name = true 219 | local idx_colon = string.find(cur_line, ":%s*(%w+)") 220 | if idx_colon then 221 | if cursor.col > idx_colon then 222 | in_property_name = false 223 | end 224 | end 225 | 226 | local node_at_cursor_type = node_at_cursor:type() 227 | 228 | if node_at_cursor_type == "block" or node_at_cursor_type == "declaration" then 229 | if in_property_name then 230 | local items = source:get_sorted_items() 231 | callback(items) 232 | return 233 | end 234 | end 235 | callback() 236 | end 237 | 238 | -- require("cmp").register_source("cmp-tw2css", source.new()) 239 | 240 | return source 241 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | column_width = 80 4 | quote_style = "AutoPreferDouble" 5 | no_call_parentheses = false 6 | --------------------------------------------------------------------------------