├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE.md ├── README.md ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── res ├── logo.png └── logo.svg ├── screenshots ├── preface.png └── preview-1.png ├── scripts └── prepare.ts ├── snippets └── html.json ├── src ├── annotation.ts ├── collections.ts ├── commands.ts ├── completions.ts ├── config.ts ├── index.ts ├── loader.ts ├── markdown.ts └── utils │ ├── Log.ts │ ├── base64.ts │ ├── index.ts │ ├── read-file.ts │ ├── svgs.ts │ └── types.ts ├── test └── fixture │ ├── .vscode │ └── settings.json │ ├── custom-collection.json │ ├── exclude │ └── index.html │ └── index.html └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: antfu 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - uses: pnpm/action-setup@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: lts/* 22 | registry-url: https://registry.npmjs.org/ 23 | - run: npm i -g @antfu/ni 24 | - run: pnpm install 25 | - run: npx changelogithub 26 | env: 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | src/generated 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "amodio.tsl-problem-matcher" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/dist/**/*.js" 14 | ], 15 | "preLaunchTask": "npm: dev" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emeraldwalk.runonsave": { 3 | "commands": [ 4 | { 5 | "match": "package.json", 6 | "isAsync": true, 7 | "cmd": "npm run update" 8 | } 9 | ] 10 | }, 11 | 12 | // Disable the default formatter, use eslint instead 13 | "prettier.enable": false, 14 | "editor.formatOnSave": false, 15 | 16 | // Auto fix 17 | "editor.codeActionsOnSave": { 18 | "source.fixAll": "explicit", 19 | "source.organizeImports": "never" 20 | }, 21 | 22 | // Silent the stylistic rules in you IDE, but still auto fix them 23 | "eslint.rules.customizations": [ 24 | { "rule": "style/*", "severity": "off" }, 25 | { "rule": "*-indent", "severity": "off" }, 26 | { "rule": "*-spacing", "severity": "off" }, 27 | { "rule": "*-spaces", "severity": "off" }, 28 | { "rule": "*-order", "severity": "off" }, 29 | { "rule": "*-dangle", "severity": "off" }, 30 | { "rule": "*-newline", "severity": "off" }, 31 | { "rule": "*quotes", "severity": "off" }, 32 | { "rule": "*semi", "severity": "off" } 33 | ], 34 | 35 | // Enable eslint for all supported languages 36 | "eslint.validate": [ 37 | "javascript", 38 | "javascriptreact", 39 | "typescript", 40 | "typescriptreact", 41 | "vue", 42 | "html", 43 | "markdown", 44 | "json", 45 | "jsonc", 46 | "yaml", 47 | "markdown", 48 | "mdx" 49 | ], 50 | "cSpell.words": [ 51 | "Inplace" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "dev", 9 | "isBackground": true, 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "problemMatcher": [ 14 | { 15 | "base": "$ts-webpack-watch", 16 | "background": { 17 | "activeOnStart": true, 18 | "beginsPattern": "Build start", 19 | "endsPattern": "Build success" 20 | } 21 | } 22 | ], 23 | "group": "build" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anthony Fu 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 |

2 | 3 | logo 4 | 5 |

6 | 7 |

8 | Visual Studio Marketplace Version 9 | Visual Studio Marketplace Downloads 10 | Visual Studio Marketplace Installs 11 |
12 | GitHub last commit 13 | GitHub issues 14 | GitHub stars 15 |

16 | 17 |
18 | 19 |

20 | preview 21 |

22 | 23 | ### Features 24 | 25 | - Inline display corresponding icons 26 | - Auto-completion for icon-sets 27 | - Hover 28 | - Snippets 29 | 30 | ## License 31 | 32 | MIT License © 2020 [Anthony Fu](https://github.com/antfu) 33 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const antfu = require('@antfu/eslint-config').default 3 | 4 | module.exports = antfu( 5 | { 6 | ignores: [ 7 | 'src/generated', 8 | ], 9 | }, 10 | ) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "antfu", 3 | "name": "iconify", 4 | "displayName": "Iconify IntelliSense", 5 | "type": "commonjs", 6 | "version": "0.10.5", 7 | "private": true, 8 | "packageManager": "pnpm@10.11.0", 9 | "description": "Intelligent Iconify previewing and searching for VS Code", 10 | "license": "MIT", 11 | "homepage": "https://github.com/antfu/vscode-iconify", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/antfu/vscode-iconify" 15 | }, 16 | "bugs": "https://github.com/antfu/vscode-iconify/issues", 17 | "keywords": [ 18 | "icons", 19 | "iconify" 20 | ], 21 | "sponsor": { 22 | "url": "https://github.com/sponsors/antfu" 23 | }, 24 | "preview": true, 25 | "categories": [ 26 | "Other", 27 | "Visualization", 28 | "Snippets" 29 | ], 30 | "main": "./dist/index.js", 31 | "icon": "res/logo.png", 32 | "files": [ 33 | "LICENSE.md", 34 | "dist/*", 35 | "res/*", 36 | "snippets/*" 37 | ], 38 | "engines": { 39 | "vscode": "^1.100.0" 40 | }, 41 | "activationEvents": [ 42 | "workspaceContains:package.json", 43 | "onLanguage:vue", 44 | "onLanguage:javascript", 45 | "onLanguage:javascriptreact", 46 | "onLanguage:typescript", 47 | "onLanguage:typescriptreact", 48 | "onLanguage:handlebars", 49 | "onLanguage:svelte", 50 | "onLanguage:html", 51 | "onLanguage:django-html", 52 | "onLanguage:css", 53 | "onLanguage:markdown", 54 | "onLanguage:mdx" 55 | ], 56 | "contributes": { 57 | "snippets": [ 58 | { 59 | "language": "html", 60 | "path": "snippets/html.json" 61 | }, 62 | { 63 | "language": "vue", 64 | "path": "snippets/html.json" 65 | } 66 | ], 67 | "commands": [ 68 | { 69 | "command": "iconify.toggle-annotations", 70 | "category": "Iconify", 71 | "title": "Toggle Annotations" 72 | }, 73 | { 74 | "command": "iconify.toggle-inplace", 75 | "category": "Iconify", 76 | "title": "Toggle In-place Mode" 77 | }, 78 | { 79 | "command": "iconify.clear-cache", 80 | "category": "Iconify", 81 | "title": "Clear icon cache" 82 | } 83 | ], 84 | "configuration": { 85 | "type": "object", 86 | "title": "Iconify IntelliSense", 87 | "properties": { 88 | "iconify.inplace": { 89 | "type": "boolean", 90 | "default": true, 91 | "description": "Use icon graph to replace the icon name." 92 | }, 93 | "iconify.annotations": { 94 | "type": "boolean", 95 | "default": true, 96 | "description": "Enabled Iconify inline annotations" 97 | }, 98 | "iconify.position": { 99 | "type": "string", 100 | "enum": [ 101 | "before", 102 | "after" 103 | ], 104 | "default": "before", 105 | "description": "Position the icon before or after the icon name" 106 | }, 107 | "iconify.color": { 108 | "type": "string", 109 | "default": "auto", 110 | "description": "Icon color hex for inline displaying" 111 | }, 112 | "iconify.delimiters": { 113 | "type": "array", 114 | "items": { 115 | "type": "string" 116 | }, 117 | "default": [ 118 | ":", 119 | "--", 120 | "-", 121 | "/" 122 | ], 123 | "description": "Delimiters for separating between collection id and icon id" 124 | }, 125 | "iconify.prefixes": { 126 | "type": "array", 127 | "items": { 128 | "type": "string" 129 | }, 130 | "default": [ 131 | "", 132 | "i-", 133 | "~icons/" 134 | ], 135 | "description": "Prefixes for matching" 136 | }, 137 | "iconify.suffixes": { 138 | "type": "array", 139 | "items": { 140 | "type": "string" 141 | }, 142 | "default": [ 143 | "", 144 | "i-" 145 | ], 146 | "description": "Suffixes for matching" 147 | }, 148 | "iconify.languageIds": { 149 | "type": "array", 150 | "items": { 151 | "type": "string" 152 | }, 153 | "default": [ 154 | "javascript", 155 | "javascriptreact", 156 | "typescript", 157 | "typescriptreact", 158 | "vue", 159 | "svelte", 160 | "html", 161 | "pug", 162 | "json", 163 | "yaml", 164 | "markdown", 165 | "mdx" 166 | ], 167 | "description": "Array of language IDs to enable annotations" 168 | }, 169 | "iconify.includes": { 170 | "type": "array", 171 | "items": { 172 | "type": "string", 173 | "enum": [ 174 | "academicons", 175 | "akar-icons", 176 | "ant-design", 177 | "arcticons", 178 | "basil", 179 | "bi", 180 | "bitcoin-icons", 181 | "bpmn", 182 | "brandico", 183 | "bx", 184 | "bxl", 185 | "bxs", 186 | "bytesize", 187 | "carbon", 188 | "catppuccin", 189 | "cbi", 190 | "charm", 191 | "ci", 192 | "cib", 193 | "cif", 194 | "cil", 195 | "circle-flags", 196 | "circum", 197 | "clarity", 198 | "codex", 199 | "codicon", 200 | "covid", 201 | "cryptocurrency", 202 | "cryptocurrency-color", 203 | "cuida", 204 | "dashicons", 205 | "devicon", 206 | "devicon-plain", 207 | "duo-icons", 208 | "ei", 209 | "el", 210 | "emojione", 211 | "emojione-monotone", 212 | "emojione-v1", 213 | "entypo", 214 | "entypo-social", 215 | "eos-icons", 216 | "ep", 217 | "et", 218 | "eva", 219 | "f7", 220 | "fa", 221 | "fa-brands", 222 | "fa-regular", 223 | "fa-solid", 224 | "fa6-brands", 225 | "fa6-regular", 226 | "fa6-solid", 227 | "fad", 228 | "famicons", 229 | "fe", 230 | "feather", 231 | "file-icons", 232 | "flag", 233 | "flagpack", 234 | "flat-color-icons", 235 | "flat-ui", 236 | "flowbite", 237 | "fluent", 238 | "fluent-color", 239 | "fluent-emoji", 240 | "fluent-emoji-flat", 241 | "fluent-emoji-high-contrast", 242 | "fluent-mdl2", 243 | "fontelico", 244 | "fontisto", 245 | "formkit", 246 | "foundation", 247 | "fxemoji", 248 | "gala", 249 | "game-icons", 250 | "garden", 251 | "geo", 252 | "gg", 253 | "gis", 254 | "gravity-ui", 255 | "gridicons", 256 | "grommet-icons", 257 | "guidance", 258 | "healthicons", 259 | "heroicons", 260 | "heroicons-outline", 261 | "heroicons-solid", 262 | "hugeicons", 263 | "humbleicons", 264 | "ic", 265 | "icomoon-free", 266 | "icon-park", 267 | "icon-park-outline", 268 | "icon-park-solid", 269 | "icon-park-twotone", 270 | "iconamoon", 271 | "iconoir", 272 | "icons8", 273 | "il", 274 | "ion", 275 | "iwwa", 276 | "ix", 277 | "jam", 278 | "la", 279 | "lets-icons", 280 | "line-md", 281 | "lineicons", 282 | "logos", 283 | "ls", 284 | "lsicon", 285 | "lucide", 286 | "lucide-lab", 287 | "mage", 288 | "majesticons", 289 | "maki", 290 | "map", 291 | "marketeq", 292 | "material-icon-theme", 293 | "material-symbols", 294 | "material-symbols-light", 295 | "mdi", 296 | "mdi-light", 297 | "medical-icon", 298 | "memory", 299 | "meteocons", 300 | "meteor-icons", 301 | "mi", 302 | "mingcute", 303 | "mono-icons", 304 | "mynaui", 305 | "nimbus", 306 | "nonicons", 307 | "noto", 308 | "noto-v1", 309 | "nrk", 310 | "octicon", 311 | "oi", 312 | "ooui", 313 | "openmoji", 314 | "oui", 315 | "pajamas", 316 | "pepicons", 317 | "pepicons-pencil", 318 | "pepicons-pop", 319 | "pepicons-print", 320 | "ph", 321 | "picon", 322 | "pixel", 323 | "pixelarticons", 324 | "prime", 325 | "proicons", 326 | "ps", 327 | "qlementine-icons", 328 | "quill", 329 | "radix-icons", 330 | "raphael", 331 | "ri", 332 | "rivet-icons", 333 | "si", 334 | "si-glyph", 335 | "simple-icons", 336 | "simple-line-icons", 337 | "skill-icons", 338 | "solar", 339 | "stash", 340 | "streamline", 341 | "streamline-emojis", 342 | "subway", 343 | "svg-spinners", 344 | "system-uicons", 345 | "tabler", 346 | "tdesign", 347 | "teenyicons", 348 | "token", 349 | "token-branded", 350 | "topcoat", 351 | "twemoji", 352 | "typcn", 353 | "uil", 354 | "uim", 355 | "uis", 356 | "uit", 357 | "uiw", 358 | "unjs", 359 | "vaadin", 360 | "vs", 361 | "vscode-icons", 362 | "websymbol", 363 | "weui", 364 | "whh", 365 | "wi", 366 | "wpf", 367 | "zmdi", 368 | "zondicons" 369 | ] 370 | }, 371 | "default": null, 372 | "description": "Collection IDs to be included for detection" 373 | }, 374 | "iconify.excludes": { 375 | "type": "array", 376 | "items": { 377 | "type": "string", 378 | "enum": [ 379 | "academicons", 380 | "akar-icons", 381 | "ant-design", 382 | "arcticons", 383 | "basil", 384 | "bi", 385 | "bitcoin-icons", 386 | "bpmn", 387 | "brandico", 388 | "bx", 389 | "bxl", 390 | "bxs", 391 | "bytesize", 392 | "carbon", 393 | "catppuccin", 394 | "cbi", 395 | "charm", 396 | "ci", 397 | "cib", 398 | "cif", 399 | "cil", 400 | "circle-flags", 401 | "circum", 402 | "clarity", 403 | "codex", 404 | "codicon", 405 | "covid", 406 | "cryptocurrency", 407 | "cryptocurrency-color", 408 | "cuida", 409 | "dashicons", 410 | "devicon", 411 | "devicon-plain", 412 | "duo-icons", 413 | "ei", 414 | "el", 415 | "emojione", 416 | "emojione-monotone", 417 | "emojione-v1", 418 | "entypo", 419 | "entypo-social", 420 | "eos-icons", 421 | "ep", 422 | "et", 423 | "eva", 424 | "f7", 425 | "fa", 426 | "fa-brands", 427 | "fa-regular", 428 | "fa-solid", 429 | "fa6-brands", 430 | "fa6-regular", 431 | "fa6-solid", 432 | "fad", 433 | "famicons", 434 | "fe", 435 | "feather", 436 | "file-icons", 437 | "flag", 438 | "flagpack", 439 | "flat-color-icons", 440 | "flat-ui", 441 | "flowbite", 442 | "fluent", 443 | "fluent-color", 444 | "fluent-emoji", 445 | "fluent-emoji-flat", 446 | "fluent-emoji-high-contrast", 447 | "fluent-mdl2", 448 | "fontelico", 449 | "fontisto", 450 | "formkit", 451 | "foundation", 452 | "fxemoji", 453 | "gala", 454 | "game-icons", 455 | "garden", 456 | "geo", 457 | "gg", 458 | "gis", 459 | "gravity-ui", 460 | "gridicons", 461 | "grommet-icons", 462 | "guidance", 463 | "healthicons", 464 | "heroicons", 465 | "heroicons-outline", 466 | "heroicons-solid", 467 | "hugeicons", 468 | "humbleicons", 469 | "ic", 470 | "icomoon-free", 471 | "icon-park", 472 | "icon-park-outline", 473 | "icon-park-solid", 474 | "icon-park-twotone", 475 | "iconamoon", 476 | "iconoir", 477 | "icons8", 478 | "il", 479 | "ion", 480 | "iwwa", 481 | "ix", 482 | "jam", 483 | "la", 484 | "lets-icons", 485 | "line-md", 486 | "lineicons", 487 | "logos", 488 | "ls", 489 | "lsicon", 490 | "lucide", 491 | "lucide-lab", 492 | "mage", 493 | "majesticons", 494 | "maki", 495 | "map", 496 | "marketeq", 497 | "material-icon-theme", 498 | "material-symbols", 499 | "material-symbols-light", 500 | "mdi", 501 | "mdi-light", 502 | "medical-icon", 503 | "memory", 504 | "meteocons", 505 | "meteor-icons", 506 | "mi", 507 | "mingcute", 508 | "mono-icons", 509 | "mynaui", 510 | "nimbus", 511 | "nonicons", 512 | "noto", 513 | "noto-v1", 514 | "nrk", 515 | "octicon", 516 | "oi", 517 | "ooui", 518 | "openmoji", 519 | "oui", 520 | "pajamas", 521 | "pepicons", 522 | "pepicons-pencil", 523 | "pepicons-pop", 524 | "pepicons-print", 525 | "ph", 526 | "picon", 527 | "pixel", 528 | "pixelarticons", 529 | "prime", 530 | "proicons", 531 | "ps", 532 | "qlementine-icons", 533 | "quill", 534 | "radix-icons", 535 | "raphael", 536 | "ri", 537 | "rivet-icons", 538 | "si", 539 | "si-glyph", 540 | "simple-icons", 541 | "simple-line-icons", 542 | "skill-icons", 543 | "solar", 544 | "stash", 545 | "streamline", 546 | "streamline-emojis", 547 | "subway", 548 | "svg-spinners", 549 | "system-uicons", 550 | "tabler", 551 | "tdesign", 552 | "teenyicons", 553 | "token", 554 | "token-branded", 555 | "topcoat", 556 | "twemoji", 557 | "typcn", 558 | "uil", 559 | "uim", 560 | "uis", 561 | "uit", 562 | "uiw", 563 | "unjs", 564 | "vaadin", 565 | "vs", 566 | "vscode-icons", 567 | "websymbol", 568 | "weui", 569 | "whh", 570 | "wi", 571 | "wpf", 572 | "zmdi", 573 | "zondicons" 574 | ] 575 | }, 576 | "default": null, 577 | "description": "Collection IDs to be excluded for detection" 578 | }, 579 | "iconify.cdnEntry": { 580 | "type": "string", 581 | "default": "https://icones.js.org/collections", 582 | "description": "CDN entry of iconify icon-sets" 583 | }, 584 | "iconify.customCollectionJsonPaths": { 585 | "type": "array", 586 | "items": { 587 | "type": "string" 588 | }, 589 | "default": [], 590 | "description": "JSON paths for custom collection" 591 | }, 592 | "iconify.customCollectionIdsMap": { 593 | "type": "object", 594 | "items": { 595 | "type": "string" 596 | }, 597 | "default": {}, 598 | "description": "Collection IDs Map for collection name alias, e.g. { 'mc': 'mingcute' }" 599 | }, 600 | "iconify.customAliasesJsonPaths": { 601 | "type": "array", 602 | "items": { 603 | "type": "string" 604 | }, 605 | "default": [], 606 | "description": "JSON paths for custom aliases" 607 | }, 608 | "iconify.customAliasesOnly": { 609 | "type": "boolean", 610 | "default": false, 611 | "description": "Only use the icon aliases. Non aliased icons will be ignored." 612 | }, 613 | "iconify.preview.include": { 614 | "type": "array", 615 | "items": { 616 | "type": "string" 617 | }, 618 | "default": [ 619 | "**/*.*" 620 | ], 621 | "description": "Glob patterns for file paths to enable icon preview" 622 | }, 623 | "iconify.preview.exclude": { 624 | "type": "array", 625 | "items": { 626 | "type": "string" 627 | }, 628 | "default": [], 629 | "description": "Glob patterns for file paths to disable icon preview" 630 | } 631 | } 632 | } 633 | }, 634 | "scripts": { 635 | "build": "tsup src/index.ts --external vscode", 636 | "dev": "nr build --watch", 637 | "lint": "eslint .", 638 | "vscode:prepublish": "nr build", 639 | "publish": "vsce publish --no-dependencies", 640 | "pack": "vsce package --no-dependencies", 641 | "typecheck": "tsc --noEmit", 642 | "prepare": "esno scripts/prepare.ts && pnpm run update", 643 | "update": "vscode-ext-gen --output src/generated/meta.ts", 644 | "release": "bumpp && nr publish" 645 | }, 646 | "devDependencies": { 647 | "@antfu/eslint-config": "^4.13.1", 648 | "@antfu/utils": "^9.2.0", 649 | "@iconify/json": "^2.2.339", 650 | "@iconify/types": "^2.0.0", 651 | "@types/fs-extra": "^11.0.4", 652 | "@types/node": "^22.15.19", 653 | "@types/vscode": "^1.100.0", 654 | "@vscode/vsce": "^3.4.1", 655 | "bumpp": "^10.1.1", 656 | "eslint": "^9.27.0", 657 | "esno": "^4.8.0", 658 | "fs-extra": "^11.3.0", 659 | "ofetch": "^1.4.1", 660 | "reactive-vscode": "^0.2.17", 661 | "strip-json-comments": "^5.0.2", 662 | "tsup": "^8.5.0", 663 | "typescript": "^5.8.3", 664 | "vscode-ext-gen": "^1.0.2" 665 | } 666 | } 667 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - esbuild 3 | - unrs-resolver 4 | -------------------------------------------------------------------------------- /res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/vscode-iconify/4c3fb82baf84a295ef171d38e1602014f27021cf/res/logo.png -------------------------------------------------------------------------------- /res/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /screenshots/preface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/vscode-iconify/4c3fb82baf84a295ef171d38e1602014f27021cf/screenshots/preface.png -------------------------------------------------------------------------------- /screenshots/preview-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/vscode-iconify/4c3fb82baf84a295ef171d38e1602014f27021cf/screenshots/preview-1.png -------------------------------------------------------------------------------- /scripts/prepare.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyMetaDataCollection } from '@iconify/json' 2 | import type { IconifyJSON } from '@iconify/types' 3 | import type { IconsetMeta } from '../src/collections' 4 | import { join, resolve } from 'node:path' 5 | import { fileURLToPath } from 'node:url' 6 | import fs from 'fs-extra' 7 | 8 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 9 | const out = resolve(__dirname, '../src/generated') 10 | 11 | async function prepareJSON() { 12 | const dir = resolve(__dirname, '../node_modules/@iconify/json') 13 | const raw: IconifyMetaDataCollection = await fs.readJSON(join(dir, 'collections.json')) 14 | 15 | const collections = Object.entries(raw).map(([id, v]) => ({ 16 | ...(v as any), 17 | id, 18 | })) 19 | 20 | const collectionsMeta: IconsetMeta[] = [] 21 | 22 | for (const info of collections) { 23 | const setData: IconifyJSON = await fs.readJSON(join(dir, 'json', `${info.id}.json`)) 24 | 25 | const icons = Object.keys(setData.icons) 26 | const { id, name, author, height, license } = info 27 | const meta = { author: author.name, height, name, id, icons, license: license.spdk } 28 | collectionsMeta.push(meta) 29 | } 30 | 31 | const collectionsIds = collectionsMeta.map(i => i.id).sort() 32 | 33 | const pkg = await fs.readJSON('./package.json') 34 | pkg.contributes.configuration.properties['iconify.includes'].items.enum = collectionsIds 35 | pkg.contributes.configuration.properties['iconify.excludes'].items.enum = collectionsIds 36 | await fs.writeJSON('./package.json', pkg, { spaces: 2 }) 37 | 38 | await fs.ensureDir(out) 39 | await fs.writeFile(join(out, 'collections.ts'), `export default \`${JSON.stringify(collectionsMeta)}\``, 'utf-8') 40 | } 41 | 42 | async function prepare() { 43 | await prepareJSON() 44 | } 45 | 46 | prepare() 47 | -------------------------------------------------------------------------------- /snippets/html.json: -------------------------------------------------------------------------------- 1 | { 2 | "Iconify HTML": { 3 | "prefix": "icon", 4 | "body": [ 5 | "" 6 | ], 7 | "description": "Iconify HTML" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/annotation.ts: -------------------------------------------------------------------------------- 1 | import type { DecorationOptions } from 'vscode' 2 | import { shallowRef, useActiveEditorDecorations, useActiveTextEditor, useDocumentText, useTextEditorSelections, watchEffect } from 'reactive-vscode' 3 | import { DecorationRangeBehavior, languages, Range, Uri, window } from 'vscode' 4 | import { config, editorConfig, isCustomAliasesFile, REGEX_COLLECTION_ICON, REGEX_FULL } from './config' 5 | import { getDataURL, getIconInfo } from './loader' 6 | import { getIconMarkdown } from './markdown' 7 | 8 | import { isTruthy } from './utils' 9 | 10 | export interface DecorationMatch extends DecorationOptions { 11 | key: string 12 | } 13 | 14 | export function useAnnotations() { 15 | const InlineIconDecoration = window.createTextEditorDecorationType({ 16 | textDecoration: 'none; opacity: 0.6 !important;', 17 | rangeBehavior: DecorationRangeBehavior.ClosedClosed, 18 | }) 19 | const HideTextDecoration = window.createTextEditorDecorationType({ 20 | textDecoration: 'none; display: none;', // a hack to inject custom style 21 | }) 22 | 23 | const editor = useActiveTextEditor() 24 | const selections = useTextEditorSelections(editor) 25 | const text = useDocumentText(() => editor.value?.document) 26 | 27 | const decorations = shallowRef([]) 28 | 29 | useActiveEditorDecorations(InlineIconDecoration, decorations) 30 | useActiveEditorDecorations( 31 | HideTextDecoration, 32 | () => config.inplace 33 | ? decorations.value 34 | .map(({ range }) => range) 35 | .filter(i => !selections.value.map(({ start }) => start.line).includes(i.start.line)) 36 | : [], 37 | ) 38 | 39 | // Calculate decorations 40 | watchEffect(async () => { 41 | if (!editor.value) 42 | return 43 | 44 | if (!config.annotations) { 45 | decorations.value = [] 46 | return 47 | } 48 | 49 | const { document } = editor.value 50 | const previewIncludePatterns = config.preview.include || [] 51 | const previewExcludePatterns = config.preview.exclude || [] 52 | 53 | let shouldPreview = previewIncludePatterns.length 54 | ? previewIncludePatterns.some(pattern => !!languages.match({ pattern }, document)) 55 | : true 56 | if (previewExcludePatterns.length && previewExcludePatterns.some(pattern => !!languages.match({ pattern }, document))) 57 | shouldPreview = false 58 | 59 | if (!shouldPreview) { 60 | decorations.value = [] 61 | return 62 | } 63 | 64 | let match 65 | const isAliasesFile = isCustomAliasesFile(document.uri.path) 66 | const regex = isAliasesFile ? REGEX_COLLECTION_ICON.value : REGEX_FULL.value 67 | if (!regex) 68 | return 69 | regex.lastIndex = 0 70 | const keys: [Range, string][] = [] 71 | 72 | // eslint-disable-next-line no-cond-assign 73 | while ((match = regex.exec(text.value!))) { 74 | const key = match[1] 75 | if (!key) 76 | continue 77 | 78 | const startPos = document.positionAt(match.index + 1) 79 | const endPos = document.positionAt(match.index + match[0].length) 80 | keys.push([new Range(startPos, endPos), key]) 81 | } 82 | 83 | const fontSize = editorConfig.fontSize 84 | const position = config.position === 'after' ? 'after' : 'before' 85 | decorations.value = (await Promise.all(keys.map(async ([range, key]) => { 86 | const info = await getIconInfo(key, !isAliasesFile) 87 | if (!info) 88 | return undefined 89 | 90 | const dataurl = await getDataURL(info, editorConfig.fontSize * 1.2) 91 | 92 | const item: DecorationMatch = { 93 | range, 94 | renderOptions: { 95 | [position]: { 96 | contentIconPath: Uri.parse(dataurl), 97 | margin: `-${fontSize}px 2px; transform: translate(-2px, 3px);`, 98 | width: `${fontSize * info.ratio * 1.1}px`, 99 | }, 100 | }, 101 | hoverMessage: await getIconMarkdown(key), 102 | key, 103 | } 104 | return item 105 | }))).filter(isTruthy) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /src/collections.ts: -------------------------------------------------------------------------------- 1 | import raw from './generated/collections' 2 | 3 | export interface IconsetMeta { 4 | id: string 5 | name?: string 6 | author?: string 7 | icons: string[] 8 | height?: number | number[] 9 | license?: string 10 | } 11 | 12 | export const collections: IconsetMeta[] = JSON.parse(raw) 13 | 14 | export const collectionIds = collections.map(i => i.id) 15 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import type { DecorationOptions } from 'vscode' 2 | import { useCommand } from 'reactive-vscode' 3 | import { config } from './config' 4 | import * as meta from './generated/meta' 5 | import { clearCache } from './loader' 6 | 7 | export interface DecorationMatch extends DecorationOptions { 8 | key: string 9 | } 10 | 11 | export function useCommands() { 12 | useCommand(meta.commands.toggleAnnotations, () => { 13 | config.$update('annotations', !config.annotations) 14 | }) 15 | 16 | useCommand(meta.commands.toggleInplace, () => { 17 | config.$update('inplace', !config.inplace) 18 | }) 19 | 20 | useCommand(meta.commands.clearCache, () => { 21 | clearCache() 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/completions.ts: -------------------------------------------------------------------------------- 1 | import type { CompletionItemProvider, TextDocument } from 'vscode' 2 | import { extensionContext } from 'reactive-vscode' 3 | import { CompletionItem, CompletionItemKind, languages, Position, Range } from 'vscode' 4 | import { config, enabledAliasIds, enabledCollectionIds, enabledCollections, REGEX_NAMESPACE, REGEX_PREFIXED } from './config' 5 | import { getCollectionMarkdown, getIconMarkdown } from './markdown' 6 | 7 | export function useCompletion() { 8 | const ctx = extensionContext.value! 9 | const iconProvider: CompletionItemProvider = { 10 | provideCompletionItems(document: TextDocument, position: Position) { 11 | const line = document.getText(new Range(new Position(position.line, 0), new Position(position.line, position.character))) 12 | 13 | const prefixMatch = line.match(REGEX_PREFIXED.value) 14 | if (!prefixMatch) 15 | return null 16 | 17 | // Index of the first character after the prefix 18 | const startIndex = prefixMatch.index! + 1 19 | 20 | const range = new Range(position.line, startIndex, position.line, position.character) 21 | const aliasCompletion = enabledAliasIds.value.map((i) => { 22 | const item = new CompletionItem(i, CompletionItemKind.Text) 23 | item.detail = `alias: ${i}` 24 | item.range = range 25 | return item 26 | }) 27 | 28 | if (config.customAliasesOnly) 29 | return aliasCompletion 30 | 31 | const namespaceMatch = line.match(REGEX_NAMESPACE.value) 32 | if (!namespaceMatch) 33 | return aliasCompletion 34 | 35 | const id = namespaceMatch[1] 36 | const info = enabledCollections.value.find(i => i.id === id) 37 | if (!info) 38 | return aliasCompletion 39 | 40 | return [ 41 | ...aliasCompletion, 42 | ...info.icons 43 | .map((i) => { 44 | const item = new CompletionItem(i, CompletionItemKind.Text) 45 | item.detail = `${id}${config.delimiters[0]}${i}` 46 | item.range = range 47 | return item 48 | }), 49 | ] 50 | }, 51 | async resolveCompletionItem(item: CompletionItem) { 52 | return { 53 | ...item, 54 | documentation: await getIconMarkdown(item.detail!), 55 | } 56 | }, 57 | } 58 | 59 | const REGEX_COLLECTION = /icon=['"][\w-]*$/ 60 | 61 | const collectionProvider: CompletionItemProvider = { 62 | provideCompletionItems(document: TextDocument, position: Position) { 63 | const line = document.getText(new Range(new Position(position.line, 0), new Position(position.line, position.character))) 64 | const match = REGEX_COLLECTION.test(line) 65 | if (!match) 66 | return null 67 | 68 | return enabledCollectionIds.value 69 | .map(c => new CompletionItem(c, CompletionItemKind.Text)) 70 | }, 71 | 72 | async resolveCompletionItem(item: CompletionItem) { 73 | return { 74 | ...item, 75 | documentation: await getCollectionMarkdown(ctx, item.label as string), 76 | } 77 | }, 78 | } 79 | 80 | ctx.subscriptions.push( 81 | languages.registerCompletionItemProvider( 82 | config.languageIds, 83 | iconProvider, 84 | ...config.delimiters, 85 | ), 86 | languages.registerCompletionItemProvider( 87 | config.languageIds, 88 | collectionProvider, 89 | '"', 90 | '\'', 91 | ), 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyJSON } from '@iconify/types' 2 | import type { IconsetMeta } from './collections' 3 | import { isAbsolute, resolve } from 'node:path' 4 | import fs from 'fs-extra' 5 | import { computed, defineConfigObject, ref, shallowReactive, shallowRef, useFsWatcher, useIsDarkTheme, useWorkspaceFolders, watchEffect } from 'reactive-vscode' 6 | import { Uri } from 'vscode' 7 | import { collectionIds, collections } from './collections' 8 | import * as Meta from './generated/meta' 9 | import { deleteTask } from './loader' 10 | import { Log, readJSON } from './utils' 11 | 12 | export const config = defineConfigObject( 13 | Meta.scopedConfigs.scope, 14 | Meta.scopedConfigs.defaults, 15 | ) 16 | 17 | export const editorConfig = defineConfigObject<{ 18 | fontSize: number 19 | }>( 20 | 'editor', 21 | { 22 | fontSize: 12, 23 | }, 24 | ) 25 | 26 | function escapeRegExp(text: string) { 27 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') 28 | } 29 | 30 | export const customCollections = shallowRef([] as IconifyJSON[]) 31 | 32 | export async function useCustomCollections() { 33 | const workspaceFolders = useWorkspaceFolders() 34 | 35 | /** key is URL.href */ 36 | const result = shallowReactive(new Map()) 37 | const iconifyJsonPaths = computed(() => { 38 | const files = Array.from( 39 | new Set(config.customCollectionJsonPaths.flatMap((file: string) => { 40 | if (isAbsolute(file)) 41 | return [file] 42 | 43 | const list: string[] = [] 44 | if (workspaceFolders.value) { 45 | for (const folder of workspaceFolders.value) 46 | list.push(resolve(folder.uri.fsPath, file)) 47 | } 48 | return list 49 | })), 50 | ) 51 | 52 | return files.filter((file) => { 53 | const exists = fs.existsSync(file) 54 | if (!exists) 55 | Log.warn(`Custom collection file does not exist: ${file}`) 56 | return exists 57 | }) 58 | }) 59 | const { onDidChange, onDidDelete, onDidCreate } = useFsWatcher(iconifyJsonPaths) 60 | 61 | async function load(url: URL) { 62 | Log.info(`Loading custom collections from:\n${url}`) 63 | try { 64 | const val: IconifyJSON = await readJSON(url) 65 | result.set(url.href, val) 66 | deleteTask(val.prefix) 67 | } 68 | catch { 69 | Log.error(`Error on loading custom collection: ${url}`) 70 | } 71 | } 72 | 73 | // Initial load 74 | iconifyJsonPaths.value.forEach(p => load(new URL(Uri.file(p)))) 75 | 76 | onDidChange(uri => load(new URL(uri))) 77 | onDidCreate(uri => load(new URL(uri))) 78 | onDidDelete(uri => result.delete(new URL(uri).href)) 79 | 80 | watchEffect(() => customCollections.value = Array.from(result.values())) 81 | } 82 | 83 | export const customAliases = ref([] as Record[]) 84 | const customAliasesFiles = ref([] as string[]) 85 | 86 | export async function useCustomAliases() { 87 | const workspaceFolders = useWorkspaceFolders() 88 | 89 | watchEffect(async () => { 90 | const result = [] as Record[] 91 | const files = Array.from( 92 | new Set(config.customAliasesJsonPaths.flatMap((file: string) => { 93 | if (isAbsolute(file)) 94 | return [file] 95 | 96 | const list: string[] = [] 97 | if (workspaceFolders.value) { 98 | for (const folder of workspaceFolders.value) 99 | list.push(resolve(folder.uri.fsPath, file)) 100 | } 101 | return list 102 | })), 103 | ) 104 | 105 | const existingFiles = files.filter((file) => { 106 | const exists = fs.existsSync(file) 107 | if (!exists) 108 | Log.warn(`Custom aliases file does not exist: ${file}`) 109 | return exists 110 | }) 111 | 112 | if (existingFiles.length) { 113 | Log.info(`Loading custom aliases from:\n${existingFiles.map(i => ` - ${i}`).join('\n')}`) 114 | 115 | await Promise.all(existingFiles.map(async (file) => { 116 | try { 117 | result.push(await readJSON(file)) 118 | } 119 | catch { 120 | Log.error(`Error on loading custom aliases: ${file}`) 121 | } 122 | })) 123 | } 124 | 125 | customAliases.value = result 126 | customAliasesFiles.value = existingFiles 127 | }) 128 | } 129 | 130 | export const enabledCollectionIds = computed(() => { 131 | const includes = config.includes?.length ? config.includes : collectionIds 132 | const excludes = config.excludes as string[] || [] 133 | 134 | const collections = [ 135 | ...includes.filter(i => !excludes.includes(i)), 136 | ...(Object.keys(config.customCollectionIdsMap)), 137 | ...customCollections.value.map(c => c.prefix), 138 | ] 139 | collections.sort((a, b) => b.length - a.length) 140 | return collections 141 | }) 142 | 143 | export const enabledCollections = computed(() => { 144 | const customData: IconsetMeta[] = customCollections.value.map(c => ({ 145 | id: c.prefix, 146 | name: c.info?.name, 147 | author: c.info?.author.name, 148 | icons: Object.keys(c.icons), 149 | height: c.info?.height, 150 | })) 151 | return [...collections, ...customData] 152 | }) 153 | 154 | export const enabledAliases = computed((): Record => { 155 | const flat: Record = {} 156 | for (const aliases of customAliases.value) { 157 | for (const [key, value] of Object.entries(aliases)) 158 | flat[key] = value 159 | } 160 | return flat 161 | }) 162 | 163 | export const enabledAliasIds = computed(() => { 164 | return Object.keys(enabledAliases.value) 165 | }) 166 | 167 | export function isCustomAliasesFile(path: string) { 168 | return customAliasesFiles.value.includes(path) 169 | } 170 | 171 | const RE_PART_DELIMITERS = computed(() => `(${config.delimiters.map(i => escapeRegExp(i)).join('|')})`) 172 | 173 | const RE_PART_PREFIXES = computed(() => { 174 | if (!config.prefixes.filter(Boolean).length) 175 | return '' 176 | const empty = config.prefixes.includes('') 177 | return `(?:${config.prefixes.filter(Boolean) 178 | .map(i => escapeRegExp(i)) 179 | .join('|')})${empty ? '?' : ''}` 180 | }) 181 | 182 | const RE_PART_SUFFIXES = computed(() => { 183 | if (!config.suffixes.filter(Boolean).length) 184 | return '' 185 | const empty = config.suffixes.includes('') 186 | return `(?:${config.suffixes.filter(Boolean) 187 | .map(i => escapeRegExp(i)) 188 | .join('|')})${empty ? '?' : ''}` 189 | }) 190 | 191 | export const REGEX_DELIMITERS = computed(() => new RegExp(RE_PART_DELIMITERS.value, 'g')) 192 | 193 | export const REGEX_PREFIXED = computed(() => { 194 | return new RegExp(`[^\\w\\d]${RE_PART_PREFIXES.value}[\\w-]*$`) 195 | }) 196 | 197 | export const REGEX_NAMESPACE = computed(() => { 198 | return new RegExp(`[^\\w\\d]${RE_PART_PREFIXES.value}(${enabledCollectionIds.value.join('|')})${RE_PART_DELIMITERS.value}[\\w-]*$`) 199 | }) 200 | 201 | export const REGEX_COLLECTION_ICON = computed(() => { 202 | return new RegExp(`[^\\w\\d]((?:${enabledCollectionIds.value.join('|')})${RE_PART_DELIMITERS.value}[\\w-]+)(?=\\b[^-])`, 'g') 203 | }) 204 | 205 | export const REGEX_FULL = computed(() => { 206 | if (config.customAliasesOnly) 207 | return new RegExp(`[^\\w\\d]${RE_PART_PREFIXES.value}(${enabledAliasIds.value.join('|')})${RE_PART_SUFFIXES.value}(?=\\b[^-])`, 'g') 208 | return new RegExp(`[^\\w\\d]${RE_PART_PREFIXES.value}((?:(?:${enabledCollectionIds.value.join('|')})${RE_PART_DELIMITERS.value}[\\w-]+)|(?:${enabledAliasIds.value.join('|')}))${RE_PART_SUFFIXES.value}(?=\\b[^-])`, 'g') 209 | }) 210 | 211 | const REGEX_STARTING_DELIMITERS = computed(() => new RegExp(`^${RE_PART_DELIMITERS.value}`, 'g')) 212 | 213 | function verifyCollection(collection: string, str: string) { 214 | return str.startsWith(collection) && REGEX_STARTING_DELIMITERS.value.test(str.slice(collection.length)) 215 | } 216 | 217 | export function parseIcon(str: string) { 218 | const collection = enabledCollectionIds.value.find(c => verifyCollection(c, str)) 219 | if (!collection) 220 | return 221 | 222 | const icon = str.slice(collection.length).replace(REGEX_STARTING_DELIMITERS.value, '') 223 | if (!icon) 224 | return 225 | 226 | return { 227 | collection: String(config.customCollectionIdsMap[collection] ?? collection), 228 | icon, 229 | } 230 | } 231 | 232 | const isDark = useIsDarkTheme() 233 | export const color = computed(() => isDark.value ? '#eee' : '#222') 234 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { defineExtension } from 'reactive-vscode' 2 | import { version } from '../package.json' 3 | import { useAnnotations } from './annotation' 4 | import { collections } from './collections' 5 | import { useCommands } from './commands' 6 | import { useCompletion } from './completions' 7 | import { useCustomAliases, useCustomCollections } from './config' 8 | import { Log } from './utils' 9 | 10 | const { activate, deactivate } = defineExtension(async () => { 11 | Log.info(`🈶 Activated, v${version}`) 12 | 13 | useCommands() 14 | 15 | await useCustomCollections() 16 | 17 | Log.info(`🎛 ${collections.length} icon sets loaded`) 18 | 19 | await useCustomAliases() 20 | 21 | Log.info(`🎛 ${collections.length} aliases loaded`) 22 | 23 | useCompletion() 24 | useAnnotations() 25 | }) 26 | 27 | export { activate, deactivate } 28 | -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyIcon, IconifyJSON } from '@iconify/types' 2 | import { Buffer } from 'node:buffer' 3 | import { $fetch } from 'ofetch' 4 | import { extensionContext } from 'reactive-vscode' 5 | import { Uri, workspace } from 'vscode' 6 | import { color, config, customCollections, enabledAliases, parseIcon } from './config' 7 | import { Log } from './utils' 8 | import { pathToSvg, toDataUrl } from './utils/svgs' 9 | 10 | let loadedIconSets: Record = {} 11 | let dataURLCache: Record = {} 12 | 13 | let _tasks: Record> = {} 14 | export function UniqPromise(fn: (id: string) => Promise) { 15 | return async (id: string) => { 16 | if (!_tasks[id]) 17 | _tasks[id] = fn(id) 18 | return await _tasks[id] 19 | } 20 | } 21 | export function deleteTask(id: string) { 22 | return delete _tasks[id] 23 | } 24 | 25 | function getCacheUri() { 26 | return Uri.joinPath(extensionContext.value!.globalStorageUri, 'icon-set-cache') 27 | } 28 | 29 | function getCacheUriForIconSet(iconSetId: string) { 30 | return Uri.joinPath(getCacheUri(), `${iconSetId}.json`) 31 | } 32 | 33 | export async function clearCache() { 34 | const ctx = extensionContext.value! 35 | _tasks = {} 36 | await workspace.fs.delete(getCacheUri(), { recursive: true }) 37 | 38 | // clear legacy cache 39 | for (const id of ctx.globalState.keys()) { 40 | if (id.startsWith('icons-')) 41 | ctx.globalState.update(id, undefined) 42 | } 43 | 44 | loadedIconSets = {} 45 | dataURLCache = {} 46 | Log.info('🗑️ Cleared all cache') 47 | } 48 | 49 | async function writeCache(iconSetId: string, data: IconifyJSON) { 50 | try { 51 | await workspace.fs.writeFile( 52 | getCacheUriForIconSet(iconSetId), 53 | Buffer.from(JSON.stringify(data)), 54 | ) 55 | } 56 | catch (e) { 57 | Log.error(e) 58 | } 59 | } 60 | 61 | async function loadCache(iconSetId: string): Promise { 62 | try { 63 | const buffer = await workspace.fs.readFile(getCacheUriForIconSet(iconSetId)) 64 | return JSON.parse(buffer.toString()) 65 | } 66 | catch {} 67 | } 68 | 69 | async function migrateCache() { 70 | const ctx = extensionContext.value! 71 | const prefix = 'icons-' 72 | for (const key of ctx.globalState.keys()) { 73 | if (key.startsWith(prefix)) { 74 | const cached = ctx.globalState.get(key)! 75 | const iconSetId = key.slice(prefix.length) 76 | loadedIconSets[iconSetId] = cached 77 | await writeCache(iconSetId, cached) 78 | ctx.globalState.update(key, undefined) 79 | Log.info(`🔀 [${iconSetId}] Migrated iconset to new storage`) 80 | } 81 | } 82 | } 83 | 84 | export const LoadIconSet = UniqPromise(async (id: string) => { 85 | await migrateCache() 86 | 87 | let data: IconifyJSON = loadedIconSets[id] || customCollections.value.find(c => c.prefix === id) 88 | 89 | if (!data) { 90 | const cached = await loadCache(id) 91 | if (cached) { 92 | loadedIconSets[id] = cached 93 | data = cached 94 | Log.info(`✅ [${id}] Loaded from disk`) 95 | } 96 | else { 97 | try { 98 | const url = `${config.cdnEntry}/${id}.json` 99 | Log.info(`⏳ [${id}] Downloading from ${url}`) 100 | data = await $fetch(url) 101 | Log.info(`✅ [${id}] Downloaded`) 102 | loadedIconSets[id] = data 103 | writeCache(id, data) 104 | } 105 | catch (e) { 106 | Log.error(e, true) 107 | } 108 | } 109 | } 110 | 111 | return data 112 | }) 113 | 114 | export interface IconInfo extends IconifyIcon { 115 | width: number 116 | height: number 117 | key: string 118 | ratio: number 119 | collection: string 120 | id: string 121 | } 122 | 123 | export async function getIconInfo(key: string, allowAliases = true) { 124 | const alias = allowAliases ? enabledAliases.value[key] : undefined 125 | if (allowAliases && config.customAliasesOnly && !alias) 126 | return 127 | 128 | const actualKey = alias ?? key 129 | 130 | const result = parseIcon(actualKey) 131 | if (!result) 132 | return 133 | 134 | const data = await LoadIconSet(result.collection) 135 | const icon = data?.icons?.[result.icon] as IconInfo 136 | if (!data || !icon) 137 | return null 138 | 139 | if (!icon.width) 140 | icon.width = data.width || 16 141 | 142 | if (!icon.height) 143 | icon.height = data.height || 16 144 | 145 | icon.collection = result.collection 146 | icon.id = result.icon 147 | icon.key = actualKey 148 | icon.ratio = (data.width! / data.height!) || 1 149 | 150 | return icon 151 | } 152 | 153 | export async function getDataURL(key: string, fontSize?: number): Promise 154 | export async function getDataURL(info: IconInfo, fontSize?: number): Promise 155 | export async function getDataURL(keyOrInfo: string | IconInfo, fontSize = 32) { 156 | const key = typeof keyOrInfo === 'string' ? keyOrInfo : keyOrInfo.key 157 | 158 | const cacheKey = color.value + fontSize + key 159 | if (dataURLCache[cacheKey]) 160 | return dataURLCache[cacheKey] 161 | 162 | const info = typeof keyOrInfo === 'string' 163 | ? await getIconInfo(key) 164 | : keyOrInfo 165 | 166 | if (!info) 167 | return '' 168 | 169 | dataURLCache[cacheKey] = toDataUrl(pathToSvg(info, fontSize).replace(/currentColor/g, color.value)) 170 | return dataURLCache[cacheKey] 171 | } 172 | -------------------------------------------------------------------------------- /src/markdown.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode' 2 | import { MarkdownString } from 'vscode' 3 | import { config, enabledCollections } from './config' 4 | import { getDataURL, getIconInfo } from './loader' 5 | 6 | export async function getIconMarkdown(key: string) { 7 | const info = await getIconInfo(key) 8 | if (!info) 9 | return '' 10 | 11 | const icon = await getDataURL(info, 150) 12 | const setId = info.collection 13 | const url = `https://icones.js.org/collection/${setId}` 14 | const collection = enabledCollections.value.find(collection => collection.id === setId) 15 | return new MarkdownString(`| |\n|:---:|\n| ![](${icon}) |\n| [\`${key}\`](${url}) |\n\n${collection?.license ?? ''}`) 16 | } 17 | 18 | export async function getCollectionMarkdown(ctx: ExtensionContext, id: string) { 19 | const collection = enabledCollections.value.find(collection => collection.id === id) 20 | if (!collection) 21 | return '' 22 | 23 | const iconKeys = collection.icons.slice(0, 5) 24 | const icons = await Promise.all(iconKeys.map(key => getDataURL([id, key].join(config.delimiters[0]), 24))) 25 | const iconsMarkdown = icons.map(icon => `![](${icon})`).join(' ') 26 | 27 | const url = `https://icones.js.org/collection/${collection.id}` 28 | return new MarkdownString(`#### [${collection.name}](${url})\n${collection.author}\n\n${iconsMarkdown}\n\n${collection.license ?? ''}`) 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/Log.ts: -------------------------------------------------------------------------------- 1 | import { defineLogger } from 'reactive-vscode' 2 | import { displayName } from '../generated/meta' 3 | 4 | export const Log = defineLogger(displayName) 5 | -------------------------------------------------------------------------------- /src/utils/base64.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/no-unlimited-disable */ 2 | /* eslint-disable */ 3 | // @ts-expect-error 4 | const Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(e){var t="";var n,r,i,s,o,u,a;var f=0;e=Base64._utf8_encode(e);while(f>2;o=(n&3)<<4|r>>4;u=(r&15)<<2|i>>6;a=i&63;if(isNaN(r)){u=a=64}else if(isNaN(i)){a=64}t=t+this._keyStr.charAt(s)+this._keyStr.charAt(o)+this._keyStr.charAt(u)+this._keyStr.charAt(a)}return t},decode:function(e){var t="";var n,r,i;var s,o,u,a;var f=0;e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(f>4;r=(o&15)<<4|u>>2;i=(u&3)<<6|a;t=t+String.fromCharCode(n);if(u!=64){t=t+String.fromCharCode(r)}if(a!=64){t=t+String.fromCharCode(i)}}t=Base64._utf8_decode(t);return t},_utf8_encode:function(e){e=e.replace(/\r\n/g,"\n");var t="";for(var n=0;n127&&r<2048){t+=String.fromCharCode(r>>6|192);t+=String.fromCharCode(r&63|128)}else{t+=String.fromCharCode(r>>12|224);t+=String.fromCharCode(r>>6&63|128);t+=String.fromCharCode(r&63|128)}}return t},_utf8_decode:function(e){var t="";var n=0;var r=c1=c2=0;while(n191&&r<224){c2=e.charCodeAt(n+1);t+=String.fromCharCode((r&31)<<6|c2&63);n+=2}else{c2=e.charCodeAt(n+1);c3=e.charCodeAt(n+2);t+=String.fromCharCode((r&15)<<12|(c2&63)<<6|c3&63);n+=3}}return t}} 5 | 6 | export default Base64 7 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Log' 2 | export * from './read-file' 3 | export * from './svgs' 4 | export * from './types' 5 | -------------------------------------------------------------------------------- /src/utils/read-file.ts: -------------------------------------------------------------------------------- 1 | import type { PathLike } from 'fs-extra' 2 | import fs from 'fs-extra' 3 | import stripJsonComments from 'strip-json-comments' 4 | 5 | /** 6 | * Read and parse a JSON file. 7 | * 8 | * Note: The file can contain comments and trailing commas. 9 | */ 10 | export async function readJSON(filePath: PathLike) { 11 | const contents = await fs.readFile(filePath, { encoding: 'utf8' }) 12 | const stripped = stripJsonComments(contents, { trailingCommas: true }) 13 | return JSON.parse(stripped) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/svgs.ts: -------------------------------------------------------------------------------- 1 | import type { IconInfo } from '../loader' 2 | import Base64 from './base64' 3 | 4 | export function toDataUrl(str: string) { 5 | return `data:image/svg+xml;base64,${Base64.encode(str)}` 6 | } 7 | 8 | export function pathToSvg(info: IconInfo, fontSize: number) { 9 | return `${info.body}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export const isTruthy = (a: T | undefined): a is T => Boolean(a) 2 | -------------------------------------------------------------------------------- /test/fixture/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iconify.color": "#ddd", 3 | "iconify.customCollectionJsonPaths": [ 4 | "./custom-collection.json" 5 | ], 6 | "iconify.delimiters": [ 7 | ":" 8 | ], 9 | "iconify.preview.exclude": [ 10 | "**/exclude/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture/custom-collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "custom", 3 | "icons": { 4 | "icon1": { 5 | "body": "" 6 | } 7 | }, 8 | "width": 24, 9 | "height": 24 10 | } 11 | -------------------------------------------------------------------------------- /test/fixture/exclude/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /test/fixture/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
-------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["ESNext"], 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------