├── .eslintrc.js
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── customData
├── alpinejs
│ └── html.json
└── petite-vue
│ └── html.json
├── esbuild.js
├── package.json
├── src
├── commands.ts
├── completion.ts
└── index.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | },
5 | parser: '@typescript-eslint/parser',
6 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
7 | rules: {
8 | '@typescript-eslint/no-unused-vars': 'warn',
9 | '@typescript-eslint/ban-ts-comment': 'off',
10 | '@typescript-eslint/no-explicit-any': 'off',
11 | '@typescript-eslint/no-non-null-assertion': 'off',
12 | '@typescript-eslint/no-namespace': 'off',
13 | '@typescript-eslint/no-empty-function': 'off',
14 | '@typescript-eslint/explicit-function-return-type': 'off',
15 | '@typescript-eslint/explicit-module-boundary-types': 'off',
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### https://raw.github.com/github/gitignore/218a941be92679ce67d0484547e3e142b2f5f6f0/Node.gitignore
2 |
3 | lib/
4 | _ref
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # Diagnostic reports (https://nodejs.org/api/report.html)
15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # nyc test coverage
31 | .nyc_output
32 |
33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34 | .grunt
35 |
36 | # Bower dependency directory (https://bower.io/)
37 | bower_components
38 |
39 | # node-waf configuration
40 | .lock-wscript
41 |
42 | # Compiled binary addons (https://nodejs.org/api/addons.html)
43 | build/Release
44 |
45 | # Dependency directories
46 | node_modules/
47 | jspm_packages/
48 |
49 | # Snowpack dependency directory (https://snowpack.dev/)
50 | web_modules/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Microbundle cache
62 | .rpt2_cache/
63 | .rts2_cache_cjs/
64 | .rts2_cache_es/
65 | .rts2_cache_umd/
66 |
67 | # Optional REPL history
68 | .node_repl_history
69 |
70 | # Output of 'npm pack'
71 | *.tgz
72 |
73 | # Yarn Integrity file
74 | .yarn-integrity
75 |
76 | # dotenv environment variables file
77 | .env
78 | .env.test
79 |
80 | # parcel-bundler cache (https://parceljs.org/)
81 | .cache
82 | .parcel-cache
83 |
84 | # Next.js build output
85 | .next
86 | out
87 |
88 | # Nuxt.js build / generate output
89 | .nuxt
90 | dist
91 |
92 | # Gatsby files
93 | .cache/
94 | # Comment in the public line in if your project uses Gatsby and not Next.js
95 | # https://nextjs.org/blog/next-9-1#public-directory-support
96 | # public
97 |
98 | # vuepress build output
99 | .vuepress/dist
100 |
101 | # Serverless directories
102 | .serverless/
103 |
104 | # FuseBox cache
105 | .fusebox/
106 |
107 | # DynamoDB Local files
108 | .dynamodb/
109 |
110 | # TernJS port file
111 | .tern-port
112 |
113 | # Stores VSCode versions used for testing VSCode extensions
114 | .vscode-test
115 |
116 | # yarn v2
117 | .yarn/cache
118 | .yarn/unplugged
119 | .yarn/build-state.yml
120 | .yarn/install-state.gz
121 | .pnp.*
122 |
123 |
124 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | node_modules
3 | tsconfig.json
4 | *.map
5 | .tags
6 | .DS_Store
7 | webpack.config.js
8 | esbuild.js
9 | yarn.lock
10 | yarn-error.log
11 | .github
12 | .eslintrc.js
13 | .prettierrc
14 | _ref
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 yaegassy
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.md:
--------------------------------------------------------------------------------
1 | # coc-html-css-support
2 |
3 | > fork from a [ecmel/vscode-html-css](https://github.com/ecmel/vscode-html-css) | [HTML CSS Support](https://marketplace.visualstudio.com/items?itemName=ecmel.vscode-html-css)
4 |
5 | HTML id and class attribute "completion" for [coc.nvim](https://github.com/neoclide/coc.nvim).
6 |
7 |
8 |
9 | ## Install
10 |
11 | `:CocInstall coc-html-css-support`
12 |
13 | ## Features
14 |
15 | - HTML id and class attribute completion.
16 | - Supports linked and embedded style sheets.
17 | - Supports template inheritance.
18 | - Supports additional style sheets.
19 | - Supports other HTML like languages.
20 | - Command to make `html.customData` built-in in `coc-html-css-support` available at the workspace level.
21 | - Require [coc-html](https://github.com/neoclide/coc-html)
22 |
23 | ## Configuration options
24 |
25 | - `html-css-support.enable`: Enable coc-html-css-support extension, default: `true`
26 | - `html-css-support.enabledLanguages`: List of languages which suggestions are desired, default: `["html"]`
27 | - `html-css-support.styleSheets`: List of local or remote style sheets for suggestions, default: `[]`
28 |
29 | ## Commands
30 |
31 | - `html-css-support.dispose`: Clear cache and reload the stylesheet
32 | - `html-css-support.customDataSetup`: Setup `html.customData` in workspace config. Supported customData are as follows
33 | - `Alpine.js`
34 | - `petite-vue`
35 |
36 | ## Example settings
37 |
38 | ### Additional Style Sheets (Example)
39 |
40 | **coc-settings.json:**
41 |
42 | ```json
43 | {
44 | "html-css-support.styleSheets": [
45 | "https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css",
46 | "/style.css",
47 | "style.css"
48 | ]
49 | }
50 | ```
51 |
52 | ### Add other HTML like languages (Example)
53 |
54 | **coc-settings.json:**
55 |
56 | ```json
57 | {
58 | "html-css-support.enabledLanguages": [
59 | "html",
60 | "vue",
61 | "blade",
62 | "htmldjango",
63 | "typescriptreact",
64 | "javascriptreact"
65 | ]
66 | }
67 | ```
68 |
69 | ## What is customData?
70 |
71 | You can read more about customData in the following repositories.
72 |
73 | -
74 | -
75 |
76 | ## Thanks
77 |
78 | - [ecmel/vscode-html-css](https://github.com/ecmel/vscode-html-css) : The origin of this repository.
79 |
80 | ## License
81 |
82 | MIT
83 |
84 | ---
85 |
86 | > This extension is built with [create-coc-extension](https://github.com/fannheyward/create-coc-extension)
87 |
--------------------------------------------------------------------------------
/customData/alpinejs/html.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.1",
3 | "tags": [],
4 | "globalAttributes": [
5 | {
6 | "name": "x-data",
7 | "description": "Alpine.js"
8 | },
9 | {
10 | "name": "x-init",
11 | "description": "Alpine.js"
12 | },
13 | {
14 | "name": "x-show",
15 | "description": "Alpine.js"
16 | },
17 | {
18 | "name": "x-bind:",
19 | "description": "Alpine.js"
20 | },
21 | {
22 | "name": "x-on:",
23 | "description": "Alpine.js"
24 | },
25 | {
26 | "name": "x-model",
27 | "description": "Alpine.js"
28 | },
29 | {
30 | "name": "x-text",
31 | "description": "Alpine.js"
32 | },
33 | {
34 | "name": "x-html",
35 | "description": "Alpine.js"
36 | },
37 | {
38 | "name": "x-ref",
39 | "description": "Alpine.js"
40 | },
41 | {
42 | "name": "x-if",
43 | "description": "Alpine.js"
44 | },
45 | {
46 | "name": "x-for",
47 | "description": "Alpine.js"
48 | },
49 | {
50 | "name": ":key",
51 | "description": "Alpine.js"
52 | },
53 | {
54 | "name": "x-transition:enter",
55 | "description": "Alpine.js"
56 | },
57 | {
58 | "name": "x-transition:enter-start",
59 | "description": "Alpine.js"
60 | },
61 | {
62 | "name": "x-transition:enter-end",
63 | "description": "Alpine.js"
64 | },
65 | {
66 | "name": "x-transition:leave",
67 | "description": "Alpine.js"
68 | },
69 | {
70 | "name": "x-transition:leave-start",
71 | "description": "Alpine.js"
72 | },
73 | {
74 | "name": "x-transition:leave-end",
75 | "description": "Alpine.js"
76 | },
77 | {
78 | "name": "x-cloak",
79 | "description": "Alpine.js"
80 | },
81 | {
82 | "name": "x-on:abort",
83 | "description": "Alpine.js"
84 | },
85 | {
86 | "name": "x-on:blur",
87 | "description": "Alpine.js"
88 | },
89 | {
90 | "name": "x-on:canplay",
91 | "description": "Alpine.js"
92 | },
93 | {
94 | "name": "x-on:canplaythrough",
95 | "description": "Alpine.js"
96 | },
97 | {
98 | "name": "x-on:change",
99 | "description": "Alpine.js"
100 | },
101 | {
102 | "name": "x-on:click",
103 | "description": "Alpine.js"
104 | },
105 | {
106 | "name": "x-on:contextmenu",
107 | "description": "Alpine.js"
108 | },
109 | {
110 | "name": "x-on:dblclick",
111 | "description": "Alpine.js"
112 | },
113 | {
114 | "name": "x-on:drag",
115 | "description": "Alpine.js"
116 | },
117 | {
118 | "name": "x-on:dragend",
119 | "description": "Alpine.js"
120 | },
121 | {
122 | "name": "x-on:dragenter",
123 | "description": "Alpine.js"
124 | },
125 | {
126 | "name": "x-on:dragleave",
127 | "description": "Alpine.js"
128 | },
129 | {
130 | "name": "x-on:dragover",
131 | "description": "Alpine.js"
132 | },
133 | {
134 | "name": "x-on:dragstart",
135 | "description": "Alpine.js"
136 | },
137 | {
138 | "name": "x-on:drop",
139 | "description": "Alpine.js"
140 | },
141 | {
142 | "name": "x-on:durationchange",
143 | "description": "Alpine.js"
144 | },
145 | {
146 | "name": "x-on:emptied",
147 | "description": "Alpine.js"
148 | },
149 | {
150 | "name": "x-on:ended",
151 | "description": "Alpine.js"
152 | },
153 | {
154 | "name": "x-on:error",
155 | "description": "Alpine.js"
156 | },
157 | {
158 | "name": "x-on:focus",
159 | "description": "Alpine.js"
160 | },
161 | {
162 | "name": "x-on:input",
163 | "description": "Alpine.js"
164 | },
165 | {
166 | "name": "x-on:invalid",
167 | "description": "Alpine.js"
168 | },
169 | {
170 | "name": "x-on:keydown",
171 | "description": "Alpine.js"
172 | },
173 | {
174 | "name": "x-on:keypress",
175 | "description": "Alpine.js"
176 | },
177 | {
178 | "name": "x-on:keyup",
179 | "description": "Alpine.js"
180 | },
181 | {
182 | "name": "x-on:load",
183 | "description": "Alpine.js"
184 | },
185 | {
186 | "name": "x-on:loadeddata",
187 | "description": "Alpine.js"
188 | },
189 | {
190 | "name": "x-on:loadedmetadata",
191 | "description": "Alpine.js"
192 | },
193 | {
194 | "name": "x-on:loadstart",
195 | "description": "Alpine.js"
196 | },
197 | {
198 | "name": "x-on:mousedown",
199 | "description": "Alpine.js"
200 | },
201 | {
202 | "name": "x-on:mousemove",
203 | "description": "Alpine.js"
204 | },
205 | {
206 | "name": "x-on:mouseout",
207 | "description": "Alpine.js"
208 | },
209 | {
210 | "name": "x-on:mouseover",
211 | "description": "Alpine.js"
212 | },
213 | {
214 | "name": "x-on:mouseup",
215 | "description": "Alpine.js"
216 | },
217 | {
218 | "name": "x-on:pause",
219 | "description": "Alpine.js"
220 | },
221 | {
222 | "name": "x-on:play",
223 | "description": "Alpine.js"
224 | },
225 | {
226 | "name": "x-on:playing",
227 | "description": "Alpine.js"
228 | },
229 | {
230 | "name": "x-on:progress",
231 | "description": "Alpine.js"
232 | },
233 | {
234 | "name": "x-on:ratechange",
235 | "description": "Alpine.js"
236 | },
237 | {
238 | "name": "x-on:reset",
239 | "description": "Alpine.js"
240 | },
241 | {
242 | "name": "x-on:resize",
243 | "description": "Alpine.js"
244 | },
245 | {
246 | "name": "x-on:readystatechange",
247 | "description": "Alpine.js"
248 | },
249 | {
250 | "name": "x-on:scroll",
251 | "description": "Alpine.js"
252 | },
253 | {
254 | "name": "x-on:seeked",
255 | "description": "Alpine.js"
256 | },
257 | {
258 | "name": "x-on:seeking",
259 | "description": "Alpine.js"
260 | },
261 | {
262 | "name": "x-on:select",
263 | "description": "Alpine.js"
264 | },
265 | {
266 | "name": "x-on:show",
267 | "description": "Alpine.js"
268 | },
269 | {
270 | "name": "x-on:stalled",
271 | "description": "Alpine.js"
272 | },
273 | {
274 | "name": "x-on:submit",
275 | "description": "Alpine.js"
276 | },
277 | {
278 | "name": "x-on:suspend",
279 | "description": "Alpine.js"
280 | },
281 | {
282 | "name": "x-on:timeupdate",
283 | "description": "Alpine.js"
284 | },
285 | {
286 | "name": "x-on:volumechange",
287 | "description": "Alpine.js"
288 | },
289 | {
290 | "name": "x-on:waiting",
291 | "description": "Alpine.js"
292 | },
293 | {
294 | "name": "x-bind:class",
295 | "description": "Alpine.js"
296 | },
297 | {
298 | "name": "x-bind:disabled",
299 | "description": "Alpine.js"
300 | },
301 | {
302 | "name": "x-bind:readonly",
303 | "description": "Alpine.js"
304 | },
305 | {
306 | "name": "x-bind:required",
307 | "description": "Alpine.js"
308 | },
309 | {
310 | "name": "x-bind:checked",
311 | "description": "Alpine.js"
312 | },
313 | {
314 | "name": "x-bind:hidden",
315 | "description": "Alpine.js"
316 | },
317 | {
318 | "name": "x-bind:selected",
319 | "description": "Alpine.js"
320 | }
321 | ],
322 | "valueSets": []
323 | }
324 |
--------------------------------------------------------------------------------
/customData/petite-vue/html.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.1",
3 | "tags": [],
4 | "globalAttributes": [
5 | {
6 | "name": "v-scope",
7 | "description": "petite-vue"
8 | },
9 | {
10 | "name": "v-effect",
11 | "description": "petite-vue"
12 | },
13 | {
14 | "name": "@mounted",
15 | "description": "petite-vue"
16 | },
17 | {
18 | "name": "@unmounted",
19 | "description": "petite-vue"
20 | },
21 | {
22 | "name": "v-bind:",
23 | "description": "petite-vue"
24 | },
25 | {
26 | "name": "v-on:",
27 | "description": "petite-vue"
28 | },
29 | {
30 | "name": "v-model",
31 | "description": "petite-vue"
32 | },
33 | {
34 | "name": "v-if",
35 | "description": "petite-vue"
36 | },
37 | {
38 | "name": "v-else",
39 | "description": "petite-vue"
40 | },
41 | {
42 | "name": "v-else-if",
43 | "description": "petite-vue"
44 | },
45 | {
46 | "name": "v-for",
47 | "description": "petite-vue"
48 | },
49 | {
50 | "name": "v-show",
51 | "description": "petite-vue"
52 | },
53 | {
54 | "name": "v-html",
55 | "description": "petite-vue"
56 | },
57 | {
58 | "name": "v-text",
59 | "description": "petite-vue"
60 | },
61 | {
62 | "name": "v-pre",
63 | "description": "petite-vue"
64 | },
65 | {
66 | "name": "v-once",
67 | "description": "petite-vue"
68 | },
69 | {
70 | "name": "v-cloak",
71 | "description": "petite-vue"
72 | }
73 | ],
74 | "valueSets": []
75 | }
76 |
--------------------------------------------------------------------------------
/esbuild.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | async function start(watch) {
3 | await require('esbuild').build({
4 | entryPoints: ['src/index.ts'],
5 | bundle: true,
6 | watch,
7 | minify: process.env.NODE_ENV === 'production',
8 | sourcemap: process.env.NODE_ENV === 'development',
9 | mainFields: ['module', 'main'],
10 | external: ['coc.nvim'],
11 | platform: 'node',
12 | target: 'node14.14',
13 | outfile: 'lib/index.js',
14 | });
15 | }
16 |
17 | let watch = false;
18 | if (process.argv.length > 2 && process.argv[2] === '--watch') {
19 | console.log('watching...');
20 | watch = {
21 | onRebuild(error) {
22 | if (error) {
23 | console.error('watch build failed:', error);
24 | } else {
25 | console.log('watch build succeeded');
26 | }
27 | },
28 | };
29 | }
30 |
31 | start(watch).catch((e) => {
32 | console.error(e);
33 | });
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coc-html-css-support",
3 | "version": "0.5.3",
4 | "description": "HTML id and class attribute completion for coc.nvim",
5 | "author": "yaegassy ",
6 | "license": "MIT",
7 | "main": "lib/index.js",
8 | "keywords": [
9 | "coc.nvim",
10 | "vim",
11 | "neovim",
12 | "css",
13 | "html",
14 | "twig",
15 | "blade",
16 | "django",
17 | "nunjucks",
18 | "mustache",
19 | "angular",
20 | "react",
21 | "vue",
22 | "lit",
23 | "multi-root ready"
24 | ],
25 | "engines": {
26 | "coc": "^0.0.80"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "https://github.com/yaegassy/coc-html-css-support"
31 | },
32 | "scripts": {
33 | "lint": "eslint src --ext ts",
34 | "clean": "rimraf lib",
35 | "watch": "node esbuild.js --watch",
36 | "build": "node esbuild.js",
37 | "prepare": "node esbuild.js"
38 | },
39 | "prettier": {
40 | "singleQuote": true,
41 | "printWidth": 120,
42 | "semi": true
43 | },
44 | "devDependencies": {
45 | "@types/css-tree": "^1.0.5",
46 | "@types/node": "^20.10.5",
47 | "@types/node-fetch": "^2.5.8",
48 | "@typescript-eslint/eslint-plugin": "^6.15.0",
49 | "@typescript-eslint/parser": "^6.15.0",
50 | "coc.nvim": "0.0.83-next.17",
51 | "css-tree": "1.1.2",
52 | "esbuild": "^0.16.17",
53 | "eslint": "^8.56.0",
54 | "eslint-config-prettier": "^9.1.0",
55 | "eslint-plugin-prettier": "^5.1.2",
56 | "node-fetch": "^2.6.1",
57 | "prettier": "^3.1.1",
58 | "rimraf": "^5.0.1",
59 | "typescript": "5.3.3"
60 | },
61 | "activationEvents": [
62 | "*"
63 | ],
64 | "contributes": {
65 | "configuration": {
66 | "type": "object",
67 | "title": "coc-html-css-support configuration",
68 | "properties": {
69 | "html-css-support.enable": {
70 | "type": "boolean",
71 | "default": true,
72 | "description": "Enable coc-html-css-support extension"
73 | },
74 | "html-css-support.enabledLanguages": {
75 | "type": "array",
76 | "description": "List of languages which suggestions are desired.",
77 | "default": [
78 | "html"
79 | ]
80 | },
81 | "html-css-support.styleSheets": {
82 | "type": "array",
83 | "description": "List of local or remote style sheets for suggestions.",
84 | "default": []
85 | }
86 | }
87 | },
88 | "commands": [
89 | {
90 | "command": "html-css-support.dispose",
91 | "title": "Clear cache and reload the stylesheet"
92 | },
93 | {
94 | "command": "html-css-support.customDataSetup",
95 | "title": "Setup `html.customData` to be used in the workspace"
96 | }
97 | ]
98 | },
99 | "dependencies": {},
100 | "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
101 | }
102 |
--------------------------------------------------------------------------------
/src/commands.ts:
--------------------------------------------------------------------------------
1 | import { ExtensionContext, extensions, window, workspace } from 'coc.nvim';
2 | import path from 'path';
3 |
4 | export function customDataSetupCommand(context: ExtensionContext) {
5 | return async () => {
6 | if (!extensions.all.find((e) => e.id === 'coc-html')) {
7 | window.showWarningMessage(`coc-html is not installed`);
8 | return;
9 | }
10 |
11 | const htmlConfig = workspace.getConfiguration('html');
12 | const picked = await window.showMenuPicker(['Alpine.js', 'petite-vue'], 'Which customData do you want to use?');
13 |
14 | switch (picked) {
15 | case -1:
16 | // Cancel!
17 | window.showInformationMessage(`It's been cancelled`);
18 | break;
19 | case 0:
20 | // Alpine.js
21 | htmlConfig.update('customData', [path.join(context.extensionPath, 'customData', 'alpinejs', 'html.json')]);
22 | break;
23 | case 1:
24 | // petite-vue
25 | htmlConfig.update('customData', [path.join(context.extensionPath, 'customData', 'petite-vue', 'html.json')]);
26 | break;
27 | default:
28 | // Cancel!
29 | window.showInformationMessage(`It's been cancelled`);
30 | break;
31 | }
32 |
33 | if (picked !== -1) {
34 | workspace.nvim.command(`CocRestart`, true);
35 | }
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/src/completion.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CompletionItem,
3 | CompletionItemKind,
4 | CompletionItemProvider,
5 | Diagnostic,
6 | DiagnosticSeverity,
7 | Disposable,
8 | Position,
9 | ProviderResult,
10 | Range,
11 | TextDocument,
12 | Uri,
13 | workspace,
14 | } from 'coc.nvim';
15 | import { parse, walk } from 'css-tree';
16 | import fetch from 'node-fetch';
17 | import { basename, dirname, extname, isAbsolute, join } from 'path';
18 |
19 | export type Context = {
20 | ids: Map;
21 | classes: Map;
22 | };
23 |
24 | export class SelectorCompletionItemProvider implements CompletionItemProvider, Disposable {
25 | readonly start = Position.create(0, 0);
26 | readonly cache = new Map();
27 | readonly files = new Map();
28 | readonly watchers = new Map();
29 | readonly isRemote = /^https?:\/\//i;
30 | readonly canComplete = /(id|class|className)\s*[=:]\s*("|')(?:(?!\2).)*$/is;
31 | readonly findLinkRel = /rel\s*=\s*("|')((?:(?!\1).)+)\1/is;
32 | readonly findLinkHref = /href\s*=\s*("|')((?:(?!\1).)+)\1/is;
33 | readonly findExtended = /(?:{{<|{{>|{%\s*extends|@extends\s*\()\s*("|')?([./A-Za-z_0-9\\\-]+)\1\s*(?:\)|%}|}})/i;
34 |
35 | dispose() {
36 | this.watchers.forEach((e) => e.dispose());
37 | this.watchers.clear();
38 | this.cache.clear();
39 | this.files.clear();
40 | }
41 |
42 | watchFile(path: string, listener: () => any) {
43 | if (this.watchers.has(path)) {
44 | return;
45 | }
46 |
47 | const watcher = workspace.createFileSystemWatcher(path);
48 |
49 | watcher.onDidCreate(listener);
50 | watcher.onDidChange(listener);
51 | watcher.onDidDelete(listener);
52 |
53 | this.watchers.set(path, watcher);
54 | }
55 |
56 | getStyleSheets(uri: Uri): string[] {
57 | return workspace.getConfiguration('html-css-support', uri.toString()).get('styleSheets', []);
58 | }
59 |
60 | getPath(uri: Uri, path: string, ext?: string): string {
61 | const folder = workspace.getWorkspaceFolder(uri.toString());
62 | const name = ext ? join(dirname(path), basename(path, ext) + ext) : path;
63 |
64 | return folder ? join(isAbsolute(path) ? folder.uri : dirname(uri.fsPath), name) : join(dirname(uri.fsPath), name);
65 | }
66 |
67 | parseTextToItems(path: string, text: string, items: CompletionItem[]) {
68 | walk(parse(text), (node) => {
69 | let kind: CompletionItemKind;
70 |
71 | switch (node.type) {
72 | case 'ClassSelector':
73 | kind = CompletionItemKind.Enum;
74 | break;
75 | case 'IdSelector':
76 | kind = CompletionItemKind.Value;
77 | break;
78 | default:
79 | return;
80 | }
81 |
82 | const resultCompletionItem: CompletionItem = {
83 | label: node.name,
84 | kind,
85 | detail: 'filename: ' + path,
86 | };
87 |
88 | items.push(resultCompletionItem);
89 | });
90 | }
91 |
92 | async fetchLocal(path: string): Promise {
93 | if (this.cache.has(path)) {
94 | return;
95 | }
96 |
97 | const items: CompletionItem[] = [];
98 |
99 | try {
100 | const content = await workspace.readFile(path);
101 | this.parseTextToItems(basename(path), content.toString(), items);
102 | } catch (error) {}
103 |
104 | this.cache.set(path, items);
105 | this.watchFile(path, () => this.cache.delete(path));
106 | }
107 |
108 | async fetchRemote(path: string): Promise {
109 | if (this.cache.has(path)) {
110 | return;
111 | }
112 |
113 | const items: CompletionItem[] = [];
114 |
115 | try {
116 | const res = await fetch(path);
117 |
118 | if (res.ok) {
119 | const text = await res.text();
120 | this.parseTextToItems(basename(path), text, items);
121 | }
122 | } catch (error) {}
123 |
124 | this.cache.set(path, items);
125 | }
126 |
127 | async fetch(uri: Uri, path: string): Promise {
128 | if (this.isRemote.test(path)) {
129 | await this.fetchRemote(path);
130 | } else {
131 | const base = basename(uri.fsPath, extname(uri.fsPath));
132 |
133 | path = this.getPath(uri, path.replace(/\${\s*fileBasenameNoExtension\s*}/, base));
134 | await this.fetchLocal(path);
135 | }
136 |
137 | return path;
138 | }
139 |
140 | findEmbedded(uri: Uri, keys: Set, text: string) {
141 | const key = uri.toString();
142 | const items: CompletionItem[] = [];
143 | const findStyles = /(?: