├── .github ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── build └── pipeline.yml ├── package-lock.json ├── package.json ├── src ├── configCompat.ts ├── data.ts ├── emmetHelper.ts ├── fileService.ts ├── test │ ├── emmetHelper.test.ts │ ├── expand.test.ts │ └── fileService.test.ts └── typings │ └── thenable.d.ts ├── testData ├── custom-snippets-invalid-json │ └── snippets.json ├── custom-snippets-profile │ ├── snippets.json │ └── syntaxProfiles.json └── custom-snippets-without-inheritance │ └── snippets.json ├── thirdpartynotices.txt ├── tsconfig.esm.json └── tsconfig.json /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | This PR fixes # -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | on: push 3 | permissions: {} 4 | jobs: 5 | yarn-ubuntu: 6 | name: yarn-ubuntu 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out repository 10 | uses: actions/checkout@v2 11 | with: 12 | persist-credentials: false 13 | - name: Run yarn 14 | run: | 15 | yarn && yarn compile && yarn test 16 | yarn-windows: 17 | name: yarn-windows 18 | runs-on: windows-latest 19 | steps: 20 | - name: Check out repository 21 | uses: actions/checkout@v2 22 | with: 23 | persist-credentials: false 24 | - name: Run yarn 25 | run: | 26 | yarn && yarn compile && yarn test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | .vscode-test 4 | *.vsix 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | build 4 | lib/cjs/test 5 | lib/esm/test 6 | node_modules 7 | src 8 | test 9 | testData 10 | .gitignore 11 | tsconfig.esm.json 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "-u", "tdd", 14 | "--timeout", "999999", 15 | "--colors", "--recursive", 16 | "${workspaceFolder}/lib/cjs/test/**/*.test.js" 17 | ], 18 | "console": "integratedTerminal", 19 | "internalConsoleOptions": "neverOpen", 20 | "skipFiles": [ 21 | "/**/*.js" 22 | ], 23 | "sourceMaps": true, 24 | "outFiles": [ 25 | "${workspaceFolder}/lib/cjs/**/*.js" 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.branchProtection": ["main"], 3 | "git.branchRandomName.enable": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft 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 | # vscode-emmet-helper 2 | A helper module to use emmet modules with Visual Studio Code 3 | 4 | 5 | Visual Studio Code extensions that provide language service and want to provide emmet abbreviation expansions 6 | in auto-complete can include this module and use the `doComplete` method. 7 | Just pass the one of the emmet supported syntaxes that you would like the completion provider to use along with other parameters that you would generally pass to a completion provider. 8 | 9 | If `emmet.includeLanguages` has a mapping for your language, then the builit-in emmet extension will provide 10 | html emmet abbreviations. Ask the user to remove the mapping, if your extension decides to provide 11 | emmet completions using this module 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /build/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(Date:yyyyMMdd)$(Rev:.r) 2 | 3 | trigger: 4 | branches: 5 | include: 6 | - main 7 | pr: none 8 | 9 | resources: 10 | repositories: 11 | - repository: templates 12 | type: github 13 | name: microsoft/vscode-engineering 14 | ref: main 15 | endpoint: Monaco 16 | 17 | parameters: 18 | - name: publishPackage 19 | displayName: 🚀 Publish emmet-helper 20 | type: boolean 21 | default: false 22 | 23 | extends: 24 | template: azure-pipelines/npm-package/pipeline.yml@templates 25 | parameters: 26 | npmPackages: 27 | - name: emmet-helper 28 | 29 | buildSteps: 30 | - script: npm ci 31 | displayName: Install dependencies 32 | 33 | - script: npm run compile && npm run compile-esm 34 | displayName: Compile npm package 35 | 36 | testPlatforms: 37 | - name: Linux 38 | nodeVersions: 39 | - 20.x 40 | - name: MacOS 41 | nodeVersions: 42 | - 20.x 43 | - name: Windows 44 | nodeVersions: 45 | - 20.x 46 | 47 | testSteps: 48 | - script: npm ci 49 | displayName: Install dependencies 50 | 51 | - script: npm run compile && npm run compile-esm 52 | displayName: Compile npm package 53 | 54 | - script: npm run test 55 | displayName: Test npm package 56 | 57 | publishPackage: ${{ parameters.publishPackage }} -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode/emmet-helper", 3 | "version": "2.11.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@vscode/emmet-helper", 9 | "version": "2.11.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "emmet": "^2.4.3", 13 | "jsonc-parser": "^2.3.0", 14 | "vscode-languageserver-textdocument": "^1.0.1", 15 | "vscode-languageserver-types": "^3.15.1", 16 | "vscode-uri": "^3.0.8" 17 | }, 18 | "devDependencies": { 19 | "@types/mocha": "^10.0.1", 20 | "@types/node": "^20.0.0", 21 | "@types/vscode": "^1.78.0", 22 | "mocha": "^10.2.0", 23 | "typescript": "^5.6.3" 24 | } 25 | }, 26 | "node_modules/@emmetio/abbreviation": { 27 | "version": "2.3.3", 28 | "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", 29 | "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@emmetio/scanner": "^1.0.4" 33 | } 34 | }, 35 | "node_modules/@emmetio/css-abbreviation": { 36 | "version": "2.1.8", 37 | "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", 38 | "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", 39 | "license": "MIT", 40 | "dependencies": { 41 | "@emmetio/scanner": "^1.0.4" 42 | } 43 | }, 44 | "node_modules/@emmetio/scanner": { 45 | "version": "1.0.4", 46 | "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", 47 | "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==", 48 | "license": "MIT" 49 | }, 50 | "node_modules/@types/mocha": { 51 | "version": "10.0.1", 52 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", 53 | "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", 54 | "dev": true, 55 | "license": "MIT" 56 | }, 57 | "node_modules/@types/node": { 58 | "version": "20.17.6", 59 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", 60 | "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", 61 | "dev": true, 62 | "license": "MIT", 63 | "dependencies": { 64 | "undici-types": "~6.19.2" 65 | } 66 | }, 67 | "node_modules/@types/vscode": { 68 | "version": "1.95.0", 69 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.95.0.tgz", 70 | "integrity": "sha512-0LBD8TEiNbet3NvWsmn59zLzOFu/txSlGxnv5yAFHCrhG9WvAnR3IvfHzMOs2aeWqgvNjq9pO99IUw8d3n+unw==", 71 | "dev": true, 72 | "license": "MIT" 73 | }, 74 | "node_modules/ansi-colors": { 75 | "version": "4.1.3", 76 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", 77 | "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", 78 | "dev": true, 79 | "license": "MIT", 80 | "engines": { 81 | "node": ">=6" 82 | } 83 | }, 84 | "node_modules/ansi-regex": { 85 | "version": "5.0.1", 86 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 87 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 88 | "dev": true, 89 | "license": "MIT", 90 | "engines": { 91 | "node": ">=8" 92 | } 93 | }, 94 | "node_modules/ansi-styles": { 95 | "version": "4.3.0", 96 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 97 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 98 | "dev": true, 99 | "license": "MIT", 100 | "dependencies": { 101 | "color-convert": "^2.0.1" 102 | }, 103 | "engines": { 104 | "node": ">=8" 105 | }, 106 | "funding": { 107 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 108 | } 109 | }, 110 | "node_modules/anymatch": { 111 | "version": "3.1.3", 112 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 113 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 114 | "dev": true, 115 | "license": "ISC", 116 | "dependencies": { 117 | "normalize-path": "^3.0.0", 118 | "picomatch": "^2.0.4" 119 | }, 120 | "engines": { 121 | "node": ">= 8" 122 | } 123 | }, 124 | "node_modules/argparse": { 125 | "version": "2.0.1", 126 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 127 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 128 | "dev": true, 129 | "license": "Python-2.0" 130 | }, 131 | "node_modules/balanced-match": { 132 | "version": "1.0.2", 133 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 134 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 135 | "dev": true, 136 | "license": "MIT" 137 | }, 138 | "node_modules/binary-extensions": { 139 | "version": "2.2.0", 140 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 141 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 142 | "dev": true, 143 | "license": "MIT", 144 | "engines": { 145 | "node": ">=8" 146 | } 147 | }, 148 | "node_modules/brace-expansion": { 149 | "version": "2.0.1", 150 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 151 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 152 | "dev": true, 153 | "license": "MIT", 154 | "dependencies": { 155 | "balanced-match": "^1.0.0" 156 | } 157 | }, 158 | "node_modules/braces": { 159 | "version": "3.0.3", 160 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 161 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 162 | "dev": true, 163 | "license": "MIT", 164 | "dependencies": { 165 | "fill-range": "^7.1.1" 166 | }, 167 | "engines": { 168 | "node": ">=8" 169 | } 170 | }, 171 | "node_modules/browser-stdout": { 172 | "version": "1.3.1", 173 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 174 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 175 | "dev": true, 176 | "license": "ISC" 177 | }, 178 | "node_modules/camelcase": { 179 | "version": "6.3.0", 180 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 181 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 182 | "dev": true, 183 | "license": "MIT", 184 | "engines": { 185 | "node": ">=10" 186 | }, 187 | "funding": { 188 | "url": "https://github.com/sponsors/sindresorhus" 189 | } 190 | }, 191 | "node_modules/chalk": { 192 | "version": "4.1.2", 193 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 194 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 195 | "dev": true, 196 | "license": "MIT", 197 | "dependencies": { 198 | "ansi-styles": "^4.1.0", 199 | "supports-color": "^7.1.0" 200 | }, 201 | "engines": { 202 | "node": ">=10" 203 | }, 204 | "funding": { 205 | "url": "https://github.com/chalk/chalk?sponsor=1" 206 | } 207 | }, 208 | "node_modules/chalk/node_modules/supports-color": { 209 | "version": "7.2.0", 210 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 211 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 212 | "dev": true, 213 | "license": "MIT", 214 | "dependencies": { 215 | "has-flag": "^4.0.0" 216 | }, 217 | "engines": { 218 | "node": ">=8" 219 | } 220 | }, 221 | "node_modules/chokidar": { 222 | "version": "3.5.3", 223 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 224 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 225 | "dev": true, 226 | "funding": [ 227 | { 228 | "type": "individual", 229 | "url": "https://paulmillr.com/funding/" 230 | } 231 | ], 232 | "license": "MIT", 233 | "dependencies": { 234 | "anymatch": "~3.1.2", 235 | "braces": "~3.0.2", 236 | "glob-parent": "~5.1.2", 237 | "is-binary-path": "~2.1.0", 238 | "is-glob": "~4.0.1", 239 | "normalize-path": "~3.0.0", 240 | "readdirp": "~3.6.0" 241 | }, 242 | "engines": { 243 | "node": ">= 8.10.0" 244 | }, 245 | "optionalDependencies": { 246 | "fsevents": "~2.3.2" 247 | } 248 | }, 249 | "node_modules/cliui": { 250 | "version": "7.0.4", 251 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 252 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 253 | "dev": true, 254 | "license": "ISC", 255 | "dependencies": { 256 | "string-width": "^4.2.0", 257 | "strip-ansi": "^6.0.0", 258 | "wrap-ansi": "^7.0.0" 259 | } 260 | }, 261 | "node_modules/color-convert": { 262 | "version": "2.0.1", 263 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 264 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 265 | "dev": true, 266 | "license": "MIT", 267 | "dependencies": { 268 | "color-name": "~1.1.4" 269 | }, 270 | "engines": { 271 | "node": ">=7.0.0" 272 | } 273 | }, 274 | "node_modules/color-name": { 275 | "version": "1.1.4", 276 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 277 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 278 | "dev": true, 279 | "license": "MIT" 280 | }, 281 | "node_modules/debug": { 282 | "version": "4.3.7", 283 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 284 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 285 | "dev": true, 286 | "license": "MIT", 287 | "dependencies": { 288 | "ms": "^2.1.3" 289 | }, 290 | "engines": { 291 | "node": ">=6.0" 292 | }, 293 | "peerDependenciesMeta": { 294 | "supports-color": { 295 | "optional": true 296 | } 297 | } 298 | }, 299 | "node_modules/decamelize": { 300 | "version": "4.0.0", 301 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 302 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 303 | "dev": true, 304 | "license": "MIT", 305 | "engines": { 306 | "node": ">=10" 307 | }, 308 | "funding": { 309 | "url": "https://github.com/sponsors/sindresorhus" 310 | } 311 | }, 312 | "node_modules/diff": { 313 | "version": "5.2.0", 314 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", 315 | "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", 316 | "dev": true, 317 | "license": "BSD-3-Clause", 318 | "engines": { 319 | "node": ">=0.3.1" 320 | } 321 | }, 322 | "node_modules/emmet": { 323 | "version": "2.4.11", 324 | "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", 325 | "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", 326 | "license": "MIT", 327 | "workspaces": [ 328 | "./packages/scanner", 329 | "./packages/abbreviation", 330 | "./packages/css-abbreviation", 331 | "./" 332 | ], 333 | "dependencies": { 334 | "@emmetio/abbreviation": "^2.3.3", 335 | "@emmetio/css-abbreviation": "^2.1.8" 336 | } 337 | }, 338 | "node_modules/emoji-regex": { 339 | "version": "8.0.0", 340 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 341 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 342 | "dev": true, 343 | "license": "MIT" 344 | }, 345 | "node_modules/escalade": { 346 | "version": "3.1.1", 347 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 348 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 349 | "dev": true, 350 | "license": "MIT", 351 | "engines": { 352 | "node": ">=6" 353 | } 354 | }, 355 | "node_modules/escape-string-regexp": { 356 | "version": "4.0.0", 357 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 358 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 359 | "dev": true, 360 | "license": "MIT", 361 | "engines": { 362 | "node": ">=10" 363 | }, 364 | "funding": { 365 | "url": "https://github.com/sponsors/sindresorhus" 366 | } 367 | }, 368 | "node_modules/fill-range": { 369 | "version": "7.1.1", 370 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 371 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 372 | "dev": true, 373 | "license": "MIT", 374 | "dependencies": { 375 | "to-regex-range": "^5.0.1" 376 | }, 377 | "engines": { 378 | "node": ">=8" 379 | } 380 | }, 381 | "node_modules/find-up": { 382 | "version": "5.0.0", 383 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 384 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 385 | "dev": true, 386 | "license": "MIT", 387 | "dependencies": { 388 | "locate-path": "^6.0.0", 389 | "path-exists": "^4.0.0" 390 | }, 391 | "engines": { 392 | "node": ">=10" 393 | }, 394 | "funding": { 395 | "url": "https://github.com/sponsors/sindresorhus" 396 | } 397 | }, 398 | "node_modules/flat": { 399 | "version": "5.0.2", 400 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 401 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 402 | "dev": true, 403 | "license": "BSD-3-Clause", 404 | "bin": { 405 | "flat": "cli.js" 406 | } 407 | }, 408 | "node_modules/fs.realpath": { 409 | "version": "1.0.0", 410 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 411 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 412 | "dev": true, 413 | "license": "ISC" 414 | }, 415 | "node_modules/fsevents": { 416 | "version": "2.3.3", 417 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 418 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 419 | "dev": true, 420 | "hasInstallScript": true, 421 | "license": "MIT", 422 | "optional": true, 423 | "os": [ 424 | "darwin" 425 | ], 426 | "engines": { 427 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 428 | } 429 | }, 430 | "node_modules/get-caller-file": { 431 | "version": "2.0.5", 432 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 433 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 434 | "dev": true, 435 | "license": "ISC", 436 | "engines": { 437 | "node": "6.* || 8.* || >= 10.*" 438 | } 439 | }, 440 | "node_modules/glob-parent": { 441 | "version": "5.1.2", 442 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 443 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 444 | "dev": true, 445 | "license": "ISC", 446 | "dependencies": { 447 | "is-glob": "^4.0.1" 448 | }, 449 | "engines": { 450 | "node": ">= 6" 451 | } 452 | }, 453 | "node_modules/has-flag": { 454 | "version": "4.0.0", 455 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 456 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 457 | "dev": true, 458 | "license": "MIT", 459 | "engines": { 460 | "node": ">=8" 461 | } 462 | }, 463 | "node_modules/he": { 464 | "version": "1.2.0", 465 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 466 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 467 | "dev": true, 468 | "license": "MIT", 469 | "bin": { 470 | "he": "bin/he" 471 | } 472 | }, 473 | "node_modules/inflight": { 474 | "version": "1.0.6", 475 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 476 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 477 | "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", 478 | "dev": true, 479 | "license": "ISC", 480 | "dependencies": { 481 | "once": "^1.3.0", 482 | "wrappy": "1" 483 | } 484 | }, 485 | "node_modules/inherits": { 486 | "version": "2.0.4", 487 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 488 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 489 | "dev": true, 490 | "license": "ISC" 491 | }, 492 | "node_modules/is-binary-path": { 493 | "version": "2.1.0", 494 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 495 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 496 | "dev": true, 497 | "license": "MIT", 498 | "dependencies": { 499 | "binary-extensions": "^2.0.0" 500 | }, 501 | "engines": { 502 | "node": ">=8" 503 | } 504 | }, 505 | "node_modules/is-extglob": { 506 | "version": "2.1.1", 507 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 508 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 509 | "dev": true, 510 | "license": "MIT", 511 | "engines": { 512 | "node": ">=0.10.0" 513 | } 514 | }, 515 | "node_modules/is-fullwidth-code-point": { 516 | "version": "3.0.0", 517 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 518 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 519 | "dev": true, 520 | "license": "MIT", 521 | "engines": { 522 | "node": ">=8" 523 | } 524 | }, 525 | "node_modules/is-glob": { 526 | "version": "4.0.3", 527 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 528 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 529 | "dev": true, 530 | "license": "MIT", 531 | "dependencies": { 532 | "is-extglob": "^2.1.1" 533 | }, 534 | "engines": { 535 | "node": ">=0.10.0" 536 | } 537 | }, 538 | "node_modules/is-number": { 539 | "version": "7.0.0", 540 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 541 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 542 | "dev": true, 543 | "license": "MIT", 544 | "engines": { 545 | "node": ">=0.12.0" 546 | } 547 | }, 548 | "node_modules/is-plain-obj": { 549 | "version": "2.1.0", 550 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 551 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 552 | "dev": true, 553 | "license": "MIT", 554 | "engines": { 555 | "node": ">=8" 556 | } 557 | }, 558 | "node_modules/is-unicode-supported": { 559 | "version": "0.1.0", 560 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 561 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 562 | "dev": true, 563 | "license": "MIT", 564 | "engines": { 565 | "node": ">=10" 566 | }, 567 | "funding": { 568 | "url": "https://github.com/sponsors/sindresorhus" 569 | } 570 | }, 571 | "node_modules/js-yaml": { 572 | "version": "4.1.0", 573 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 574 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 575 | "dev": true, 576 | "license": "MIT", 577 | "dependencies": { 578 | "argparse": "^2.0.1" 579 | }, 580 | "bin": { 581 | "js-yaml": "bin/js-yaml.js" 582 | } 583 | }, 584 | "node_modules/jsonc-parser": { 585 | "version": "2.3.1", 586 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", 587 | "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", 588 | "license": "MIT" 589 | }, 590 | "node_modules/locate-path": { 591 | "version": "6.0.0", 592 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 593 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 594 | "dev": true, 595 | "license": "MIT", 596 | "dependencies": { 597 | "p-locate": "^5.0.0" 598 | }, 599 | "engines": { 600 | "node": ">=10" 601 | }, 602 | "funding": { 603 | "url": "https://github.com/sponsors/sindresorhus" 604 | } 605 | }, 606 | "node_modules/log-symbols": { 607 | "version": "4.1.0", 608 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 609 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 610 | "dev": true, 611 | "license": "MIT", 612 | "dependencies": { 613 | "chalk": "^4.1.0", 614 | "is-unicode-supported": "^0.1.0" 615 | }, 616 | "engines": { 617 | "node": ">=10" 618 | }, 619 | "funding": { 620 | "url": "https://github.com/sponsors/sindresorhus" 621 | } 622 | }, 623 | "node_modules/minimatch": { 624 | "version": "5.1.6", 625 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", 626 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", 627 | "dev": true, 628 | "license": "ISC", 629 | "dependencies": { 630 | "brace-expansion": "^2.0.1" 631 | }, 632 | "engines": { 633 | "node": ">=10" 634 | } 635 | }, 636 | "node_modules/mocha": { 637 | "version": "10.8.2", 638 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", 639 | "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", 640 | "dev": true, 641 | "license": "MIT", 642 | "dependencies": { 643 | "ansi-colors": "^4.1.3", 644 | "browser-stdout": "^1.3.1", 645 | "chokidar": "^3.5.3", 646 | "debug": "^4.3.5", 647 | "diff": "^5.2.0", 648 | "escape-string-regexp": "^4.0.0", 649 | "find-up": "^5.0.0", 650 | "glob": "^8.1.0", 651 | "he": "^1.2.0", 652 | "js-yaml": "^4.1.0", 653 | "log-symbols": "^4.1.0", 654 | "minimatch": "^5.1.6", 655 | "ms": "^2.1.3", 656 | "serialize-javascript": "^6.0.2", 657 | "strip-json-comments": "^3.1.1", 658 | "supports-color": "^8.1.1", 659 | "workerpool": "^6.5.1", 660 | "yargs": "^16.2.0", 661 | "yargs-parser": "^20.2.9", 662 | "yargs-unparser": "^2.0.0" 663 | }, 664 | "bin": { 665 | "_mocha": "bin/_mocha", 666 | "mocha": "bin/mocha.js" 667 | }, 668 | "engines": { 669 | "node": ">= 14.0.0" 670 | } 671 | }, 672 | "node_modules/mocha/node_modules/glob": { 673 | "version": "8.1.0", 674 | "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", 675 | "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", 676 | "deprecated": "Glob versions prior to v9 are no longer supported", 677 | "dev": true, 678 | "license": "ISC", 679 | "dependencies": { 680 | "fs.realpath": "^1.0.0", 681 | "inflight": "^1.0.4", 682 | "inherits": "2", 683 | "minimatch": "^5.0.1", 684 | "once": "^1.3.0" 685 | }, 686 | "engines": { 687 | "node": ">=12" 688 | }, 689 | "funding": { 690 | "url": "https://github.com/sponsors/isaacs" 691 | } 692 | }, 693 | "node_modules/ms": { 694 | "version": "2.1.3", 695 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 696 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 697 | "dev": true, 698 | "license": "MIT" 699 | }, 700 | "node_modules/normalize-path": { 701 | "version": "3.0.0", 702 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 703 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 704 | "dev": true, 705 | "license": "MIT", 706 | "engines": { 707 | "node": ">=0.10.0" 708 | } 709 | }, 710 | "node_modules/once": { 711 | "version": "1.4.0", 712 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 713 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 714 | "dev": true, 715 | "license": "ISC", 716 | "dependencies": { 717 | "wrappy": "1" 718 | } 719 | }, 720 | "node_modules/p-limit": { 721 | "version": "3.1.0", 722 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 723 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 724 | "dev": true, 725 | "license": "MIT", 726 | "dependencies": { 727 | "yocto-queue": "^0.1.0" 728 | }, 729 | "engines": { 730 | "node": ">=10" 731 | }, 732 | "funding": { 733 | "url": "https://github.com/sponsors/sindresorhus" 734 | } 735 | }, 736 | "node_modules/p-locate": { 737 | "version": "5.0.0", 738 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 739 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 740 | "dev": true, 741 | "license": "MIT", 742 | "dependencies": { 743 | "p-limit": "^3.0.2" 744 | }, 745 | "engines": { 746 | "node": ">=10" 747 | }, 748 | "funding": { 749 | "url": "https://github.com/sponsors/sindresorhus" 750 | } 751 | }, 752 | "node_modules/path-exists": { 753 | "version": "4.0.0", 754 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 755 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 756 | "dev": true, 757 | "license": "MIT", 758 | "engines": { 759 | "node": ">=8" 760 | } 761 | }, 762 | "node_modules/picomatch": { 763 | "version": "2.3.1", 764 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 765 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 766 | "dev": true, 767 | "license": "MIT", 768 | "engines": { 769 | "node": ">=8.6" 770 | }, 771 | "funding": { 772 | "url": "https://github.com/sponsors/jonschlinkert" 773 | } 774 | }, 775 | "node_modules/randombytes": { 776 | "version": "2.1.0", 777 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 778 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 779 | "dev": true, 780 | "license": "MIT", 781 | "dependencies": { 782 | "safe-buffer": "^5.1.0" 783 | } 784 | }, 785 | "node_modules/readdirp": { 786 | "version": "3.6.0", 787 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 788 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 789 | "dev": true, 790 | "license": "MIT", 791 | "dependencies": { 792 | "picomatch": "^2.2.1" 793 | }, 794 | "engines": { 795 | "node": ">=8.10.0" 796 | } 797 | }, 798 | "node_modules/require-directory": { 799 | "version": "2.1.1", 800 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 801 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 802 | "dev": true, 803 | "license": "MIT", 804 | "engines": { 805 | "node": ">=0.10.0" 806 | } 807 | }, 808 | "node_modules/safe-buffer": { 809 | "version": "5.2.1", 810 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 811 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 812 | "dev": true, 813 | "funding": [ 814 | { 815 | "type": "github", 816 | "url": "https://github.com/sponsors/feross" 817 | }, 818 | { 819 | "type": "patreon", 820 | "url": "https://www.patreon.com/feross" 821 | }, 822 | { 823 | "type": "consulting", 824 | "url": "https://feross.org/support" 825 | } 826 | ], 827 | "license": "MIT" 828 | }, 829 | "node_modules/serialize-javascript": { 830 | "version": "6.0.2", 831 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", 832 | "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", 833 | "dev": true, 834 | "license": "BSD-3-Clause", 835 | "dependencies": { 836 | "randombytes": "^2.1.0" 837 | } 838 | }, 839 | "node_modules/string-width": { 840 | "version": "4.2.3", 841 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 842 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 843 | "dev": true, 844 | "license": "MIT", 845 | "dependencies": { 846 | "emoji-regex": "^8.0.0", 847 | "is-fullwidth-code-point": "^3.0.0", 848 | "strip-ansi": "^6.0.1" 849 | }, 850 | "engines": { 851 | "node": ">=8" 852 | } 853 | }, 854 | "node_modules/strip-ansi": { 855 | "version": "6.0.1", 856 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 857 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 858 | "dev": true, 859 | "license": "MIT", 860 | "dependencies": { 861 | "ansi-regex": "^5.0.1" 862 | }, 863 | "engines": { 864 | "node": ">=8" 865 | } 866 | }, 867 | "node_modules/strip-json-comments": { 868 | "version": "3.1.1", 869 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 870 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 871 | "dev": true, 872 | "license": "MIT", 873 | "engines": { 874 | "node": ">=8" 875 | }, 876 | "funding": { 877 | "url": "https://github.com/sponsors/sindresorhus" 878 | } 879 | }, 880 | "node_modules/supports-color": { 881 | "version": "8.1.1", 882 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 883 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 884 | "dev": true, 885 | "license": "MIT", 886 | "dependencies": { 887 | "has-flag": "^4.0.0" 888 | }, 889 | "engines": { 890 | "node": ">=10" 891 | }, 892 | "funding": { 893 | "url": "https://github.com/chalk/supports-color?sponsor=1" 894 | } 895 | }, 896 | "node_modules/to-regex-range": { 897 | "version": "5.0.1", 898 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 899 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 900 | "dev": true, 901 | "license": "MIT", 902 | "dependencies": { 903 | "is-number": "^7.0.0" 904 | }, 905 | "engines": { 906 | "node": ">=8.0" 907 | } 908 | }, 909 | "node_modules/typescript": { 910 | "version": "5.6.3", 911 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", 912 | "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", 913 | "dev": true, 914 | "license": "Apache-2.0", 915 | "bin": { 916 | "tsc": "bin/tsc", 917 | "tsserver": "bin/tsserver" 918 | }, 919 | "engines": { 920 | "node": ">=14.17" 921 | } 922 | }, 923 | "node_modules/undici-types": { 924 | "version": "6.19.8", 925 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 926 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 927 | "dev": true, 928 | "license": "MIT" 929 | }, 930 | "node_modules/vscode-languageserver-textdocument": { 931 | "version": "1.0.12", 932 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", 933 | "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", 934 | "license": "MIT" 935 | }, 936 | "node_modules/vscode-languageserver-types": { 937 | "version": "3.17.5", 938 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", 939 | "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", 940 | "license": "MIT" 941 | }, 942 | "node_modules/vscode-uri": { 943 | "version": "3.0.8", 944 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", 945 | "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", 946 | "license": "MIT" 947 | }, 948 | "node_modules/workerpool": { 949 | "version": "6.5.1", 950 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", 951 | "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", 952 | "dev": true, 953 | "license": "Apache-2.0" 954 | }, 955 | "node_modules/wrap-ansi": { 956 | "version": "7.0.0", 957 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 958 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 959 | "dev": true, 960 | "license": "MIT", 961 | "dependencies": { 962 | "ansi-styles": "^4.0.0", 963 | "string-width": "^4.1.0", 964 | "strip-ansi": "^6.0.0" 965 | }, 966 | "engines": { 967 | "node": ">=10" 968 | }, 969 | "funding": { 970 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 971 | } 972 | }, 973 | "node_modules/wrappy": { 974 | "version": "1.0.2", 975 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 976 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 977 | "dev": true, 978 | "license": "ISC" 979 | }, 980 | "node_modules/y18n": { 981 | "version": "5.0.8", 982 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 983 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 984 | "dev": true, 985 | "license": "ISC", 986 | "engines": { 987 | "node": ">=10" 988 | } 989 | }, 990 | "node_modules/yargs": { 991 | "version": "16.2.0", 992 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 993 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 994 | "dev": true, 995 | "license": "MIT", 996 | "dependencies": { 997 | "cliui": "^7.0.2", 998 | "escalade": "^3.1.1", 999 | "get-caller-file": "^2.0.5", 1000 | "require-directory": "^2.1.1", 1001 | "string-width": "^4.2.0", 1002 | "y18n": "^5.0.5", 1003 | "yargs-parser": "^20.2.2" 1004 | }, 1005 | "engines": { 1006 | "node": ">=10" 1007 | } 1008 | }, 1009 | "node_modules/yargs-parser": { 1010 | "version": "20.2.9", 1011 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 1012 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", 1013 | "dev": true, 1014 | "license": "ISC", 1015 | "engines": { 1016 | "node": ">=10" 1017 | } 1018 | }, 1019 | "node_modules/yargs-unparser": { 1020 | "version": "2.0.0", 1021 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1022 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1023 | "dev": true, 1024 | "license": "MIT", 1025 | "dependencies": { 1026 | "camelcase": "^6.0.0", 1027 | "decamelize": "^4.0.0", 1028 | "flat": "^5.0.2", 1029 | "is-plain-obj": "^2.1.0" 1030 | }, 1031 | "engines": { 1032 | "node": ">=10" 1033 | } 1034 | }, 1035 | "node_modules/yocto-queue": { 1036 | "version": "0.1.0", 1037 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1038 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1039 | "dev": true, 1040 | "license": "MIT", 1041 | "engines": { 1042 | "node": ">=10" 1043 | }, 1044 | "funding": { 1045 | "url": "https://github.com/sponsors/sindresorhus" 1046 | } 1047 | } 1048 | } 1049 | } 1050 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode/emmet-helper", 3 | "version": "2.11.0", 4 | "description": "Helper to use emmet modules in Visual Studio Code", 5 | "main": "./lib/cjs/emmetHelper.js", 6 | "module": "./lib/esm/emmetHelper.js", 7 | "types": "./lib/cjs/emmetHelper.d.ts", 8 | "author": "Microsoft Corporation", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Microsoft/vscode-emmet-helper" 12 | }, 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/Microsoft/vscode-emmet-helper" 16 | }, 17 | "devDependencies": { 18 | "@types/mocha": "^10.0.1", 19 | "@types/node": "^20.0.0", 20 | "@types/vscode": "^1.78.0", 21 | "mocha": "^10.2.0", 22 | "typescript": "^5.6.3" 23 | }, 24 | "dependencies": { 25 | "emmet": "^2.4.3", 26 | "jsonc-parser": "^2.3.0", 27 | "vscode-languageserver-textdocument": "^1.0.1", 28 | "vscode-languageserver-types": "^3.15.1", 29 | "vscode-uri": "^3.0.8" 30 | }, 31 | "scripts": { 32 | "watch": "tsc -watch -p ./", 33 | "compile": "tsc -p ./", 34 | "compile-esm": "tsc -p ./tsconfig.esm.json", 35 | "test": "mocha lib/cjs/test" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/configCompat.ts: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 Sergey Chikuyonok 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | export interface SnippetsMap { 26 | [name: string]: string; 27 | } 28 | 29 | /** 30 | * Parses raw snippets definitions with possibly multiple keys into a plan 31 | * snippet map 32 | */ 33 | export function parseSnippets(snippets: SnippetsMap): SnippetsMap { 34 | const result: SnippetsMap = {}; 35 | Object.keys(snippets).forEach(k => { 36 | for (const name of k.split('|')) { 37 | result[name] = snippets[k]; 38 | } 39 | }); 40 | 41 | return result; 42 | } 43 | 44 | /** 45 | * List of all known syntaxes 46 | */ 47 | export const syntaxes = { 48 | markup: ['html', 'xml', 'xsl', 'jsx', 'js', 'pug', 'slim', 'haml', 'vue'], 49 | stylesheet: ['css', 'sass', 'scss', 'less', 'sss', 'stylus'] 50 | }; -------------------------------------------------------------------------------- /src/data.ts: -------------------------------------------------------------------------------- 1 | export const cssData = { 2 | "properties": ["additive-symbols", "align-content", "align-items", "justify-items", "justify-self", "justify-items", "align-self", "all", "alt", "animation", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode", "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", "backface-visibility", "background", "background-attachment", "background-blend-mode", "background-clip", "background-color", "background-image", "background-origin", "background-position", "background-position-x", "background-position-y", "background-repeat", "background-size", "behavior", "block-size", "border", "border-block-end", "border-block-start", "border-block-end-color", "border-block-start-color", "border-block-end-style", "border-block-start-style", "border-block-end-width", "border-block-start-width", "border-bottom", "border-bottom-color", "border-bottom-left-radius", "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", "border-collapse", "border-color", "border-image", "border-image-outset", "border-image-repeat", "border-image-slice", "border-image-source", "border-image-width", "border-inline-end", "border-inline-start", "border-inline-end-color", "border-inline-start-color", "border-inline-end-style", "border-inline-start-style", "border-inline-end-width", "border-inline-start-width", "border-left", "border-left-color", "border-left-style", "border-left-width", "border-radius", "border-right", "border-right-color", "border-right-style", "border-right-width", "border-spacing", "border-style", "border-top", "border-top-color", "border-top-left-radius", "border-top-right-radius", "border-top-style", "border-top-width", "border-width", "bottom", "box-decoration-break", "box-shadow", "box-sizing", "break-after", "break-before", "break-inside", "caption-side", "caret-color", "clear", "clip", "clip-path", "clip-rule", "color", "color-interpolation-filters", "column-count", "column-fill", "column-gap", "column-rule", "column-rule-color", "column-rule-style", "column-rule-width", "columns", "column-span", "column-width", "contain", "content", "counter-increment", "counter-reset", "cursor", "direction", "display", "empty-cells", "enable-background", "fallback", "fill", "fill-opacity", "fill-rule", "filter", "flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", "float", "flood-color", "flood-opacity", "font", "font-family", "font-feature-settings", "font-kerning", "font-language-override", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-synthesis", "font-variant", "font-variant-alternates", "font-variant-caps", "font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric", "font-variant-position", "font-weight", "glyph-orientation-horizontal", "glyph-orientation-vertical", "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows", "grid-column", "grid-column-end", "grid-column-gap", "grid-column-start", "grid-gap", "grid-row", "grid-row-end", "grid-row-gap", "grid-row-start", "grid-template", "grid-template-areas", "grid-template-columns", "grid-template-rows", "height", "hyphens", "image-orientation", "image-rendering", "ime-mode", "inline-size", "isolation", "justify-content", "kerning", "left", "letter-spacing", "lighting-color", "line-break", "line-height", "list-style", "list-style-image", "list-style-position", "list-style-type", "margin", "margin-block-end", "margin-block-start", "margin-bottom", "margin-inline-end", "margin-inline-start", "margin-left", "margin-right", "margin-top", "marker", "marker-end", "marker-mid", "marker-start", "mask-type", "max-block-size", "max-height", "max-inline-size", "max-width", "min-block-size", "min-height", "min-inline-size", "min-width", "mix-blend-mode", "motion", "motion-offset", "motion-path", "motion-rotation", "-moz-animation", "-moz-animation-delay", "-moz-animation-direction", "-moz-animation-duration", "-moz-animation-iteration-count", "-moz-animation-name", "-moz-animation-play-state", "-moz-animation-timing-function", "-moz-appearance", "-moz-backface-visibility", "-moz-background-clip", "-moz-background-inline-policy", "-moz-background-origin", "-moz-border-bottom-colors", "-moz-border-image", "-moz-border-left-colors", "-moz-border-right-colors", "-moz-border-top-colors", "-moz-box-align", "-moz-box-direction", "-moz-box-flex", "-moz-box-flexgroup", "-moz-box-ordinal-group", "-moz-box-orient", "-moz-box-pack", "-moz-box-sizing", "-moz-column-count", "-moz-column-gap", "-moz-column-rule", "-moz-column-rule-color", "-moz-column-rule-style", "-moz-column-rule-width", "-moz-columns", "-moz-column-width", "-moz-font-feature-settings", "-moz-hyphens", "-moz-perspective", "-moz-perspective-origin", "-moz-text-align-last", "-moz-text-decoration-color", "-moz-text-decoration-line", "-moz-text-decoration-style", "-moz-text-size-adjust", "-moz-transform", "-moz-transform-origin", "-moz-transition", "-moz-transition-delay", "-moz-transition-duration", "-moz-transition-property", "-moz-transition-timing-function", "-moz-user-focus", "-moz-user-select", "-ms-accelerator", "-ms-behavior", "-ms-block-progression", "-ms-content-zoom-chaining", "-ms-content-zooming", "-ms-content-zoom-limit", "-ms-content-zoom-limit-max", "-ms-content-zoom-limit-min", "-ms-content-zoom-snap", "-ms-content-zoom-snap-points", "-ms-content-zoom-snap-type", "-ms-filter", "-ms-flex", "-ms-flex-align", "-ms-flex-direction", "-ms-flex-flow", "-ms-flex-item-align", "-ms-flex-line-pack", "-ms-flex-order", "-ms-flex-pack", "-ms-flex-wrap", "-ms-flow-from", "-ms-flow-into", "-ms-grid-column", "-ms-grid-column-align", "-ms-grid-columns", "-ms-grid-column-span", "-ms-grid-layer", "-ms-grid-row", "-ms-grid-row-align", "-ms-grid-rows", "-ms-grid-row-span", "-ms-high-contrast-adjust", "-ms-hyphenate-limit-chars", "-ms-hyphenate-limit-lines", "-ms-hyphenate-limit-zone", "-ms-hyphens", "-ms-ime-mode", "-ms-interpolation-mode", "-ms-layout-grid", "-ms-layout-grid-char", "-ms-layout-grid-line", "-ms-layout-grid-mode", "-ms-layout-grid-type", "-ms-line-break", "-ms-overflow-style", "-ms-perspective", "-ms-perspective-origin", "-ms-perspective-origin-x", "-ms-perspective-origin-y", "-ms-progress-appearance", "-ms-scrollbar-3dlight-color", "-ms-scrollbar-arrow-color", "-ms-scrollbar-base-color", "-ms-scrollbar-darkshadow-color", "-ms-scrollbar-face-color", "-ms-scrollbar-highlight-color", "-ms-scrollbar-shadow-color", "-ms-scrollbar-track-color", "-ms-scroll-chaining", "-ms-scroll-limit", "-ms-scroll-limit-x-max", "-ms-scroll-limit-x-min", "-ms-scroll-limit-y-max", "-ms-scroll-limit-y-min", "-ms-scroll-rails", "-ms-scroll-snap-points-x", "-ms-scroll-snap-points-y", "-ms-scroll-snap-type", "-ms-scroll-snap-x", "-ms-scroll-snap-y", "-ms-scroll-translation", "-ms-text-align-last", "-ms-text-autospace", "-ms-text-combine-horizontal", "-ms-text-justify", "-ms-text-kashida-space", "-ms-text-overflow", "-ms-text-size-adjust", "-ms-text-underline-position", "-ms-touch-action", "-ms-touch-select", "-ms-transform", "-ms-transform-origin", "-ms-transform-origin-x", "-ms-transform-origin-y", "-ms-transform-origin-z", "-ms-user-select", "-ms-word-break", "-ms-word-wrap", "-ms-wrap-flow", "-ms-wrap-margin", "-ms-wrap-through", "-ms-writing-mode", "-ms-zoom", "-ms-zoom-animation", "nav-down", "nav-index", "nav-left", "nav-right", "nav-up", "negative", "-o-animation", "-o-animation-delay", "-o-animation-direction", "-o-animation-duration", "-o-animation-fill-mode", "-o-animation-iteration-count", "-o-animation-name", "-o-animation-play-state", "-o-animation-timing-function", "object-fit", "object-position", "-o-border-image", "-o-object-fit", "-o-object-position", "opacity", "order", "orphans", "-o-table-baseline", "-o-tab-size", "-o-text-overflow", "-o-transform", "-o-transform-origin", "-o-transition", "-o-transition-delay", "-o-transition-duration", "-o-transition-property", "-o-transition-timing-function", "offset-block-end", "offset-block-start", "offset-inline-end", "offset-inline-start", "outline", "outline-color", "outline-offset", "outline-style", "outline-width", "overflow", "overflow-wrap", "overflow-x", "overflow-y", "pad", "padding", "padding-bottom", "padding-block-end", "padding-block-start", "padding-inline-end", "padding-inline-start", "padding-left", "padding-right", "padding-top", "page-break-after", "page-break-before", "page-break-inside", "paint-order", "perspective", "perspective-origin", "pointer-events", "position", "prefix", "quotes", "range", "resize", "right", "ruby-align", "ruby-overhang", "ruby-position", "ruby-span", "scrollbar-3dlight-color", "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-darkshadow-color", "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color", "scrollbar-track-color", "scroll-behavior", "scroll-snap-coordinate", "scroll-snap-destination", "scroll-snap-points-x", "scroll-snap-points-y", "scroll-snap-type", "shape-image-threshold", "shape-margin", "shape-outside", "shape-rendering", "size", "src", "stop-color", "stop-opacity", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "suffix", "system", "symbols", "table-layout", "tab-size", "text-align", "text-align-last", "text-anchor", "text-decoration", "text-decoration-color", "text-decoration-line", "text-decoration-style", "text-indent", "text-justify", "text-orientation", "text-overflow", "text-rendering", "text-shadow", "text-transform", "text-underline-position", "top", "touch-action", "transform", "transform-origin", "transform-style", "transition", "transition-delay", "transition-duration", "transition-property", "transition-timing-function", "unicode-bidi", "unicode-range", "user-select", "vertical-align", "visibility", "-webkit-animation", "-webkit-animation-delay", "-webkit-animation-direction", "-webkit-animation-duration", "-webkit-animation-fill-mode", "-webkit-animation-iteration-count", "-webkit-animation-name", "-webkit-animation-play-state", "-webkit-animation-timing-function", "-webkit-appearance", "-webkit-backdrop-filter", "-webkit-backface-visibility", "-webkit-background-clip", "-webkit-background-composite", "-webkit-background-origin", "-webkit-border-image", "-webkit-box-align", "-webkit-box-direction", "-webkit-box-flex", "-webkit-box-flex-group", "-webkit-box-ordinal-group", "-webkit-box-orient", "-webkit-box-pack", "-webkit-box-reflect", "-webkit-box-sizing", "-webkit-break-after", "-webkit-break-before", "-webkit-break-inside", "-webkit-column-break-after", "-webkit-column-break-before", "-webkit-column-break-inside", "-webkit-column-count", "-webkit-column-gap", "-webkit-column-rule", "-webkit-column-rule-color", "-webkit-column-rule-style", "-webkit-column-rule-width", "-webkit-columns", "-webkit-column-span", "-webkit-column-width", "-webkit-filter", "-webkit-flow-from", "-webkit-flow-into", "-webkit-font-feature-settings", "-webkit-hyphens", "-webkit-line-break", "-webkit-margin-bottom-collapse", "-webkit-margin-collapse", "-webkit-margin-start", "-webkit-margin-top-collapse", "-webkit-mask-clip", "-webkit-mask-image", "-webkit-mask-origin", "-webkit-mask-repeat", "-webkit-mask-size", "-webkit-nbsp-mode", "-webkit-overflow-scrolling", "-webkit-padding-start", "-webkit-perspective", "-webkit-perspective-origin", "-webkit-region-fragment", "-webkit-tap-highlight-color", "-webkit-text-fill-color", "-webkit-text-size-adjust", "-webkit-text-stroke", "-webkit-text-stroke-color", "-webkit-text-stroke-width", "-webkit-touch-callout", "-webkit-transform", "-webkit-transform-origin", "-webkit-transform-origin-x", "-webkit-transform-origin-y", "-webkit-transform-origin-z", "-webkit-transform-style", "-webkit-transition", "-webkit-transition-delay", "-webkit-transition-duration", "-webkit-transition-property", "-webkit-transition-timing-function", "-webkit-user-drag", "-webkit-user-modify", "-webkit-user-select", "white-space", "widows", "width", "will-change", "word-break", "word-spacing", "word-wrap", "writing-mode", "z-index", "zoom"] 3 | } 4 | 5 | export const htmlData = { 6 | "tags": [ 7 | "body", "head", "html", 8 | "address", "blockquote", "dd", "div", "section", "article", "aside", "header", "footer", "nav", "menu", "dl", "dt", "fieldset", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "iframe", "noframes", "object", "ol", "p", "ul", "applet", "center", "dir", "hr", "pre", 9 | "a", "abbr", "acronym", "area", "b", "base", "basefont", "bdo", "big", "br", "button", "caption", "cite", "code", "col", "colgroup", "del", "dfn", "em", "font", "i", "img", "input", "ins", "isindex", "kbd", "label", "legend", "li", "link", "map", "meta", "noscript", "optgroup", "option", "param", "q", "s", "samp", "script", "select", "small", "span", "strike", "strong", "style", "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "title", "tr", "tt", "u", "var", 10 | "canvas", "main", "figure", "plaintext", "figcaption", "hgroup", "details", "summary", "audio", "bdi", "data", "datalist", "dialog", "embed", "mark", "math", "meter", "output", "picture", "portal", "progress", "rp", "rt", "ruby", "search", "slot", "source", "template", "time", "track", "video", "wbr" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/emmetHelper.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | 7 | import * as JSONC from 'jsonc-parser'; 8 | import { TextDecoder } from 'util'; 9 | import { TextDocument } from 'vscode-languageserver-textdocument'; 10 | import { CompletionItem, CompletionItemKind, CompletionList, InsertTextFormat, Position, Range, TextEdit } from 'vscode-languageserver-types'; 11 | import { URI } from 'vscode-uri'; 12 | import { cssData, htmlData } from './data'; 13 | import { FileService, FileStat, FileType, isAbsolutePath, joinPath } from './fileService'; 14 | 15 | import expand, { Config, extract, ExtractOptions, MarkupAbbreviation, Options, parseMarkup, parseStylesheet, resolveConfig, stringifyMarkup, stringifyStylesheet, StylesheetAbbreviation, SyntaxType, UserConfig } from 'emmet'; 16 | import { parseSnippets, SnippetsMap, syntaxes } from './configCompat'; 17 | 18 | // /* workaround for webpack issue: https://github.com/webpack/webpack/issues/5756 19 | // @emmetio/extract-abbreviation has a cjs that uses a default export 20 | // */ 21 | // const extract = typeof _extractAbbreviation === 'function' ? _extractAbbreviation : _extractAbbreviation.default; 22 | 23 | export { FileService, FileType, FileStat }; 24 | 25 | let l10n: { t: (message: string) => string }; 26 | try { 27 | l10n = require('vscode').l10n; 28 | } catch { 29 | // Fallback to the identity function. 30 | l10n = { 31 | t: (message: string) => message 32 | }; 33 | } 34 | 35 | const snippetKeyCache = new Map(); 36 | let markupSnippetKeys: string[]; 37 | const stylesheetCustomSnippetsKeyCache = new Map(); 38 | const htmlAbbreviationStartRegex = /^[a-z,A-Z,!,(,[,#,\.\{]/; 39 | // take off { for jsx because it interferes with the language 40 | const jsxAbbreviationStartRegex = /^[a-z,A-Z,!,(,[,#,\.]/; 41 | const cssAbbreviationRegex = /^-?[a-z,A-Z,!,@,#]/; 42 | const htmlAbbreviationRegex = /[a-z,A-Z\.]/; 43 | const commonlyUsedTags = [...htmlData.tags, 'lorem']; 44 | const bemFilterSuffix = 'bem'; 45 | const filterDelimitor = '|'; 46 | const trimFilterSuffix = 't'; 47 | const commentFilterSuffix = 'c'; 48 | const maxFilters = 3; 49 | 50 | /** 51 | * Emmet configuration as derived from the Emmet related VS Code settings 52 | */ 53 | export interface VSCodeEmmetConfig { 54 | showExpandedAbbreviation?: string; 55 | showAbbreviationSuggestions?: boolean; 56 | syntaxProfiles?: object; 57 | variables?: object; 58 | preferences?: object; 59 | excludeLanguages?: string[]; 60 | showSuggestionsAsSnippets?: boolean; 61 | } 62 | 63 | /** 64 | * Returns all applicable emmet expansions for abbreviation at given position in a CompletionList 65 | * @param document TextDocument in which completions are requested 66 | * @param position Position in the document at which completions are requested 67 | * @param syntax Emmet supported language 68 | * @param emmetConfig Emmet Configurations as derived from VS Code 69 | */ 70 | export function doComplete(document: TextDocument, position: Position, syntax: string, emmetConfig: VSCodeEmmetConfig): CompletionList | undefined { 71 | if (emmetConfig.showExpandedAbbreviation === 'never' || !getEmmetMode(syntax, emmetConfig.excludeLanguages)) { 72 | return; 73 | } 74 | 75 | const isStyleSheetRes = isStyleSheet(syntax); 76 | 77 | // Fetch markupSnippets so that we can provide possible abbreviation completions 78 | // For example, when text at position is `a`, completions should return `a:blank`, `a:link`, `acr` etc. 79 | if (!isStyleSheetRes) { 80 | if (!snippetKeyCache.has(syntax)) { 81 | const registry: SnippetsMap = { 82 | ...getDefaultSnippets(syntax), 83 | ...customSnippetsRegistry[syntax] 84 | }; 85 | snippetKeyCache.set(syntax, Object.keys(registry)); 86 | } 87 | markupSnippetKeys = snippetKeyCache.get(syntax) ?? []; 88 | } 89 | 90 | const extractOptions: Partial = { lookAhead: !isStyleSheetRes, type: isStyleSheetRes ? 'stylesheet' : 'markup' }; 91 | const extractedValue = extractAbbreviation(document, position, extractOptions); 92 | if (!extractedValue) { 93 | return; 94 | } 95 | const { abbreviationRange, abbreviation, filter } = extractedValue; 96 | const currentLineTillPosition = getCurrentLine(document, position).substr(0, position.character); 97 | const currentWord = getCurrentWord(currentLineTillPosition); 98 | 99 | // Don't attempt to expand open tags 100 | if (currentWord === abbreviation 101 | && currentLineTillPosition.endsWith(`<${abbreviation}`) 102 | && syntaxes.markup.includes(syntax)) { 103 | return; 104 | } 105 | 106 | const expandOptions = getExpandOptions(syntax, emmetConfig, filter); 107 | 108 | let expandedText: string = ""; 109 | let expandedAbbr: CompletionItem | undefined; 110 | let completionItems: CompletionItem[] = []; 111 | 112 | // Create completion item after expanding given abbreviation 113 | // if abbreviation is valid and expanded value is not noise 114 | const createExpandedAbbr = (syntax: string, abbr: string) => { 115 | if (!isAbbreviationValid(syntax, abbreviation)) { 116 | return; 117 | } 118 | 119 | try { 120 | expandedText = expand(abbr, expandOptions); 121 | 122 | // manually patch https://github.com/microsoft/vscode/issues/120245 for now 123 | if (isStyleSheetRes && '!important'.startsWith(abbr)) { 124 | expandedText = '!important'; 125 | } 126 | } catch (e) { 127 | } 128 | 129 | if (!expandedText || isExpandedTextNoise(syntax, abbr, expandedText, expandOptions.options)) { 130 | return; 131 | } 132 | 133 | expandedAbbr = CompletionItem.create(abbr); 134 | expandedAbbr.textEdit = TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(expandedText))); 135 | expandedAbbr.documentation = replaceTabStopsWithCursors(expandedText); 136 | expandedAbbr.insertTextFormat = InsertTextFormat.Snippet; 137 | expandedAbbr.detail = l10n.t('Emmet Abbreviation'); 138 | expandedAbbr.label = abbreviation; 139 | expandedAbbr.label += filter ? '|' + filter.replace(',', '|') : ""; 140 | completionItems = [expandedAbbr]; 141 | } 142 | 143 | if (isStyleSheet(syntax)) { 144 | createExpandedAbbr(syntax, abbreviation); 145 | 146 | // When abbr is longer than usual emmet snippets and matches better with existing css property, then no emmet 147 | if (abbreviation.length > 4 148 | && cssData.properties.find(x => x.startsWith(abbreviation))) { 149 | return CompletionList.create([], true); 150 | } 151 | 152 | if (expandedAbbr && expandedText.length) { 153 | expandedAbbr.textEdit = TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(expandedText))); 154 | expandedAbbr.documentation = replaceTabStopsWithCursors(expandedText); 155 | expandedAbbr.label = removeTabStops(expandedText); 156 | expandedAbbr.filterText = abbreviation; 157 | 158 | // Custom snippets should show up in completions if abbreviation is a prefix 159 | const stylesheetCustomSnippetsKeys = stylesheetCustomSnippetsKeyCache.has(syntax) ? 160 | stylesheetCustomSnippetsKeyCache.get(syntax) : stylesheetCustomSnippetsKeyCache.get('css'); 161 | completionItems = makeSnippetSuggestion( 162 | stylesheetCustomSnippetsKeys ?? [], 163 | abbreviation, 164 | abbreviation, 165 | abbreviationRange, 166 | expandOptions, 167 | 'Emmet Custom Snippet', 168 | false); 169 | 170 | if (!completionItems.find(x => x.textEdit?.newText && x.textEdit?.newText === expandedAbbr?.textEdit?.newText)) { 171 | 172 | // Fix for https://github.com/Microsoft/vscode/issues/28933#issuecomment-309236902 173 | // When user types in propertyname, emmet uses it to match with snippet names, resulting in width -> widows or font-family -> font: family 174 | // Filter out those cases here. 175 | const abbrRegex = new RegExp('.*' + abbreviation.split('').map(x => (x === '$' || x === '+') ? '\\' + x : x).join('.*') + '.*', 'i'); 176 | if (/\d/.test(abbreviation) || abbrRegex.test(expandedAbbr.label)) { 177 | completionItems.push(expandedAbbr); 178 | } 179 | } 180 | } 181 | } else { 182 | createExpandedAbbr(syntax, abbreviation); 183 | 184 | let tagToFindMoreSuggestionsFor = abbreviation; 185 | const newTagMatches = abbreviation.match(/(>|\+)([\w:-]+)$/); 186 | if (newTagMatches && newTagMatches.length === 3) { 187 | tagToFindMoreSuggestionsFor = newTagMatches[2]; 188 | } 189 | 190 | if (syntax !== 'xml') { 191 | const commonlyUsedTagSuggestions = makeSnippetSuggestion(commonlyUsedTags, tagToFindMoreSuggestionsFor, abbreviation, abbreviationRange, expandOptions, 'Emmet Abbreviation'); 192 | completionItems = completionItems.concat(commonlyUsedTagSuggestions); 193 | } 194 | 195 | if (emmetConfig.showAbbreviationSuggestions === true) { 196 | const abbreviationSuggestions = makeSnippetSuggestion(markupSnippetKeys.filter(x => !commonlyUsedTags.includes(x)), tagToFindMoreSuggestionsFor, abbreviation, abbreviationRange, expandOptions, 'Emmet Abbreviation'); 197 | 198 | // Workaround for the main expanded abbr not appearing before the snippet suggestions 199 | if (expandedAbbr && abbreviationSuggestions.length > 0 && tagToFindMoreSuggestionsFor !== abbreviation) { 200 | expandedAbbr.sortText = '0' + expandedAbbr.label; 201 | abbreviationSuggestions.forEach(item => { 202 | // Workaround for snippet suggestions items getting filtered out as the complete abbr does not start with snippetKey 203 | item.filterText = abbreviation 204 | // Workaround for the main expanded abbr not appearing before the snippet suggestions 205 | item.sortText = '9' + abbreviation; 206 | }); 207 | } 208 | completionItems = completionItems.concat(abbreviationSuggestions); 209 | } 210 | 211 | // https://github.com/microsoft/vscode/issues/66680 212 | if (syntax === 'html' && completionItems.length >= 2 && abbreviation.includes(":") 213 | && expandedAbbr?.textEdit?.newText === `<${abbreviation}>\${0}`) { 214 | completionItems = completionItems.filter(item => item.label !== abbreviation); 215 | } 216 | } 217 | 218 | if (emmetConfig.showSuggestionsAsSnippets === true) { 219 | completionItems.forEach(x => x.kind = CompletionItemKind.Snippet); 220 | } 221 | return completionItems.length ? CompletionList.create(completionItems, true) : undefined; 222 | } 223 | 224 | /** 225 | * Create & return snippets for snippet keys that start with given prefix 226 | */ 227 | function makeSnippetSuggestion( 228 | snippetKeys: string[], 229 | prefix: string, 230 | abbreviation: string, 231 | abbreviationRange: Range, 232 | expandOptions: UserConfig, 233 | snippetDetail: string, 234 | skipFullMatch: boolean = true 235 | ): CompletionItem[] { 236 | if (!prefix || !snippetKeys) { 237 | return []; 238 | } 239 | const snippetCompletions: CompletionItem[] = []; 240 | snippetKeys.forEach(snippetKey => { 241 | if (!snippetKey.startsWith(prefix.toLowerCase()) || (skipFullMatch && snippetKey === prefix.toLowerCase())) { 242 | return; 243 | } 244 | 245 | const currentAbbr = abbreviation + snippetKey.substr(prefix.length); 246 | let expandedAbbr; 247 | try { 248 | expandedAbbr = expand(currentAbbr, expandOptions); 249 | } catch (e) { 250 | 251 | } 252 | if (!expandedAbbr) { 253 | return; 254 | } 255 | 256 | const item = CompletionItem.create(prefix + snippetKey.substr(prefix.length)); 257 | item.documentation = replaceTabStopsWithCursors(expandedAbbr); 258 | item.detail = snippetDetail; 259 | item.textEdit = TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(expandedAbbr))); 260 | item.insertTextFormat = InsertTextFormat.Snippet; 261 | 262 | snippetCompletions.push(item); 263 | }); 264 | return snippetCompletions; 265 | } 266 | 267 | function getCurrentWord(currentLineTillPosition: string): string | undefined { 268 | if (currentLineTillPosition) { 269 | const matches = currentLineTillPosition.match(/[\w,:,-,\.]*$/) 270 | if (matches) { 271 | return matches[0]; 272 | } 273 | } 274 | } 275 | 276 | function replaceTabStopsWithCursors(expandedWord: string): string { 277 | return expandedWord.replace(/([^\\])\$\{\d+\}/g, '$1|').replace(/\$\{\d+:([^\}]+)\}/g, '$1'); 278 | } 279 | 280 | function removeTabStops(expandedWord: string): string { 281 | return expandedWord.replace(/([^\\])\$\{\d+\}/g, '$1').replace(/\$\{\d+:([^\}]+)\}/g, '$1'); 282 | } 283 | 284 | function escapeNonTabStopDollar(text: string): string { 285 | return text ? text.replace(/([^\\])(\$)([^\{])/g, '$1\\$2$3') : text; 286 | } 287 | 288 | function addFinalTabStop(text: string): string { 289 | if (!text || !text.trim()) { 290 | return text; 291 | } 292 | 293 | let maxTabStop = -1; 294 | type TabStopRange = { numberStart: number, numberEnd: number }; 295 | let maxTabStopRanges: TabStopRange[] = []; 296 | let foundLastStop = false; 297 | let replaceWithLastStop = false; 298 | let i = 0; 299 | const n = text.length; 300 | 301 | try { 302 | while (i < n && !foundLastStop) { 303 | // Look for ${ 304 | if (text[i++] != '$' || text[i++] != '{') { 305 | continue; 306 | } 307 | 308 | // Find tabstop 309 | let numberStart = -1; 310 | let numberEnd = -1; 311 | while (i < n && /\d/.test(text[i])) { 312 | numberStart = numberStart < 0 ? i : numberStart; 313 | numberEnd = i + 1; 314 | i++; 315 | } 316 | 317 | // If ${ was not followed by a number and either } or :, then its not a tabstop 318 | if (numberStart === -1 || numberEnd === -1 || i >= n || (text[i] != '}' && text[i] != ':')) { 319 | continue; 320 | } 321 | 322 | // If ${0} was found, then break 323 | const currentTabStop = text.substring(numberStart, numberEnd); 324 | foundLastStop = currentTabStop === '0'; 325 | if (foundLastStop) { 326 | break; 327 | } 328 | 329 | let foundPlaceholder = false; 330 | if (text[i++] == ':') { 331 | // TODO: Nested placeholders may break here 332 | while (i < n) { 333 | if (text[i] == '}') { 334 | foundPlaceholder = true; 335 | break; 336 | } 337 | i++; 338 | } 339 | } 340 | 341 | // Decide to replace currentTabStop with ${0} only if its the max among all tabstops and is not a placeholder 342 | if (Number(currentTabStop) > Number(maxTabStop)) { 343 | maxTabStop = Number(currentTabStop); 344 | maxTabStopRanges = [{ numberStart, numberEnd }]; 345 | replaceWithLastStop = !foundPlaceholder; 346 | } else if (Number(currentTabStop) === maxTabStop) { 347 | maxTabStopRanges.push({ numberStart, numberEnd }); 348 | } 349 | } 350 | } catch (e) { 351 | 352 | } 353 | 354 | if (replaceWithLastStop && !foundLastStop) { 355 | for (let i = 0; i < maxTabStopRanges.length; i++) { 356 | const rangeStart = maxTabStopRanges[i].numberStart; 357 | const rangeEnd = maxTabStopRanges[i].numberEnd; 358 | text = text.substr(0, rangeStart) + '0' + text.substr(rangeEnd); 359 | } 360 | } 361 | 362 | return text; 363 | } 364 | 365 | function getCurrentLine(document: TextDocument, position: Position): string { 366 | const offset = document.offsetAt(position); 367 | const text = document.getText(); 368 | let start = 0; 369 | let end = text.length; 370 | for (let i = offset - 1; i >= 0; i--) { 371 | if (text[i] === '\n') { 372 | start = i + 1; 373 | break; 374 | } 375 | } 376 | for (let i = offset; i < text.length; i++) { 377 | if (text[i] === '\n') { 378 | end = i; 379 | break; 380 | } 381 | } 382 | return text.substring(start, end); 383 | } 384 | 385 | let customSnippetsRegistry: Record = {}; 386 | let variablesFromFile = {}; 387 | let profilesFromFile = {}; 388 | 389 | export const emmetSnippetField = (index: number, placeholder: string) => `\${${index}${placeholder ? ':' + placeholder : ''}}`; 390 | 391 | /** Returns whether or not syntax is a supported stylesheet syntax, like CSS */ 392 | export function isStyleSheet(syntax: string): boolean { 393 | return syntaxes.stylesheet.includes(syntax); 394 | } 395 | 396 | /** Returns the syntax type, either markup (e.g. for HTML) or stylesheet (e.g. for CSS) */ 397 | export function getSyntaxType(syntax: string): SyntaxType { 398 | return isStyleSheet(syntax) ? 'stylesheet' : 'markup'; 399 | } 400 | 401 | /** Returns the default syntax (html or css) to use for the snippets registry */ 402 | export function getDefaultSyntax(syntax: string): string { 403 | return isStyleSheet(syntax) ? 'css' : 'html'; 404 | } 405 | 406 | /** Returns the default snippets that Emmet suggests */ 407 | export function getDefaultSnippets(syntax: string): SnippetsMap { 408 | const syntaxType = getSyntaxType(syntax); 409 | const emptyUserConfig: UserConfig = { type: syntaxType, syntax }; 410 | const resolvedConfig: Config = resolveConfig(emptyUserConfig); 411 | 412 | // https://github.com/microsoft/vscode/issues/97632 413 | // don't return markup (HTML) snippets for XML 414 | return syntax === 'xml' ? {} : resolvedConfig.snippets; 415 | } 416 | 417 | function getFilters(text: string, pos: number): { pos: number, filter: string | undefined } { 418 | let filter: string | undefined; 419 | for (let i = 0; i < maxFilters; i++) { 420 | if (text.endsWith(`${filterDelimitor}${bemFilterSuffix}`, pos)) { 421 | pos -= bemFilterSuffix.length + 1; 422 | filter = filter ? bemFilterSuffix + ',' + filter : bemFilterSuffix; 423 | } else if (text.endsWith(`${filterDelimitor}${commentFilterSuffix}`, pos)) { 424 | pos -= commentFilterSuffix.length + 1; 425 | filter = filter ? commentFilterSuffix + ',' + filter : commentFilterSuffix; 426 | } else if (text.endsWith(`${filterDelimitor}${trimFilterSuffix}`, pos)) { 427 | pos -= trimFilterSuffix.length + 1; 428 | filter = filter ? trimFilterSuffix + ',' + filter : trimFilterSuffix; 429 | } else { 430 | break; 431 | } 432 | } 433 | return { 434 | pos: pos, 435 | filter: filter 436 | } 437 | } 438 | 439 | /** 440 | * Extracts abbreviation from the given position in the given document 441 | * @param document The TextDocument from which abbreviation needs to be extracted 442 | * @param position The Position in the given document from where abbreviation needs to be extracted 443 | * @param options The options to pass to the @emmetio/extract-abbreviation module 444 | */ 445 | export function extractAbbreviation(document: TextDocument, position: Position, options?: Partial): { abbreviation: string, abbreviationRange: Range, filter: string | undefined } | undefined { 446 | const currentLine = getCurrentLine(document, position); 447 | const currentLineTillPosition = currentLine.substr(0, position.character); 448 | const { pos, filter } = getFilters(currentLineTillPosition, position.character); 449 | const lengthOccupiedByFilter = filter ? filter.length + 1 : 0; 450 | const result = extract(currentLine, pos, options); 451 | if (!result) { 452 | return; 453 | } 454 | const rangeToReplace = Range.create(position.line, result.location, position.line, result.location + result.abbreviation.length + lengthOccupiedByFilter); 455 | return { 456 | abbreviationRange: rangeToReplace, 457 | abbreviation: result.abbreviation, 458 | filter 459 | }; 460 | } 461 | 462 | /** 463 | * Extracts abbreviation from the given text 464 | * @param text Text from which abbreviation needs to be extracted 465 | * @param syntax Syntax used to extract the abbreviation from the given text 466 | */ 467 | export function extractAbbreviationFromText(text: string, syntax: string): { abbreviation: string, filter: string | undefined } | undefined { 468 | if (!text) { 469 | return; 470 | } 471 | const { pos, filter } = getFilters(text, text.length); 472 | const extractOptions = (isStyleSheet(syntax) || syntax === 'stylesheet') ? 473 | { syntax: 'stylesheet', lookAhead: false } : 474 | { lookAhead: true }; 475 | const result = extract(text, pos, extractOptions); 476 | if (!result) { 477 | return; 478 | } 479 | return { 480 | abbreviation: result.abbreviation, 481 | filter 482 | }; 483 | } 484 | 485 | /** 486 | * Returns a boolean denoting validity of given abbreviation in the context of given syntax 487 | * Not needed once https://github.com/emmetio/atom-plugin/issues/22 is fixed 488 | * @param syntax string 489 | * @param abbreviation string 490 | */ 491 | export function isAbbreviationValid(syntax: string, abbreviation: string): boolean { 492 | if (!abbreviation) { 493 | return false; 494 | } 495 | if (isStyleSheet(syntax)) { 496 | if (abbreviation.includes('#')) { 497 | if (abbreviation.startsWith('#')) { 498 | const hexColorRegex = /^#[\d,a-f,A-F]{1,6}$/; 499 | return hexColorRegex.test(abbreviation); 500 | } else if (commonlyUsedTags.includes(abbreviation.substring(0, abbreviation.indexOf('#')))) { 501 | return false; 502 | } 503 | } 504 | return cssAbbreviationRegex.test(abbreviation); 505 | } 506 | if (abbreviation.startsWith('!')) { 507 | return !/[^!]/.test(abbreviation); 508 | } 509 | 510 | // Its common for users to type (sometextinsidebrackets), this should not be treated as an abbreviation 511 | // Grouping in abbreviation is valid only if it's inside a text node or preceeded/succeeded with one of the symbols for nesting, sibling, repeater or climb up 512 | // Also, cases such as `span[onclick="alert();"]` are valid 513 | if ((/\(/.test(abbreviation) || /\)/.test(abbreviation)) 514 | && !/\{[^\}\{]*[\(\)]+[^\}\{]*\}(?:[>\+\*\^]|$)/.test(abbreviation) 515 | && !/\(.*\)[>\+\*\^]/.test(abbreviation) 516 | && !/\[[^\[\]\(\)]+=".*"\]/.test(abbreviation) 517 | && !/[>\+\*\^]\(.*\)/.test(abbreviation)) { 518 | return false; 519 | } 520 | 521 | if (syntax === 'jsx') { 522 | return (jsxAbbreviationStartRegex.test(abbreviation) && htmlAbbreviationRegex.test(abbreviation)); 523 | } 524 | 525 | // Fix for jinja syntax https://github.com/microsoft/vscode/issues/179422 526 | if (/^{%|{#|{{/.test(abbreviation)) { 527 | return false; 528 | } 529 | 530 | return (htmlAbbreviationStartRegex.test(abbreviation) && htmlAbbreviationRegex.test(abbreviation)); 531 | } 532 | 533 | function isExpandedTextNoise(syntax: string, abbreviation: string, expandedText: string, options: Partial | undefined): boolean { 534 | // Unresolved css abbreviations get expanded to a blank property value 535 | // Eg: abc -> abc: ; or abc:d -> abc: d; which is noise if it gets suggested for every word typed 536 | if (isStyleSheet(syntax) && options) { 537 | const between = options['stylesheet.between'] ?? ': '; 538 | const after = options['stylesheet.after'] ?? ';'; 539 | 540 | // Remove overlapping between `abbreviation` and `between`, if any 541 | let endPrefixIndex = abbreviation.indexOf(between[0], Math.max(abbreviation.length - between.length, 0)); 542 | endPrefixIndex = endPrefixIndex >= 0 ? endPrefixIndex : abbreviation.length; 543 | const abbr = abbreviation.substring(0, endPrefixIndex); 544 | 545 | return expandedText === `${abbr}${between}\${0}${after}` || 546 | expandedText.replace(/\s/g, '') === abbreviation.replace(/\s/g, '') + after; 547 | } 548 | 549 | // we don't want common html tags suggested for xml 550 | if (syntax === 'xml' && 551 | commonlyUsedTags.some(tag => tag.startsWith(abbreviation.toLowerCase()))) { 552 | return true; 553 | } 554 | 555 | if (commonlyUsedTags.includes(abbreviation.toLowerCase()) || 556 | markupSnippetKeys.includes(abbreviation)) { 557 | return false; 558 | } 559 | 560 | // Custom tags can have - or : 561 | if (/[-,:]/.test(abbreviation) && !/--|::/.test(abbreviation) && 562 | !abbreviation.endsWith(':')) { 563 | return false; 564 | } 565 | 566 | // users might write successive dots '..', '...' which shouldn't be treated as an abbreviation 567 | if (/^\.{2,}$/.test(abbreviation)) { 568 | return true; 569 | } 570 | 571 | // Its common for users to type some text and end it with period, this should not be treated as an abbreviation 572 | // Else it becomes noise. 573 | 574 | // When user just types '.', return the expansion 575 | // Otherwise emmet loses change to participate later 576 | // For example in `.foo`. See https://github.com/Microsoft/vscode/issues/66013 577 | if (abbreviation === '.') { 578 | return false; 579 | } 580 | 581 | const dotMatches = abbreviation.match(/^([a-z,A-Z,\d]*)\.$/); 582 | if (dotMatches) { 583 | // Valid html tags such as `div.` 584 | if (dotMatches[1] && htmlData.tags.includes(dotMatches[1])) { 585 | return false; 586 | } 587 | return true; 588 | } 589 | 590 | // Fix for https://github.com/microsoft/vscode/issues/89746 591 | // PascalCase tags are common in jsx code, which should not be treated as noise. 592 | // Eg: MyAwesomComponent -> 593 | if (syntax === 'jsx' && /^([A-Z][A-Za-z0-9]*)+$/.test(abbreviation)) { 594 | return false; 595 | } 596 | 597 | // Unresolved html abbreviations get expanded as if it were a tag 598 | // Eg: abc -> which is noise if it gets suggested for every word typed 599 | return (expandedText.toLowerCase() === `<${abbreviation.toLowerCase()}>\${1}`); 600 | } 601 | 602 | type ExpandOptionsConfig = { 603 | type: SyntaxType, 604 | options: Partial, 605 | variables: SnippetsMap, 606 | snippets: SnippetsMap, 607 | syntax: string, 608 | text: string | string[] | undefined 609 | maxRepeat: number 610 | } 611 | 612 | /** 613 | * Returns options to be used by emmet 614 | */ 615 | export function getExpandOptions(syntax: string, emmetConfig?: VSCodeEmmetConfig, filter?: string): ExpandOptionsConfig { 616 | emmetConfig = emmetConfig ?? {}; 617 | emmetConfig['preferences'] = emmetConfig['preferences'] ?? {}; 618 | 619 | const preferences: any = emmetConfig['preferences']; 620 | const stylesheetSyntax = isStyleSheet(syntax) ? syntax : 'css'; 621 | 622 | // Fetch Profile 623 | const profile = getProfile(syntax, emmetConfig['syntaxProfiles'] ?? {}); 624 | const filtersFromProfile: string[] = (profile && profile['filters']) ? profile['filters'].split(',') : []; 625 | const trimmedFilters = filtersFromProfile.map(filterFromProfile => filterFromProfile.trim()); 626 | const bemEnabled = (filter && filter.split(',').some(x => x.trim() === 'bem')) || trimmedFilters.includes('bem'); 627 | const commentEnabled = (filter && filter.split(',').some(x => x.trim() === 'c')) || trimmedFilters.includes('c'); 628 | 629 | // Fetch formatters 630 | const formatters = getFormatters(syntax, emmetConfig['preferences']); 631 | const unitAliases: SnippetsMap = (formatters?.stylesheet && formatters.stylesheet['unitAliases']) || {}; 632 | 633 | // These options are the default values provided by vscode for 634 | // extension preferences 635 | const defaultVSCodeOptions: Partial = { 636 | // inlineElements: string[], 637 | // 'output.indent': string, 638 | // 'output.baseIndent': string, 639 | // 'output.newline': string, 640 | // 'output.tagCase': profile['tagCase'], 641 | // 'output.attributeCase': profile['attributeCase'], 642 | // 'output.attributeQuotes': profile['attributeQuotes'], 643 | // 'output.format': profile['format'] ?? true, 644 | // 'output.formatLeafNode': boolean, 645 | 'output.formatSkip': ['html'], 646 | 'output.formatForce': ['body'], 647 | 'output.inlineBreak': 0, 648 | 'output.compactBoolean': false, 649 | // 'output.booleanAttributes': string[], 650 | 'output.reverseAttributes': false, 651 | // 'output.selfClosingStyle': profile['selfClosingStyle'], 652 | 'output.field': emmetSnippetField, 653 | // 'output.text': TextOutput, 654 | 'markup.href': true, 655 | 'comment.enabled': false, 656 | 'comment.trigger': ['id', 'class'], 657 | 'comment.before': '', 658 | 'comment.after': '\n', 659 | 'bem.enabled': false, 660 | 'bem.element': '__', 661 | 'bem.modifier': '_', 662 | 'jsx.enabled': syntax === 'jsx', 663 | // 'stylesheet.keywords': string[], 664 | // 'stylesheet.unitless': string[], 665 | 'stylesheet.shortHex': true, 666 | 'stylesheet.between': syntax === 'stylus' ? ' ' : ': ', 667 | 'stylesheet.after': (syntax === 'sass' || syntax === 'stylus') ? '' : ';', 668 | 'stylesheet.intUnit': 'px', 669 | 'stylesheet.floatUnit': 'em', 670 | 'stylesheet.unitAliases': { e: 'em', p: '%', x: 'ex', r: 'rem' }, 671 | // 'stylesheet.json': boolean, 672 | // 'stylesheet.jsonDoubleQuotes': boolean, 673 | 'stylesheet.fuzzySearchMinScore': 0.3, 674 | }; 675 | 676 | // These options come from user prefs in the vscode repo 677 | let userPreferenceOptions: Partial = { 678 | // inlineElements: string[], 679 | // 'output.indent': string, 680 | // 'output.baseIndent': string, 681 | // 'output.newline': string, 682 | 'output.tagCase': profile['tagCase'], 683 | 'output.attributeCase': profile['attributeCase'], 684 | 'output.attributeQuotes': profile['attributeQuotes'], 685 | 'output.format': profile['format'] ?? true, 686 | // 'output.formatLeafNode': boolean, 687 | 'output.formatSkip': preferences['format.noIndentTags'], 688 | 'output.formatForce': preferences['format.forceIndentationForTags'], 689 | 'output.inlineBreak': profile['inlineBreak'] ?? preferences['output.inlineBreak'], 690 | 'output.compactBoolean': profile['compactBooleanAttributes'] ?? preferences['profile.allowCompactBoolean'], 691 | // 'output.booleanAttributes': string[], 692 | 'output.reverseAttributes': preferences['output.reverseAttributes'], 693 | 'output.selfClosingStyle': profile['selfClosingStyle'] ?? preferences['output.selfClosingStyle'] ?? getClosingStyle(syntax), 694 | 'output.field': emmetSnippetField, 695 | // 'output.text': TextOutput, 696 | // 'markup.href': boolean, 697 | 'comment.enabled': commentEnabled, 698 | 'comment.trigger': preferences['filter.commentTrigger'], 699 | 'comment.before': preferences['filter.commentBefore'], 700 | 'comment.after': preferences['filter.commentAfter'], 701 | 'bem.enabled': bemEnabled, 702 | 'bem.element': preferences['bem.elementSeparator'] ?? '__', 703 | 'bem.modifier': preferences['bem.modifierSeparator'] ?? '_', 704 | 'jsx.enabled': syntax === 'jsx', 705 | // 'stylesheet.keywords': string[], 706 | // 'stylesheet.unitless': string[], 707 | 'stylesheet.shortHex': preferences['css.color.short'], 708 | 'stylesheet.between': preferences[`${stylesheetSyntax}.valueSeparator`], 709 | 'stylesheet.after': preferences[`${stylesheetSyntax}.propertyEnd`], 710 | 'stylesheet.intUnit': preferences['css.intUnit'], 711 | 'stylesheet.floatUnit': preferences['css.floatUnit'], 712 | 'stylesheet.unitAliases': unitAliases, 713 | // 'stylesheet.json': boolean, 714 | // 'stylesheet.jsonDoubleQuotes': boolean, 715 | 'stylesheet.fuzzySearchMinScore': preferences['css.fuzzySearchMinScore'] 716 | }; 717 | 718 | if (syntax === 'jsx') { 719 | // Ref https://github.com/emmetio/emmet/blob/master/src/config.ts#L391 720 | const defaultMarkupAttributeOptions = { 721 | 'class': 'className', 722 | 'class*': 'styleName', 723 | 'for': 'htmlFor' 724 | }; 725 | const defaultMarkupValuePrefixOptions = { 726 | 'class*': 'styles' 727 | }; 728 | 729 | // Rather than trying to merge these specific options upstream, 730 | // we can merge them here before passing them upstream. 731 | if (profile['markup.attributes']) { 732 | userPreferenceOptions['markup.attributes'] = { 733 | ...defaultMarkupAttributeOptions, 734 | ...profile['markup.attributes'] 735 | }; 736 | } 737 | if (profile['markup.valuePrefix']) { 738 | userPreferenceOptions['markup.valuePrefix'] = { 739 | ...defaultMarkupValuePrefixOptions, 740 | ...profile['markup.valuePrefix'] 741 | }; 742 | } 743 | } 744 | 745 | if (syntax === 'vue') { 746 | // Ref https://github.com/emmetio/emmet/blob/master/src/config.ts#L404 747 | const defaultMarkupAttributeOptions = { 748 | 'class*': ':class', 749 | }; 750 | 751 | const defaultMarkupValuePrefixOptions = { 752 | 'class*': '$style' 753 | }; 754 | 755 | if (profile['markup.attributes']) { 756 | userPreferenceOptions['markup.attributes'] = { 757 | ...defaultMarkupAttributeOptions, 758 | ...profile['markup.attributes'] 759 | }; 760 | } 761 | 762 | if (profile['markup.valuePrefix']) { 763 | userPreferenceOptions['markup.valuePrefix'] = { 764 | ...defaultMarkupValuePrefixOptions, 765 | ...profile['markup.valuePrefix'] 766 | }; 767 | } 768 | } 769 | 770 | const combinedOptions: any = {}; 771 | [...Object.keys(defaultVSCodeOptions), ...Object.keys(userPreferenceOptions)].forEach(key => { 772 | const castKey = key as keyof Options; 773 | combinedOptions[castKey] = userPreferenceOptions[castKey] ?? defaultVSCodeOptions[castKey]; 774 | }); 775 | const mergedAliases = { ...defaultVSCodeOptions['stylesheet.unitAliases'], ...userPreferenceOptions['stylesheet.unitAliases'] }; 776 | combinedOptions['stylesheet.unitAliases'] = mergedAliases; 777 | 778 | const type = getSyntaxType(syntax); 779 | const variables = getVariables(emmetConfig['variables']); 780 | const baseSyntax = getDefaultSyntax(syntax); 781 | const snippets = (type === 'stylesheet') ? 782 | (customSnippetsRegistry[syntax] ?? customSnippetsRegistry[baseSyntax]) : 783 | customSnippetsRegistry[syntax]; 784 | 785 | return { 786 | type, 787 | options: combinedOptions, 788 | variables, 789 | snippets, 790 | syntax, 791 | // context: null, 792 | text: undefined, 793 | maxRepeat: 1000, 794 | // cache: null 795 | }; 796 | } 797 | 798 | function getClosingStyle(syntax: string): string { 799 | switch (syntax) { 800 | case 'xhtml': return 'xhtml'; 801 | case 'xml': return 'xml'; 802 | case 'xsl': return 'xml'; 803 | case 'jsx': return 'xhtml'; 804 | default: return 'html'; 805 | } 806 | } 807 | 808 | /** 809 | * Parses given abbreviation using given options and returns a tree 810 | * @param abbreviation string 811 | * @param options options used by the emmet module to parse given abbreviation 812 | */ 813 | export function parseAbbreviation(abbreviation: string, options: UserConfig): StylesheetAbbreviation | MarkupAbbreviation { 814 | const resolvedOptions = resolveConfig(options); 815 | return (options.type === 'stylesheet') ? 816 | parseStylesheet(abbreviation, resolvedOptions) : 817 | parseMarkup(abbreviation, resolvedOptions); 818 | } 819 | 820 | /** 821 | * Expands given abbreviation using given options 822 | * @param abbreviation string or parsed abbreviation 823 | * @param config options used by the @emmetio/expand-abbreviation module to expand given abbreviation 824 | */ 825 | export function expandAbbreviation(abbreviation: string | MarkupAbbreviation | StylesheetAbbreviation, config: UserConfig): string { 826 | let expandedText; 827 | const resolvedConfig = resolveConfig(config); 828 | if (config.type === 'stylesheet') { 829 | if (typeof abbreviation === 'string') { 830 | expandedText = expand(abbreviation, resolvedConfig); 831 | } else { 832 | expandedText = stringifyStylesheet(abbreviation as StylesheetAbbreviation, resolvedConfig); 833 | } 834 | } else { 835 | if (typeof abbreviation === 'string') { 836 | expandedText = expand(abbreviation, resolvedConfig); 837 | } else { 838 | expandedText = stringifyMarkup(abbreviation as MarkupAbbreviation, resolvedConfig); 839 | } 840 | } 841 | return escapeNonTabStopDollar(addFinalTabStop(expandedText)); 842 | } 843 | 844 | /** 845 | * Maps and returns syntaxProfiles of previous format to ones compatible with new emmet modules 846 | * @param syntax 847 | */ 848 | function getProfile(syntax: string, profilesFromSettings: any): any { 849 | if (!profilesFromSettings) { 850 | profilesFromSettings = {}; 851 | } 852 | const profilesConfig = Object.assign({}, profilesFromFile, profilesFromSettings); 853 | 854 | const options = profilesConfig[syntax]; 855 | if (!options || typeof options === 'string') { 856 | if (options === 'xhtml') { 857 | return { 858 | selfClosingStyle: 'xhtml' 859 | }; 860 | } 861 | return {}; 862 | } 863 | const newOptions: any = {}; 864 | for (const key in options) { 865 | switch (key) { 866 | case 'tag_case': 867 | newOptions['tagCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : ''; 868 | break; 869 | case 'attr_case': 870 | newOptions['attributeCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : ''; 871 | break; 872 | case 'attr_quotes': 873 | newOptions['attributeQuotes'] = options[key]; 874 | break; 875 | case 'tag_nl': 876 | newOptions['format'] = (options[key] === true || options[key] === false) ? options[key] : true; 877 | break; 878 | case 'inline_break': 879 | newOptions['inlineBreak'] = options[key]; 880 | break; 881 | case 'self_closing_tag': 882 | if (options[key] === true) { 883 | newOptions['selfClosingStyle'] = 'xml'; break; 884 | } 885 | if (options[key] === false) { 886 | newOptions['selfClosingStyle'] = 'html'; break; 887 | } 888 | newOptions['selfClosingStyle'] = options[key]; 889 | break; 890 | case 'compact_bool': 891 | newOptions['compactBooleanAttributes'] = options[key]; 892 | break; 893 | default: 894 | newOptions[key] = options[key]; 895 | break; 896 | } 897 | } 898 | return newOptions; 899 | } 900 | 901 | /** 902 | * Returns variables to be used while expanding snippets 903 | */ 904 | function getVariables(variablesFromSettings: object | undefined): SnippetsMap { 905 | if (!variablesFromSettings) { 906 | return variablesFromFile; 907 | } 908 | return Object.assign({}, variablesFromFile, variablesFromSettings) as SnippetsMap; 909 | } 910 | 911 | function getFormatters(syntax: string, preferences: any): any { 912 | if (!preferences || typeof preferences !== 'object') { 913 | return {}; 914 | } 915 | 916 | if (!isStyleSheet(syntax)) { 917 | const commentFormatter: any = {}; 918 | for (const key in preferences) { 919 | switch (key) { 920 | case 'filter.commentAfter': 921 | commentFormatter['after'] = preferences[key]; 922 | break; 923 | case 'filter.commentBefore': 924 | commentFormatter['before'] = preferences[key]; 925 | break; 926 | case 'filter.commentTrigger': 927 | commentFormatter['trigger'] = preferences[key]; 928 | break; 929 | default: 930 | break; 931 | } 932 | } 933 | return { 934 | comment: commentFormatter 935 | }; 936 | } 937 | let fuzzySearchMinScore = typeof preferences?.['css.fuzzySearchMinScore'] === 'number' ? preferences['css.fuzzySearchMinScore'] : 0.3; 938 | if (fuzzySearchMinScore > 1) { 939 | fuzzySearchMinScore = 1 940 | } else if (fuzzySearchMinScore < 0) { 941 | fuzzySearchMinScore = 0 942 | } 943 | const stylesheetFormatter: any = { 944 | 'fuzzySearchMinScore': fuzzySearchMinScore 945 | }; 946 | for (const key in preferences) { 947 | switch (key) { 948 | case 'css.floatUnit': 949 | stylesheetFormatter['floatUnit'] = preferences[key]; 950 | break; 951 | case 'css.intUnit': 952 | stylesheetFormatter['intUnit'] = preferences[key]; 953 | break; 954 | case 'css.unitAliases': 955 | const unitAliases: any = {}; 956 | preferences[key].split(',').forEach((alias: string) => { 957 | if (!alias || !alias.trim() || !alias.includes(':')) { 958 | return; 959 | } 960 | const aliasName = alias.substr(0, alias.indexOf(':')); 961 | const aliasValue = alias.substr(aliasName.length + 1); 962 | if (!aliasName.trim() || !aliasValue) { 963 | return; 964 | } 965 | unitAliases[aliasName.trim()] = aliasValue; 966 | }); 967 | stylesheetFormatter['unitAliases'] = unitAliases; 968 | break; 969 | case `${syntax}.valueSeparator`: 970 | stylesheetFormatter['between'] = preferences[key]; 971 | break; 972 | case `${syntax}.propertyEnd`: 973 | stylesheetFormatter['after'] = preferences[key]; 974 | break; 975 | default: 976 | break; 977 | } 978 | } 979 | return { 980 | stylesheet: stylesheetFormatter 981 | }; 982 | } 983 | 984 | /** 985 | * Updates customizations from snippets.json and syntaxProfiles.json files in the directory configured in emmet.extensionsPath setting 986 | * @param emmetExtensionsPathSetting setting passed from emmet.extensionsPath. Supports multiple paths 987 | */ 988 | export async function updateExtensionsPath(emmetExtensionsPathSetting: string[], fs: FileService, workspaceFolderPaths?: URI[], homeDir?: URI): Promise { 989 | resetSettingsFromFile(); 990 | 991 | if (!emmetExtensionsPathSetting.length) { 992 | return; 993 | } 994 | 995 | // Extract URIs from the given setting 996 | const emmetExtensionsPathUri: URI[] = []; 997 | for (let emmetExtensionsPath of emmetExtensionsPathSetting) { 998 | if (typeof emmetExtensionsPath !== 'string') { 999 | console.warn("The following emmetExtensionsPath isn't a string: " + JSON.stringify(emmetExtensionsPath)); 1000 | continue; 1001 | } 1002 | 1003 | emmetExtensionsPath = emmetExtensionsPath.trim(); 1004 | if (emmetExtensionsPath.length && emmetExtensionsPath[0] === '~') { 1005 | if (homeDir) { 1006 | emmetExtensionsPathUri.push(joinPath(homeDir, emmetExtensionsPath.substring(1))); 1007 | } 1008 | } else if (!isAbsolutePath(emmetExtensionsPath)) { 1009 | if (workspaceFolderPaths) { 1010 | // Try pushing the path for each workspace root 1011 | for (const workspacePath of workspaceFolderPaths) { 1012 | emmetExtensionsPathUri.push(joinPath(workspacePath, emmetExtensionsPath)); 1013 | } 1014 | } 1015 | } else { 1016 | emmetExtensionsPathUri.push(URI.file(emmetExtensionsPath)); 1017 | } 1018 | } 1019 | 1020 | // For each URI, grab the files 1021 | for (const uri of emmetExtensionsPathUri) { 1022 | try { 1023 | if ((await fs.stat(uri)).type !== FileType.Directory) { 1024 | // Invalid directory, or path is not a directory 1025 | continue; 1026 | } 1027 | } catch (e) { 1028 | // stat threw an error 1029 | continue; 1030 | } 1031 | 1032 | const snippetsPath = joinPath(uri, 'snippets.json'); 1033 | const profilesPath = joinPath(uri, 'syntaxProfiles.json'); 1034 | let decoder: TextDecoder | undefined; 1035 | if (typeof (globalThis as any).TextDecoder === 'function') { 1036 | decoder = new (globalThis as any).TextDecoder() as TextDecoder; 1037 | } else { 1038 | decoder = new TextDecoder(); 1039 | } 1040 | 1041 | // the only errors we want to throw here are JSON parse errors 1042 | let snippetsDataStr = ""; 1043 | try { 1044 | const snippetsData = await fs.readFile(snippetsPath); 1045 | snippetsDataStr = decoder.decode(snippetsData); 1046 | } catch (e) { 1047 | } 1048 | if (snippetsDataStr.length) { 1049 | try { 1050 | const snippetsJson = tryParseFile(snippetsPath, snippetsDataStr); 1051 | if (snippetsJson['variables']) { 1052 | updateVariables(snippetsJson['variables']); 1053 | } 1054 | updateSnippets(snippetsJson); 1055 | } catch (e) { 1056 | resetSettingsFromFile(); 1057 | throw e; 1058 | } 1059 | } 1060 | 1061 | let profilesDataStr = ""; 1062 | try { 1063 | const profilesData = await fs.readFile(profilesPath); 1064 | profilesDataStr = decoder.decode(profilesData); 1065 | } catch (e) { 1066 | } 1067 | if (profilesDataStr.length) { 1068 | try { 1069 | const profilesJson = tryParseFile(profilesPath, profilesDataStr); 1070 | updateProfiles(profilesJson); 1071 | } catch (e) { 1072 | resetSettingsFromFile(); 1073 | throw e; 1074 | } 1075 | } 1076 | } 1077 | } 1078 | 1079 | function tryParseFile(strPath: URI, dataStr: string): any { 1080 | let errors: JSONC.ParseError[] = []; 1081 | const json = JSONC.parse(dataStr, errors); 1082 | if (errors.length) { 1083 | throw new Error(`Found error ${JSONC.printParseErrorCode(errors[0].error)} while parsing the file ${strPath} at offset ${errors[0].offset}`); 1084 | } 1085 | return json; 1086 | } 1087 | 1088 | /** 1089 | * Assigns variables from one snippet file under emmet.extensionsPath to 1090 | * variablesFromFile 1091 | */ 1092 | function updateVariables(varsJson: any) { 1093 | if (typeof varsJson === 'object' && varsJson) { 1094 | variablesFromFile = Object.assign({}, variablesFromFile, varsJson); 1095 | } else { 1096 | throw new Error(l10n.t('Invalid emmet.variables field. See https://code.visualstudio.com/docs/editor/emmet#_emmet-configuration for a valid example.')); 1097 | } 1098 | } 1099 | 1100 | /** 1101 | * Assigns profiles from one profile file under emmet.extensionsPath to 1102 | * profilesFromFile 1103 | */ 1104 | function updateProfiles(profileJson: any) { 1105 | if (typeof profileJson === 'object' && profileJson) { 1106 | profilesFromFile = Object.assign({}, profilesFromFile, profileJson); 1107 | } else { 1108 | throw new Error(l10n.t('Invalid syntax profile. See https://code.visualstudio.com/docs/editor/emmet#_emmet-configuration for a valid example.')); 1109 | } 1110 | } 1111 | 1112 | /** 1113 | * Assigns snippets from one snippet file under emmet.extensionsPath to 1114 | * customSnippetsRegistry, snippetKeyCache, and stylesheetCustomSnippetsKeyCache 1115 | */ 1116 | function updateSnippets(snippetsJson: any) { 1117 | if (typeof snippetsJson === 'object' && snippetsJson) { 1118 | Object.keys(snippetsJson).forEach(syntax => { 1119 | if (!snippetsJson[syntax]['snippets']) { 1120 | return; 1121 | } 1122 | const baseSyntax = getDefaultSyntax(syntax); 1123 | let customSnippets = snippetsJson[syntax]['snippets']; 1124 | if (snippetsJson[baseSyntax] && snippetsJson[baseSyntax]['snippets'] && baseSyntax !== syntax) { 1125 | customSnippets = Object.assign({}, snippetsJson[baseSyntax]['snippets'], snippetsJson[syntax]['snippets']) 1126 | } 1127 | if (!isStyleSheet(syntax)) { 1128 | // In Emmet 2.0 all snippets should be valid abbreviations 1129 | // Convert old snippets that do not follow this format to new format 1130 | for (const snippetKey in customSnippets) { 1131 | if (customSnippets.hasOwnProperty(snippetKey) 1132 | && customSnippets[snippetKey].startsWith('<') 1133 | && customSnippets[snippetKey].endsWith('>')) { 1134 | customSnippets[snippetKey] = `{${customSnippets[snippetKey]}}` 1135 | } 1136 | } 1137 | } else { 1138 | const prevSnippetKeys = stylesheetCustomSnippetsKeyCache.get(syntax); 1139 | const mergedSnippetKeys = Object.assign([], prevSnippetKeys, Object.keys(customSnippets)); 1140 | stylesheetCustomSnippetsKeyCache.set(syntax, mergedSnippetKeys); 1141 | } 1142 | const prevSnippetsRegistry = customSnippetsRegistry[syntax]; 1143 | const newSnippets = parseSnippets(customSnippets); 1144 | const mergedSnippets = Object.assign({}, prevSnippetsRegistry, newSnippets); 1145 | customSnippetsRegistry[syntax] = mergedSnippets; 1146 | }); 1147 | } else { 1148 | throw new Error(l10n.t('Invalid snippets file. See https://code.visualstudio.com/docs/editor/emmet#_using-custom-emmet-snippets for a valid example.')); 1149 | } 1150 | } 1151 | 1152 | function resetSettingsFromFile() { 1153 | customSnippetsRegistry = {}; 1154 | snippetKeyCache.clear(); 1155 | stylesheetCustomSnippetsKeyCache.clear(); 1156 | profilesFromFile = {}; 1157 | variablesFromFile = {}; 1158 | } 1159 | 1160 | 1161 | /** 1162 | * Get the corresponding emmet mode for given vscode language mode 1163 | * Eg: jsx for typescriptreact/javascriptreact or pug for jade 1164 | * If the language is not supported by emmet or has been exlcuded via `exlcudeLanguages` setting, 1165 | * then nothing is returned 1166 | * 1167 | * @param language 1168 | * @param exlcudedLanguages Array of language ids that user has chosen to exlcude for emmet 1169 | */ 1170 | export function getEmmetMode(language: string, excludedLanguages: string[] = []): string | undefined { 1171 | if (!language || excludedLanguages.includes(language)) { 1172 | return; 1173 | } 1174 | if (/\b(typescriptreact|javascriptreact|jsx-tags)\b/.test(language)) { // treat tsx like jsx 1175 | return 'jsx'; 1176 | } 1177 | if (language === 'sass-indented') { // map sass-indented to sass 1178 | return 'sass'; 1179 | } 1180 | if (language === 'jade') { 1181 | return 'pug'; 1182 | } 1183 | if (syntaxes.markup.includes(language) || syntaxes.stylesheet.includes(language)) { 1184 | return language; 1185 | } 1186 | } 1187 | -------------------------------------------------------------------------------- /src/fileService.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { URI as Uri } from 'vscode-uri'; 7 | 8 | export enum FileType { 9 | /** 10 | * The file type is unknown. 11 | */ 12 | Unknown = 0, 13 | /** 14 | * A regular file. 15 | */ 16 | File = 1, 17 | /** 18 | * A directory. 19 | */ 20 | Directory = 2, 21 | /** 22 | * A symbolic link to a file. 23 | */ 24 | SymbolicLink = 64 25 | } 26 | export interface FileStat { 27 | /** 28 | * The type of the file, e.g. is a regular file, a directory, or symbolic link 29 | * to a file. 30 | */ 31 | type: FileType; 32 | /** 33 | * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. 34 | */ 35 | ctime: number; 36 | /** 37 | * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. 38 | */ 39 | mtime: number; 40 | /** 41 | * The size in bytes. 42 | */ 43 | size: number; 44 | } 45 | 46 | export interface FileService { 47 | readFile(uri: Uri): Thenable; 48 | stat(uri: Uri): Thenable; 49 | } 50 | 51 | // following https://nodejs.org/api/path.html#path_path_isabsolute_path 52 | const PathMatchRegex = new RegExp('^(/|//|\\\\\\\\|[A-Za-z]:(/|\\\\))'); 53 | const Dot = '.'.charCodeAt(0); 54 | 55 | export function isAbsolutePath(path: string) { 56 | return PathMatchRegex.test(path); 57 | } 58 | 59 | export function resolvePath(uri: Uri, path: string): Uri { 60 | if (isAbsolutePath(path)) { 61 | return uri.with({ path: normalizePath(path.split('/')) }); 62 | } 63 | return joinPath(uri, path); 64 | } 65 | 66 | export function normalizePath(parts: string[]): string { 67 | const newParts: string[] = []; 68 | for (const part of parts) { 69 | if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) { 70 | // ignore 71 | } else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) { 72 | newParts.pop(); 73 | } else { 74 | newParts.push(part); 75 | } 76 | } 77 | if (parts.length > 1 && parts[parts.length - 1].length === 0) { 78 | newParts.push(''); 79 | } 80 | let res = newParts.join('/'); 81 | if (parts[0].length === 0) { 82 | res = '/' + res; 83 | } 84 | return res; 85 | } 86 | 87 | export function joinPath(uri: Uri, ...paths: string[]): Uri { 88 | const parts = uri.path.split('/'); 89 | for (const path of paths) { 90 | parts.push(...path.split('/')); 91 | } 92 | return uri.with({ path: normalizePath(parts) }); 93 | } 94 | -------------------------------------------------------------------------------- /src/test/emmetHelper.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { ExtractOptions } from 'emmet'; 3 | import * as fs from 'fs'; 4 | import { describe, it } from 'mocha'; 5 | import * as path from 'path'; 6 | import * as util from 'util'; 7 | import { TextDocument } from 'vscode-languageserver-textdocument'; 8 | import { CompletionItemKind, Position } from 'vscode-languageserver-types'; 9 | import { URI } from 'vscode-uri'; 10 | import { doComplete, emmetSnippetField, expandAbbreviation, extractAbbreviation, extractAbbreviationFromText, getExpandOptions, isAbbreviationValid, updateExtensionsPath as updateExtensionsPathHelper } from '../emmetHelper'; 11 | import { FileService, FileType } from '../fileService'; 12 | 13 | const extensionsPath = [path.join(path.normalize(path.join(__dirname, '../../..')), 'testData', 'custom-snippets-profile')]; 14 | const bemFilterExample = 'ul.search-form._wide>li.-querystring+li.-btn_large'; 15 | const expectedBemFilterOutput = 16 | `
    17 |
  • \${1}
  • 18 |
  • \${0}
  • 19 |
`; 20 | const expectedBemFilterOutputDocs = expectedBemFilterOutput.replace(/\$\{\d+\}/g, '|'); 21 | const commentFilterExample = 'ul.nav>li#item'; 22 | const expectedCommentFilterOutput = 23 | ` 27 | `; 28 | const expectedCommentFilterOutputDocs = expectedCommentFilterOutput.replace(/\$\{\d+\}/g, '|'); 29 | const bemCommentFilterExample = bemFilterExample; 30 | const expectedBemCommentFilterOutput = 31 | `
    32 |
  • \${1}
  • 33 | 34 |
  • \${0}
  • 35 | 36 |
37 | `; 38 | const expectedBemCommentFilterOutputDocs = expectedBemCommentFilterOutput.replace(/\$\{\d+\}/g, '|'); 39 | 40 | const fileService: FileService = { 41 | async readFile(uri: URI): Promise { 42 | if (uri.scheme === 'file') { 43 | return await util.promisify(fs.readFile)(uri.fsPath); 44 | } 45 | throw new Error(`schema ${uri.scheme} not supported`); 46 | }, 47 | stat(uri: URI) { 48 | if (uri.scheme === 'file') { 49 | return new Promise((c, e) => { 50 | fs.stat(uri.fsPath, (err, stats) => { 51 | if (err) { 52 | if (err.code === 'ENOENT') { 53 | return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 }); 54 | } else { 55 | return e(err); 56 | } 57 | } 58 | 59 | let type = FileType.Unknown; 60 | if (stats.isFile()) { 61 | type = FileType.File; 62 | } else if (stats.isDirectory()) { 63 | type = FileType.Directory; 64 | } else if (stats.isSymbolicLink()) { 65 | type = FileType.SymbolicLink; 66 | } 67 | 68 | c({ 69 | type, 70 | ctime: stats.ctime.getTime(), 71 | mtime: stats.mtime.getTime(), 72 | size: stats.size 73 | }); 74 | }); 75 | }); 76 | } 77 | throw new Error(`schema ${uri.scheme} not supported`); 78 | } 79 | } 80 | 81 | function updateExtensionsPath(extPath: string[]): Promise { 82 | return updateExtensionsPathHelper(extPath, fileService, [URI.file('/home/projects/test')]) 83 | } 84 | 85 | describe('Validate Abbreviations', () => { 86 | it('should return true for valid abbreviations', () => { 87 | const htmlAbbreviations = [ 88 | 'ul>li', 89 | 'ul', 90 | 'h1', 91 | 'picture>source', 92 | 'ul>li*3', 93 | '(ul>li)+div', 94 | '.hello', 95 | '!', 96 | '#hello', 97 | '.item[id=ok]', 98 | '.', 99 | '.foo', 100 | 'div{ foo (bar) baz }', 101 | 'div{ foo ((( abc }', 102 | 'div{()}', 103 | 'div{ a (b) c}', 104 | 'div{ a (b) c}+div{ a (( }' 105 | ]; 106 | const cssAbbreviations = ['#123', '#abc', 'bd1#s']; 107 | htmlAbbreviations.forEach(abbr => { 108 | assert(isAbbreviationValid('html', abbr), `${abbr} should be treated as valid abbreviation`); 109 | }); 110 | htmlAbbreviations.forEach(abbr => { 111 | assert(isAbbreviationValid('haml', abbr), `${abbr} should be treated as valid abbreviation`); 112 | }); 113 | cssAbbreviations.forEach(abbr => { 114 | assert(isAbbreviationValid('css', abbr), `${abbr} should be treated as valid abbreviation`); 115 | }); 116 | cssAbbreviations.forEach(abbr => { 117 | assert(isAbbreviationValid('scss', abbr), `${abbr} should be treated as valid abbreviation`); 118 | }); 119 | }); 120 | it('should return false for invalid abbreviations', () => { 121 | const htmlAbbreviations = [ 122 | '!ul!', 123 | '(hello)', 124 | 'super(hello)', 125 | 'console.log(hello)', 126 | 'console.log(._hello)', 127 | '()', 128 | '[]', 129 | '(my.data[0].element)', 130 | 'if(!ok)', 131 | 'while(!ok)', 132 | '(!ok)', 133 | 'div{ foo }(bar){ baz }', 134 | 'div{ foo ((}( abc }', 135 | 'div{ a}(b) c}', 136 | 'div{ a (b){c}', 137 | 'div{ a}(b){c}', 138 | 'div{ a (( dsf} d (( sf )) }' 139 | ]; 140 | const cssAbbreviations = ['123', '#xyz']; 141 | htmlAbbreviations.forEach(abbr => { 142 | assert(!isAbbreviationValid('html', abbr), `${abbr} should be treated as invalid abbreviation in html`); 143 | }); 144 | htmlAbbreviations.forEach(abbr => { 145 | assert(!isAbbreviationValid('haml', abbr), `${abbr} should be treated as invalid abbreviation in haml`); 146 | }); 147 | cssAbbreviations.forEach(abbr => { 148 | assert(!isAbbreviationValid('css', abbr), `${abbr} should be treated as invalid abbreviation in css`); 149 | }); 150 | cssAbbreviations.forEach(abbr => { 151 | assert(!isAbbreviationValid('scss', abbr), `${abbr} should be treated as invalid abbreviation in scss`); 152 | }); 153 | }) 154 | }); 155 | 156 | describe('Extract Abbreviations', () => { 157 | it('should extract abbreviations from document html', () => { 158 | const testCases: [string, number, number, string, number, number, number, number, string | undefined][] = [ 159 | ['
ul>li*3
', 0, 7, 'ul', 0, 5, 0, 7, undefined], 160 | ['
ul>li*3
', 0, 10, 'ul>li', 0, 5, 0, 10, undefined], 161 | ['
ul>li*3
', 0, 12, 'ul>li*3', 0, 5, 0, 12, undefined], 162 | ['ul>li', 0, 5, 'ul>li', 0, 0, 0, 5, undefined], 163 | ['ul>li|bem', 0, 9, 'ul>li', 0, 0, 0, 9, 'bem'], 164 | ['ul>li|c|bem', 0, 11, 'ul>li', 0, 0, 0, 11, 'c,bem'], 165 | ['ul>li|bem|c', 0, 11, 'ul>li', 0, 0, 0, 11, 'bem,c'], 166 | ['ul>li|t|bem|c', 0, 13, 'ul>li', 0, 0, 0, 13, 't,bem,c'], 167 | ['div[a="b" c="d"]>md-button', 0, 26, 'div[a="b" c="d"]>md-button', 0, 0, 0, 26, undefined], 168 | ['div[a=b c="d"]>md-button', 0, 24, 'div[a=b c="d"]>md-button', 0, 0, 0, 24, undefined], 169 | ['div[a=b c=d]>md-button', 0, 22, 'div[a=b c=d]>md-button', 0, 0, 0, 22, undefined] 170 | ] 171 | 172 | testCases.forEach(([content, positionLine, positionChar, expectedAbbr, expectedRangeStartLine, expectedRangeStartChar, expectedRangeEndLine, expectedRangeEndChar, expectedFilter]) => { 173 | const document = TextDocument.create('test://test/test.html', 'html', 0, content); 174 | const position = Position.create(positionLine, positionChar); 175 | const result = extractAbbreviation(document, position); 176 | assert.ok(result); 177 | const { abbreviationRange, abbreviation, filter } = result; 178 | assert.strictEqual(expectedAbbr, abbreviation); 179 | assert.strictEqual(expectedRangeStartLine, abbreviationRange.start.line); 180 | assert.strictEqual(expectedRangeStartChar, abbreviationRange.start.character); 181 | assert.strictEqual(expectedRangeEndLine, abbreviationRange.end.line); 182 | assert.strictEqual(expectedRangeEndChar, abbreviationRange.end.character); 183 | assert.strictEqual(filter, expectedFilter); 184 | }); 185 | }); 186 | it('should extract abbreviations from document css', () => { 187 | const testCases: [string, number, number, string, number, number, number, number, string | undefined][] = [ 188 | ['
', 0, 14, 'dn', 0, 12, 0, 14, undefined], 189 | ['
', 0, 18, 'trf:rx', 0, 12, 0, 18, undefined], 190 | ['
', 0, 23, '-mwo-trf:rx', 0, 12, 0, 23, undefined], 191 | ] 192 | 193 | testCases.forEach(([content, positionLine, positionChar, expectedAbbr, expectedRangeStartLine, expectedRangeStartChar, expectedRangeEndLine, expectedRangeEndChar, expectedFilter]) => { 194 | const document = TextDocument.create('test://test/test.html', 'html', 0, content); 195 | const position = Position.create(positionLine, positionChar); 196 | const extractOptions: Partial = { type: 'stylesheet', lookAhead: false }; 197 | const result = extractAbbreviation(document, position, extractOptions); 198 | assert.ok(result); 199 | const { abbreviationRange, abbreviation, filter } = result; 200 | 201 | assert.strictEqual(expectedAbbr, abbreviation); 202 | assert.strictEqual(expectedRangeStartLine, abbreviationRange.start.line); 203 | assert.strictEqual(expectedRangeStartChar, abbreviationRange.start.character); 204 | assert.strictEqual(expectedRangeEndLine, abbreviationRange.end.line); 205 | assert.strictEqual(expectedRangeEndChar, abbreviationRange.end.character); 206 | assert.strictEqual(filter, expectedFilter); 207 | }); 208 | }); 209 | 210 | it('should extract abbreviations from text', () => { 211 | const testCases: [string, string, string | undefined][] = [ 212 | ['ul', 'ul', undefined], 213 | ['ul>li', 'ul>li', undefined], 214 | ['ul>li*3', 'ul>li*3', undefined], 215 | ['ul>li|bem', 'ul>li', 'bem'], 216 | ['ul>li|t', 'ul>li', 't'], 217 | ['ul>li|bem|c', 'ul>li', 'bem,c'], 218 | ['ul>li|c|bem', 'ul>li', 'c,bem'], 219 | ['ul>li|c|bem|t', 'ul>li', 'c,bem,t'], 220 | ] 221 | 222 | testCases.forEach(([content, expectedAbbr, expectedFilter]) => { 223 | const result = extractAbbreviationFromText(content, 'html'); 224 | assert.ok(result); 225 | const { abbreviation, filter } = result; 226 | assert.strictEqual(expectedAbbr, abbreviation); 227 | assert.strictEqual(filter, expectedFilter); 228 | }); 229 | }); 230 | }); 231 | 232 | describe('Test Basic Expand Options', () => { 233 | it('should check for basic expand options', () => { 234 | const syntax = 'anythingreally'; 235 | const expandOptions = getExpandOptions(syntax); 236 | 237 | assert.strictEqual(expandOptions.options['output.field'], emmetSnippetField) 238 | assert.strictEqual(expandOptions.syntax, syntax); 239 | }); 240 | 241 | it('should support jsx markup attributes', () => { 242 | const syntax = 'jsx'; 243 | const emmetConfig = { 244 | syntaxProfiles: { 245 | jsx: { 246 | 'markup.attributes': { 247 | 'class': 'attributePrefix' 248 | }, 249 | 'markup.valuePrefix': { 250 | 'class': 'valuePrefix' 251 | } 252 | } 253 | } 254 | }; 255 | const expandOptions = getExpandOptions(syntax, emmetConfig); 256 | assert.ok(expandOptions.options['markup.valuePrefix']); 257 | assert.ok(expandOptions.options['markup.attributes']); 258 | assert.strictEqual(expandOptions.options['markup.valuePrefix']!['class'], 'valuePrefix'); 259 | assert.strictEqual(expandOptions.options['markup.attributes']!['class'], 'attributePrefix'); 260 | }) 261 | 262 | it('should support vue markup attributes', () => { 263 | const syntax = 'vue'; 264 | const emmetConfig = { 265 | syntaxProfiles: { 266 | vue: { 267 | 'markup.attributes': { 268 | 'class': 'attributePrefix' 269 | }, 270 | 'markup.valuePrefix': { 271 | 'class': 'valuePrefix' 272 | } 273 | } 274 | } 275 | }; 276 | const expandOptions = getExpandOptions(syntax, emmetConfig); 277 | assert.ok(expandOptions.options['markup.valuePrefix']); 278 | assert.ok(expandOptions.options['markup.attributes']); 279 | assert.strictEqual(expandOptions.options['markup.valuePrefix']!['class'], 'valuePrefix'); 280 | assert.strictEqual(expandOptions.options['markup.attributes']!['class'], 'attributePrefix'); 281 | }) 282 | }); 283 | 284 | describe('Test addons in Expand Options', () => { 285 | it('should add jsx as addon for jsx syntax', () => { 286 | const syntax = 'jsx'; 287 | const expandOptions = getExpandOptions(syntax); 288 | 289 | assert.strictEqual(expandOptions.options['jsx.enabled'], true); 290 | }); 291 | 292 | it('should add bem as addon when bem filter is provided', () => { 293 | const syntax = 'anythingreally'; 294 | const expandOptions = getExpandOptions(syntax, {}, 'bem'); 295 | 296 | assert.strictEqual(expandOptions.options['bem.element'], '__'); 297 | }); 298 | 299 | it('should add bem before jsx as addon when bem filter is provided', () => { 300 | const syntax = 'jsx'; 301 | const expandOptions = getExpandOptions(syntax, {}, 'bem'); 302 | 303 | assert.strictEqual(expandOptions.options['bem.element'], '__'); 304 | assert.strictEqual(expandOptions.options['jsx.enabled'], true); 305 | }); 306 | }); 307 | 308 | describe('Test output profile settings', () => { 309 | it('should convert output profile from old format to new', () => { 310 | const profile = { 311 | tag_case: 'lower', 312 | attr_case: 'lower', 313 | attr_quotes: 'single', 314 | tag_nl: true, 315 | inline_break: 2, 316 | self_closing_tag: 'xhtml' 317 | } 318 | 319 | const expandOptions = getExpandOptions('html', { syntaxProfiles: { html: profile } }); 320 | 321 | assert.strictEqual(profile['tag_case'], expandOptions.options['output.tagCase']); 322 | assert.strictEqual(profile['attr_case'], expandOptions.options['output.attributeCase']); 323 | assert.strictEqual(profile['attr_quotes'], expandOptions.options['output.attributeQuotes']); 324 | assert.strictEqual(profile['tag_nl'], expandOptions.options['output.format']); 325 | assert.strictEqual(profile['inline_break'], expandOptions.options['output.inlineBreak']); 326 | assert.strictEqual(profile['self_closing_tag'], expandOptions.options['output.selfClosingStyle']); 327 | }); 328 | 329 | it('should convert self_closing_style', () => { 330 | const testCases = [true, false, 'xhtml']; 331 | const expectedValue = ['xml', 'html', 'xhtml']; 332 | 333 | for (let i = 0; i < testCases.length; i++) { 334 | const expandOptions = getExpandOptions('html', { syntaxProfiles: { html: { self_closing_tag: testCases[i] } } }); 335 | assert.strictEqual(expandOptions.options['output.selfClosingStyle'], expectedValue[i]); 336 | } 337 | }); 338 | 339 | it('should convert tag_nl', () => { 340 | const testCases = [true, false, 'decide']; 341 | const expectedValue = [true, false, true]; 342 | 343 | for (let i = 0; i < testCases.length; i++) { 344 | const expandOptions = getExpandOptions('html', { syntaxProfiles: { html: { tag_nl: testCases[i] } } }); 345 | assert.strictEqual(expandOptions.options['output.format'], expectedValue[i]); 346 | } 347 | }); 348 | 349 | it('should use output profile in new format as is', () => { 350 | const profile = { 351 | tagCase: 'lower', 352 | attributeCase: 'lower', 353 | attributeQuotes: 'single', 354 | format: true, 355 | inlineBreak: 2, 356 | selfClosingStyle: 'xhtml' 357 | }; 358 | 359 | const expandOptions = getExpandOptions('html', { syntaxProfiles: { html: profile } }); 360 | Object.keys(profile).forEach(key => { 361 | assert.strictEqual((expandOptions as any).options[`output.${key}`], (profile as any)[key]); 362 | }); 363 | }); 364 | 365 | it('should use profile from settings that overrides the ones from extensionsPath', async () => { 366 | await updateExtensionsPath(extensionsPath); 367 | const profile = { 368 | tag_case: 'lower', 369 | attr_case: 'lower', 370 | attr_quotes: 'single', 371 | tag_nl: true, 372 | inline_break: 2, 373 | self_closing_tag: 'xhtml' 374 | }; 375 | const expandOptions = getExpandOptions('html', { syntaxProfiles: { html: profile } }); 376 | 377 | assert.strictEqual(expandOptions.options['output.tagCase'], 'lower'); 378 | assert.strictEqual(profile['tag_case'], 'lower'); 379 | }); 380 | }); 381 | 382 | describe('Test variables settings', () => { 383 | it('should take in variables as is', () => { 384 | const variables = { 385 | lang: 'de', 386 | charset: 'UTF-8' 387 | } 388 | 389 | const expandOptions = getExpandOptions('html', { variables }); 390 | Object.keys(variables).forEach(key => { 391 | assert.strictEqual(expandOptions.variables[key], (variables as any)[key]); 392 | }); 393 | }); 394 | 395 | it('should use variables from the extensionsPath', async () => { 396 | await updateExtensionsPath(extensionsPath); 397 | 398 | const expandOptions = getExpandOptions('html', {}); 399 | assert.strictEqual(expandOptions.variables['lang'], 'fr'); 400 | }); 401 | 402 | it('should use given variables that override ones from extensionsPath', async () => { 403 | await updateExtensionsPath(extensionsPath); 404 | 405 | const variables = { 406 | lang: 'en', 407 | charset: 'UTF-8' 408 | } 409 | const expandOptions = getExpandOptions('html', { variables }); 410 | assert.strictEqual(expandOptions.variables['lang'], variables['lang']); 411 | }); 412 | }); 413 | 414 | describe('Test custom snippets', () => { 415 | it('should use custom snippets for given syntax from extensionsPath', async () => { 416 | const customSnippetKey = 'ch'; 417 | await updateExtensionsPath([]); 418 | const expandOptionsWithoutCustomSnippets = getExpandOptions('css'); 419 | assert(!expandOptionsWithoutCustomSnippets.snippets); 420 | 421 | // Use custom snippets from extensionsPath 422 | await updateExtensionsPath(extensionsPath); 423 | const expandOptionsWithCustomSnippets = getExpandOptions('css'); 424 | 425 | assert.ok(expandOptionsWithCustomSnippets.snippets); 426 | assert.strictEqual(Object.keys(expandOptionsWithCustomSnippets.snippets).some(key => key === customSnippetKey), true); 427 | }); 428 | 429 | it('should use custom snippets inherited from base syntax from extensionsPath', async () => { 430 | const customSnippetKey = 'ch'; 431 | 432 | await updateExtensionsPath([]); 433 | const expandOptionsWithoutCustomSnippets = getExpandOptions('scss'); 434 | assert(!expandOptionsWithoutCustomSnippets.snippets); 435 | 436 | // Use custom snippets from extensionsPath 437 | await updateExtensionsPath(extensionsPath); 438 | 439 | const expandOptionsWithCustomSnippets = getExpandOptions('css'); 440 | const expandOptionsWithCustomSnippetsInheritedSyntax = getExpandOptions('scss'); 441 | 442 | assert.ok(expandOptionsWithCustomSnippets.snippets); 443 | assert.ok(expandOptionsWithCustomSnippetsInheritedSyntax.snippets); 444 | assert.strictEqual(Object.keys(expandOptionsWithCustomSnippets.snippets).some(key => key === customSnippetKey), true); 445 | assert.strictEqual(Object.keys(expandOptionsWithCustomSnippetsInheritedSyntax.snippets).some(key => key === customSnippetKey), true); 446 | }); 447 | 448 | it('should use custom snippets for given syntax in the absence of base syntax from extensionsPath', async () => { 449 | const customSnippetKey = 'ch'; 450 | await updateExtensionsPath([]); 451 | const expandOptionsWithoutCustomSnippets = getExpandOptions('scss'); 452 | assert(!expandOptionsWithoutCustomSnippets.snippets); 453 | 454 | // Use custom snippets from extensionsPath 455 | await updateExtensionsPath([path.join(path.normalize(path.join(__dirname, '../../..')), 'testData', 'custom-snippets-without-inheritance')]); 456 | const expandOptionsWithCustomSnippets = getExpandOptions('scss'); 457 | 458 | assert.ok(expandOptionsWithCustomSnippets.snippets); 459 | assert.strictEqual(Object.keys(expandOptionsWithCustomSnippets.snippets).some(key => key === customSnippetKey), true); 460 | }); 461 | 462 | it('should throw error when snippets file from extensionsPath has invalid json', async () => { 463 | const invalidJsonPath = path.join(path.normalize(path.join(__dirname, '../../..')), 'testData', 'custom-snippets-invalid-json'); 464 | try { 465 | await updateExtensionsPath([invalidJsonPath]); 466 | return Promise.reject('There should be an error as snippets file contained invalid json'); 467 | } catch (e) { 468 | assert.ok(e); 469 | } 470 | }); 471 | 472 | it('should reset custom snippets when no extensionsPath is given', async () => { 473 | const customSnippetKey = 'ch'; 474 | await updateExtensionsPath(extensionsPath); 475 | assert.strictEqual(Object.keys(getExpandOptions('scss').snippets!).some(key => key === customSnippetKey), true); 476 | 477 | await updateExtensionsPath([]); 478 | assert.ok(!getExpandOptions('scss').snippets, 'There should be no custom snippets as extensionPath was not given'); 479 | }); 480 | 481 | it('should do nothing when non-existent extensionsPath is given', async () => { 482 | const customSnippetKey = 'ch'; 483 | await updateExtensionsPath(extensionsPath); 484 | assert.strictEqual(Object.keys(getExpandOptions('scss').snippets!).some(key => key === customSnippetKey), true); 485 | 486 | try { 487 | await updateExtensionsPath(["./this/is/not/valid"]); 488 | assert.ok(!getExpandOptions('scss').snippets, 'There should be no custom snippets as extensionPath was faulty'); 489 | } catch (e) { 490 | throw new Error('There should not be an error'); 491 | } 492 | }); 493 | 494 | it('should do nothing when directory with no snippets is given', async () => { 495 | const customSnippetKey = 'ch'; 496 | await updateExtensionsPath(extensionsPath); 497 | 498 | const foundCustomSnippet = Object.keys(getExpandOptions('scss').snippets!) 499 | .some(key => key === customSnippetKey); 500 | assert.strictEqual(foundCustomSnippet, true); 501 | 502 | const extensionsPathParent = path.join(path.normalize(path.join(__dirname, '../../..')), 'testData'); 503 | try { 504 | await updateExtensionsPath([extensionsPathParent]); 505 | assert.ok(!getExpandOptions('scss').snippets, 'There should be no custom snippets as extensionPath was faulty'); 506 | } catch (e) { 507 | throw new Error('There should not be an error'); 508 | } 509 | }); 510 | 511 | // https://github.com/microsoft/vscode/issues/116741 512 | it('should use the first valid custom snippets from an array of extensions path', async () => { 513 | const customSnippetKey = 'ch'; 514 | await updateExtensionsPath([]); 515 | const expandOptionsWithoutCustomSnippets = getExpandOptions('css'); 516 | assert(!expandOptionsWithoutCustomSnippets.snippets); 517 | 518 | // Use custom snippets from extensionsPathArray 519 | const extensionsPathArray = ["./this/is/not/valid"].concat(extensionsPath); 520 | await updateExtensionsPath(extensionsPathArray); 521 | const expandOptionsWithCustomSnippets = getExpandOptions('css'); 522 | 523 | assert.ok(expandOptionsWithCustomSnippets.snippets); 524 | assert.strictEqual(Object.keys(expandOptionsWithCustomSnippets.snippets).some(key => key === customSnippetKey), true); 525 | }); 526 | 527 | // https://github.com/microsoft/vscode/issues/117515 528 | it('should override earlier snippets with later snippets', async () => { 529 | const extensionsPathArray = [ 530 | path.join(path.normalize(path.join(__dirname, '../../..')), 'testData', 'custom-snippets-profile'), 531 | path.join(path.normalize(path.join(__dirname, '../../..')), 'testData', 'custom-snippets-without-inheritance') 532 | ]; 533 | try { 534 | await updateExtensionsPath(extensionsPathArray); 535 | const expandOptions = getExpandOptions('css'); 536 | assert.ok(expandOptions); 537 | assert.ok(expandOptions.snippets); 538 | assert.strictEqual(expandOptions.snippets['hello'], 'margin: 100px;'); 539 | } catch (e) { 540 | throw new Error('There should not be an error'); 541 | } 542 | }); 543 | 544 | // https://github.com/microsoft/vscode/issues/120435 545 | it('should do nothing when all extensionsPath in the array are invalid', async () => { 546 | const extensionsPathArray = ["./this/is/not/valid", "./this/is/also/not/valid"]; 547 | try { 548 | await updateExtensionsPath(extensionsPathArray); 549 | } catch (e) { 550 | throw new Error('There should not be an error'); 551 | } 552 | }); 553 | 554 | // https://github.com/microsoft/vscode/issues/130868 555 | it('should still suggest link:css snippet', async () => { 556 | await updateExtensionsPath(extensionsPath); 557 | const document = TextDocument.create('test://test/test.html', 'html', 0, 'lin'); 558 | const position = Position.create(0, 3); 559 | const completionList = doComplete(document, position, 'html', { 560 | preferences: {}, 561 | showExpandedAbbreviation: 'always', 562 | showAbbreviationSuggestions: true, 563 | syntaxProfiles: {}, 564 | variables: {} 565 | }); 566 | 567 | assert.ok(completionList); 568 | assert.ok(completionList.items.some(y => y.label === 'link'), 'No link suggestion given.'); 569 | assert.ok(completionList.items.some(y => y.label === 'link:css'), 'No link:css suggestion given.'); 570 | }); 571 | }); 572 | 573 | describe('Test emmet preferences', () => { 574 | it('should use stylesheet preferences', () => { 575 | assert.strictEqual(expandAbbreviation('m10', getExpandOptions('css', { preferences: { 'css.propertyEnd': ';;' } })), 'margin: 10px;;'); 576 | assert.strictEqual(expandAbbreviation('m10', getExpandOptions('scss', { preferences: { 'scss.valueSeparator': '::' } })), 'margin::10px;'); 577 | assert.strictEqual(expandAbbreviation('m10', getExpandOptions('less', { preferences: { 'css.intUnit': 'pt' } })), 'margin: 10pt;'); 578 | assert.strictEqual(expandAbbreviation('m10.2', getExpandOptions('css', { preferences: { 'css.floatUnit': 'ex' } })), 'margin: 10.2ex;'); 579 | assert.strictEqual(expandAbbreviation('m10r', getExpandOptions('css', { preferences: { 'css.unitAliases': 'e:em, p:%,r: /rem' } })), 'margin: 10 /rem;'); 580 | assert.strictEqual(expandAbbreviation('m10p', getExpandOptions('css', { preferences: { 'css.unitAliases': 'e:em, p:%,r: /rem' } })), 'margin: 10%;'); 581 | }); 582 | }); 583 | 584 | describe('Test filters (bem and comment)', () => { 585 | it('should expand haml', async () => { 586 | await updateExtensionsPath([]); 587 | assert.strictEqual(expandAbbreviation('ul[data="class"]', getExpandOptions('haml', {})), '%ul(data="class") ${0}'); 588 | }); 589 | 590 | it('should expand attributes with []', async () => { 591 | await updateExtensionsPath([]); 592 | assert.strictEqual(expandAbbreviation('div[[a]="b"]', getExpandOptions('html', {})), '
${0}
'); 593 | }); 594 | 595 | it('should expand abbreviations that are nodes with no name', async () => { 596 | await updateExtensionsPath([]); 597 | assert.strictEqual(expandAbbreviation('c', getExpandOptions('html', {})), ''); 598 | }); 599 | 600 | it('should use filters from expandOptions', async () => { 601 | await updateExtensionsPath([]); 602 | assert.strictEqual(expandAbbreviation(bemFilterExample, getExpandOptions('html', {}, 'bem')), expectedBemFilterOutput); 603 | assert.strictEqual(expandAbbreviation(commentFilterExample, getExpandOptions('html', {}, 'c')), expectedCommentFilterOutput); 604 | assert.strictEqual(expandAbbreviation(bemCommentFilterExample, getExpandOptions('html', {}, 'bem,c')), expectedBemCommentFilterOutput); 605 | }); 606 | 607 | it('should use filters from syntaxProfiles', async () => { 608 | await updateExtensionsPath([]); 609 | assert.strictEqual(expandAbbreviation(bemFilterExample, getExpandOptions('html', { 610 | syntaxProfiles: { 611 | html: { 612 | filters: 'html, bem' 613 | } 614 | } 615 | })), expectedBemFilterOutput); 616 | assert.strictEqual(expandAbbreviation(commentFilterExample, getExpandOptions('html', { 617 | syntaxProfiles: { 618 | html: { 619 | filters: 'html, c' 620 | } 621 | } 622 | })), expectedCommentFilterOutput); 623 | }); 624 | }); 625 | 626 | describe('Test completions', () => { 627 | it('should provide multiple common tags completions in html', async () => { 628 | await updateExtensionsPath([]); 629 | const document = TextDocument.create('test://test/test.html', 'html', 0, 'd'); 630 | const position = Position.create(0, 1); 631 | const completionList = doComplete(document, position, 'html', { 632 | preferences: {}, 633 | showExpandedAbbreviation: 'always', 634 | showAbbreviationSuggestions: true, 635 | syntaxProfiles: {}, 636 | variables: {} 637 | }); 638 | const expectedItems = ['dl', 'dt', 'dd', 'div']; 639 | 640 | assert.ok(completionList); 641 | assert.ok(expectedItems.every(x => completionList.items.some(y => y.label === x)), 'All common tags starting with d not found'); 642 | }); 643 | 644 | it('should provide multiple snippet suggestions in html', async () => { 645 | await updateExtensionsPath([]); 646 | const document = TextDocument.create('test://test/test.html', 'html', 0, 'a:'); 647 | const position = Position.create(0, 2); 648 | const completionList = doComplete(document, position, 'html', { 649 | preferences: {}, 650 | showExpandedAbbreviation: 'always', 651 | showAbbreviationSuggestions: true, 652 | syntaxProfiles: {}, 653 | variables: {} 654 | }); 655 | const expectedItems = ['a:link', 'a:mail', 'a:tel']; 656 | 657 | assert.ok(completionList); 658 | assert.ok(expectedItems.every(x => completionList.items.some(y => y.label === x)), 'All snippet suggestions for a: not found'); 659 | }); 660 | 661 | it('should not provide any suggestions in html for class names or id', async () => { 662 | await updateExtensionsPath([]); 663 | const testCases = ['div.col', 'div#col']; 664 | testCases.forEach(abbr => { 665 | const document = TextDocument.create('test://test/test.html', 'html', 0, abbr); 666 | const position = Position.create(0, abbr.length); 667 | const completionList = doComplete(document, position, 'html', { 668 | preferences: {}, 669 | showExpandedAbbreviation: 'always', 670 | showAbbreviationSuggestions: true, 671 | syntaxProfiles: {}, 672 | variables: {} 673 | }); 674 | 675 | assert.ok(completionList); 676 | assert.ok(completionList.items.every(x => x.label !== 'colg'), `colg is not a valid suggestion for ${abbr}`); 677 | }); 678 | }); 679 | 680 | it('should provide multiple snippet suggestions in html for nested abbreviations', async () => { 681 | await updateExtensionsPath([]); 682 | const testCases = ['ul>a:', 'ul+a:']; 683 | testCases.forEach(abbr => { 684 | const document = TextDocument.create('test://test/test.html', 'html', 0, abbr); 685 | const position = Position.create(0, abbr.length); 686 | const completionList = doComplete(document, position, 'html', { 687 | preferences: {}, 688 | showExpandedAbbreviation: 'always', 689 | showAbbreviationSuggestions: true, 690 | syntaxProfiles: {}, 691 | variables: {} 692 | }); 693 | const expectedItems = ['a:link', 'a:mail', 'a:tel']; 694 | 695 | assert.ok(completionList); 696 | assert.ok(expectedItems.every(x => completionList.items.some(y => y.label === x)), 'All snippet suggestions for a: not found'); 697 | }); 698 | }); 699 | 700 | it('should not provide link:m as a suggestion', async () => { 701 | // https://github.com/microsoft/vscode/issues/66680 702 | await updateExtensionsPath([]); 703 | const abbr = 'link:m'; 704 | const document = TextDocument.create('test://test/test.html', 'html', 0, abbr); 705 | const position = Position.create(0, abbr.length); 706 | const completionList = doComplete(document, position, 'html', { 707 | preferences: {}, 708 | showExpandedAbbreviation: 'always', 709 | showAbbreviationSuggestions: true, 710 | syntaxProfiles: {}, 711 | variables: {} 712 | }); 713 | 714 | assert.ok(completionList); 715 | assert.strictEqual(completionList.items.every(x => x.label !== 'link:m'), true); 716 | }); 717 | 718 | it('should not provide marginright as a suggestion SCSS', async () => { 719 | // https://github.com/microsoft/vscode-emmet-helper/issues/42 720 | await updateExtensionsPath([]); 721 | const abbr = 'marginright'; 722 | const document = TextDocument.create('test://test/test.scss', 'scss', 0, abbr); 723 | const position = Position.create(0, abbr.length); 724 | const completionList = doComplete(document, position, 'scss', { 725 | preferences: {}, 726 | showExpandedAbbreviation: 'always', 727 | showAbbreviationSuggestions: true, 728 | syntaxProfiles: {}, 729 | variables: {} 730 | }); 731 | 732 | assert.strictEqual(completionList, undefined); 733 | }); 734 | 735 | it('should provide completions html', async () => { 736 | await updateExtensionsPath([]); 737 | const bemFilterExampleWithInlineFilter = bemFilterExample + '|bem'; 738 | const commentFilterExampleWithInlineFilter = commentFilterExample + '|c'; 739 | const bemCommentFilterExampleWithInlineFilter = bemCommentFilterExample + '|bem|c'; 740 | const commentBemFilterExampleWithInlineFilter = bemCommentFilterExample + '|c|bem'; 741 | const testCases: [string, number, number, string, string, string][] = [ 742 | ['
ul>li*3
', 0, 7, 'ul', '
    |
', '
    \${0}
'], 743 | ['
UL
', 0, 7, 'UL', '
    |
', '
    \${0}
'], 744 | ['
ul>li*3
', 0, 10, 'ul>li', '
    \n\t
  • |
  • \n
', '
    \n\t
  • \${0}
  • \n
'], 745 | ['
(ul>li)*3
', 0, 14, '(ul>li)*3', '
    \n\t
  • |
  • \n
\n
    \n\t
  • |
  • \n
\n
    \n\t
  • |
  • \n
', '
    \n\t
  • \${1}
  • \n
\n
    \n\t
  • \${2}
  • \n
\n
    \n\t
  • \${0}
  • \n
'], 746 | ['
custom-tag
', 0, 15, 'custom-tag', '|', '\${0}'], 747 | ['
custom:tag
', 0, 15, 'custom:tag', '|', '\${0}'], 748 | ['
sp
', 0, 7, 'span', '|', '\${0}'], 749 | ['
SP
', 0, 7, 'SPan', '|', '\${0}'], 750 | ['
u-l-z
', 0, 10, 'u-l-z', '|', '\${0}'], 751 | ['
div.foo_
', 0, 13, 'div.foo_', '
|
', '
\${0}
'], 752 | [bemFilterExampleWithInlineFilter, 0, bemFilterExampleWithInlineFilter.length, bemFilterExampleWithInlineFilter, expectedBemFilterOutputDocs, expectedBemFilterOutput], 753 | [commentFilterExampleWithInlineFilter, 0, commentFilterExampleWithInlineFilter.length, commentFilterExampleWithInlineFilter, expectedCommentFilterOutputDocs, expectedCommentFilterOutput], 754 | [bemCommentFilterExampleWithInlineFilter, 0, bemCommentFilterExampleWithInlineFilter.length, bemCommentFilterExampleWithInlineFilter, expectedBemCommentFilterOutputDocs, expectedBemCommentFilterOutput], 755 | [commentBemFilterExampleWithInlineFilter, 0, commentBemFilterExampleWithInlineFilter.length, commentBemFilterExampleWithInlineFilter, expectedBemCommentFilterOutputDocs, expectedBemCommentFilterOutput], 756 | ['li*2+link:css', 0, 13, 'li*2+link:css', '
  • |
  • \n
  • |
  • \n', '
  • \${1}
  • \n
  • \${2}
  • \n'], 757 | ['li*10', 0, 5, 'li*10', '
  • |
  • \n
  • |
  • \n
  • |
  • \n
  • |
  • \n
  • |
  • \n
  • |
  • \n
  • |
  • \n
  • |
  • \n
  • |
  • \n
  • |
  • ', 758 | '
  • \${1}
  • \n
  • \${2}
  • \n
  • \${3}
  • \n
  • \${4}
  • \n
  • \${5}
  • \n
  • \${6}
  • \n
  • \${7}
  • \n
  • \${8}
  • \n
  • \${9}
  • \n
  • \${0}
  • '], 759 | ]; 760 | testCases.forEach(([content, positionLine, positionChar, expectedAbbr, expectedExpansionDocs, expectedExpansion]) => { 761 | const document = TextDocument.create('test://test/test.html', 'html', 0, content); 762 | const position = Position.create(positionLine, positionChar); 763 | const completionList = doComplete(document, position, 'html', { 764 | preferences: {}, 765 | showExpandedAbbreviation: 'always', 766 | showAbbreviationSuggestions: false, 767 | syntaxProfiles: {}, 768 | variables: {} 769 | }); 770 | 771 | assert.ok(completionList); 772 | assert.strictEqual(completionList.items[0].label, expectedAbbr); 773 | assert.strictEqual(completionList.items[0].documentation, expectedExpansionDocs); 774 | assert.strictEqual(completionList.items[0].textEdit?.newText, expectedExpansion); 775 | }); 776 | }); 777 | 778 | it('should provide completions css', async () => { 779 | await updateExtensionsPath([]); 780 | const testCases: [string, string][] = [ 781 | ['trf', 'transform: ;'], 782 | ['trf:rx', 'transform: rotateX(angle);'], 783 | ['trfrx', 'transform: rotateX(angle);'], 784 | ['m10+p10', 'margin: 10px;\npadding: 10px;'], 785 | ['brs', 'border-radius: ;'], 786 | ['brs5', 'border-radius: 5px;'], 787 | ['brs10px', 'border-radius: 10px;'], 788 | ['p', 'padding: ;'] 789 | ]; 790 | const positionLine = 0; 791 | testCases.forEach(([abbreviation, expected]) => { 792 | const document = TextDocument.create('test://test/test.css', 'css', 0, abbreviation); 793 | const position = Position.create(positionLine, abbreviation.length); 794 | const completionList = doComplete(document, position, 'css', { 795 | preferences: {}, 796 | showExpandedAbbreviation: 'always', 797 | showAbbreviationSuggestions: false, 798 | syntaxProfiles: {}, 799 | variables: {} 800 | }); 801 | 802 | assert.ok(completionList); 803 | assert.strictEqual(completionList.items[0].label, expected); 804 | assert.strictEqual(completionList.items[0].filterText, abbreviation); 805 | }); 806 | }); 807 | 808 | it('should not provide html completions for xml', async () => { 809 | // https://github.com/microsoft/vscode/issues/97632 810 | await updateExtensionsPath([]); 811 | const testCases: string[] = ['a', 'bo', 'body']; 812 | const positionLine = 0; 813 | testCases.forEach(abbreviation => { 814 | const document = TextDocument.create('test://test/test.xml', 'xml', 0, abbreviation); 815 | const position = Position.create(positionLine, abbreviation.length); 816 | const completionList = doComplete(document, position, 'xml', { 817 | preferences: {}, 818 | showExpandedAbbreviation: 'always', 819 | showAbbreviationSuggestions: true, 820 | syntaxProfiles: {}, 821 | variables: {} 822 | }); 823 | 824 | assert.strictEqual(completionList, undefined); 825 | }); 826 | }); 827 | 828 | it('should provide hex color completions css', async () => { 829 | await updateExtensionsPath([]); 830 | const testCases: [string, string][] = [ 831 | ['#1', '#111'], 832 | ['#ab', '#ababab'], 833 | ['#abc', '#abc'], 834 | ['c:#1', 'color: #111;'], 835 | ['c:#1a', 'color: #1a1a1a;'], 836 | ['bgc:1', 'background-color: 1px;'], 837 | ['c:#0.1', 'color: rgba(0, 0, 0, 0.1);'] 838 | ]; 839 | const positionLine = 0; 840 | testCases.forEach(([abbreviation, expected]) => { 841 | const document = TextDocument.create('test://test/test.css', 'css', 0, abbreviation); 842 | const position = Position.create(positionLine, abbreviation.length); 843 | const completionList = doComplete(document, position, 'css', { 844 | preferences: {}, 845 | showExpandedAbbreviation: 'always', 846 | showAbbreviationSuggestions: false, 847 | syntaxProfiles: {}, 848 | variables: {} 849 | }); 850 | 851 | assert.ok(completionList); 852 | assert.strictEqual(completionList.items[0].label, expected); 853 | assert.strictEqual(completionList.items[0].filterText, abbreviation); 854 | }); 855 | }); 856 | 857 | it.skip('should provide empty incomplete completion list for abbreviations that just have the vendor prefix', async () => { 858 | await updateExtensionsPath([]); 859 | const testCases: [string, number, number][] = [ 860 | ['-', 0, 1], 861 | ['-m-', 0, 3], 862 | ['-s-', 0, 3], 863 | ['-o-', 0, 3], 864 | ['-w-', 0, 3], 865 | ['-ow-', 0, 4], 866 | ['-mw-', 0, 4], 867 | ['-mo', 0, 3], 868 | ]; 869 | testCases.forEach(([abbreviation, positionLine, positionChar]) => { 870 | const document = TextDocument.create('test://test/test.css', 'css', 0, abbreviation); 871 | const position = Position.create(positionLine, positionChar); 872 | const completionList = doComplete(document, position, 'css', { 873 | preferences: {}, 874 | showExpandedAbbreviation: 'always', 875 | showAbbreviationSuggestions: false, 876 | syntaxProfiles: {}, 877 | variables: {} 878 | }); 879 | 880 | assert.ok(completionList); 881 | assert.strictEqual(completionList.items.length, 0, completionList.items.length ? completionList.items[0].label : 'all good'); 882 | assert.strictEqual(completionList.isIncomplete, true); 883 | }); 884 | }) 885 | 886 | it('should provide completions for text that are prefix for snippets, ensure $ doesnt get escaped', async () => { 887 | await updateExtensionsPath([]); 888 | const testCases: [string, number, number][] = [ 889 | ['
    l
    ', 0, 7] 890 | ]; 891 | testCases.forEach(([content, positionLine, positionChar]) => { 892 | const document = TextDocument.create('test://test/test.html', 'html', 0, content); 893 | const position = Position.create(positionLine, positionChar); 894 | const completionList = doComplete(document, position, 'html', { 895 | preferences: {}, 896 | showExpandedAbbreviation: 'always', 897 | showAbbreviationSuggestions: true, 898 | syntaxProfiles: {}, 899 | variables: {} 900 | }); 901 | 902 | assert.ok(completionList); 903 | assert.strictEqual(completionList.items.find(x => x.label === 'link')!.documentation, ''); 904 | assert.strictEqual(completionList.items.find(x => x.label === 'link')!.textEdit?.newText, ''); 905 | assert.strictEqual(completionList.items.find(x => x.label === 'link:css')!.documentation, ''); 906 | assert.strictEqual(completionList.items.find(x => x.label === 'link:css')!.textEdit?.newText, ''); 907 | }); 908 | }); 909 | 910 | it('should provide completions for scss', async () => { 911 | await updateExtensionsPath([]); 912 | const testCases: [string, number, number][] = [ 913 | ['m:a', 0, 3] 914 | ]; 915 | testCases.forEach(([content, positionLine, positionChar]) => { 916 | const document = TextDocument.create('test://test/test.scss', 'scss', 0, content); 917 | const position = Position.create(positionLine, positionChar); 918 | const completionList = doComplete(document, position, 'scss', { 919 | preferences: {}, 920 | showExpandedAbbreviation: 'always', 921 | showAbbreviationSuggestions: false, 922 | syntaxProfiles: {}, 923 | variables: {} 924 | }); 925 | 926 | assert.ok(completionList); 927 | assert.strictEqual(completionList.items.find(x => x.label === 'margin: auto;')!.documentation, 'margin: auto;'); 928 | }); 929 | }); 930 | 931 | it('should provide completions with escaped $ in scss', async () => { 932 | await updateExtensionsPath([]); 933 | const testCases: [string, number, number][] = [ 934 | ['bgi$hello', 0, 9] 935 | ]; 936 | testCases.forEach(([content, positionLine, positionChar]) => { 937 | const document = TextDocument.create('test://test/test.scss', 'scss', 0, content); 938 | const position = Position.create(positionLine, positionChar); 939 | const completionList = doComplete(document, position, 'scss', { 940 | preferences: {}, 941 | showExpandedAbbreviation: 'always', 942 | showAbbreviationSuggestions: false, 943 | syntaxProfiles: {}, 944 | variables: {} 945 | }); 946 | 947 | assert.ok(completionList); 948 | assert.strictEqual(completionList.items.find(x => x.label === 'background-image: $hello;')!.documentation, 'background-image: $hello;'); 949 | assert.strictEqual(completionList.items.find(x => x.label === 'background-image: $hello;')!.textEdit?.newText, 'background-image: \\$hello;'); 950 | }); 951 | }); 952 | 953 | it('should provide completions with escaped $ in html', async () => { 954 | await updateExtensionsPath([]); 955 | const testCases: [string, number, number, string, string][] = [ 956 | ['span{\\$5}', 0, 9, '$5', '\\$5'], 957 | ['span{\\$hello}', 0, 13, '$hello', '\\$hello'] 958 | ]; 959 | testCases.forEach(([content, positionLine, positionChar, expectedDoc, expectedSnippetText]) => { 960 | const document = TextDocument.create('test://test/test.html', 'html', 0, content); 961 | const position = Position.create(positionLine, positionChar); 962 | const completionList = doComplete(document, position, 'html', { 963 | preferences: {}, 964 | showExpandedAbbreviation: 'always', 965 | showAbbreviationSuggestions: false, 966 | syntaxProfiles: {}, 967 | variables: {} 968 | }); 969 | 970 | assert.ok(completionList); 971 | assert.strictEqual(completionList.items.find(x => x.label === content)!.documentation, expectedDoc); 972 | assert.strictEqual(completionList.items.find(x => x.label === content)!.textEdit?.newText, expectedSnippetText); 973 | }); 974 | }); 975 | 976 | it('should provide completions using custom snippets html', async () => { 977 | await updateExtensionsPath(extensionsPath); 978 | const testCases: [string, number, number, string, string][] = [ 979 | ['
    hey
    ', 0, 8, 'hey', '
      \n\t
    • |
    • \n\t
    • |
    • \n
    '] 980 | ]; 981 | testCases.forEach(([content, positionLine, positionChar, expectedAbbr, expectedExpansion]) => { 982 | const document = TextDocument.create('test://test/test.html', 'html', 0, content); 983 | const position = Position.create(positionLine, positionChar); 984 | const completionList = doComplete(document, position, 'html', { 985 | preferences: {}, 986 | showExpandedAbbreviation: 'always', 987 | showAbbreviationSuggestions: false, 988 | syntaxProfiles: { 989 | 'html': { 990 | 'tag_case': 'lower' 991 | } 992 | }, 993 | variables: {} 994 | }); 995 | 996 | assert.ok(completionList); 997 | assert.strictEqual(completionList.items[0].label, expectedAbbr); 998 | assert.strictEqual(completionList.items[0].documentation, expectedExpansion); 999 | }); 1000 | }); 1001 | 1002 | it('should provide completions using custom snippets css and unit aliases', async () => { 1003 | await updateExtensionsPath(extensionsPath); 1004 | const testCases: [string, number, number, string, string, string | undefined][] = [ 1005 | ['hel', 0, 3, 'hello', 'margin: 10px;', undefined], 1006 | ['hello', 0, 5, 'hello', 'margin: 10px;', undefined], 1007 | ['m10p', 0, 4, 'margin: 10%;', 'margin: 10%;', 'm10p'], 1008 | ['m10e', 0, 4, 'margin: 10hi;', 'margin: 10hi;', 'm10e'], 1009 | ['m10h', 0, 4, 'margin: 10hello;', 'margin: 10hello;', 'm10h'], 1010 | ['p10-20', 0, 6, 'padding: 10px 20px;', 'padding: 10px 20px;', 'p10-20'] // The - in the number range will result in filtering this item out, so filter text should match abbreviation 1011 | ]; 1012 | testCases.forEach(([content, positionLine, positionChar, expectedAbbr, expectedExpansion, expectedFilterText]) => { 1013 | const document = TextDocument.create('test://test/test.css', 'css', 0, content); 1014 | const position = Position.create(positionLine, positionChar); 1015 | const completionList = doComplete(document, position, 'css', { 1016 | preferences: { 1017 | 'css.unitAliases': 'e:hi,h:hello' 1018 | }, 1019 | showExpandedAbbreviation: 'always', 1020 | showAbbreviationSuggestions: false, 1021 | syntaxProfiles: {}, 1022 | variables: {} 1023 | }); 1024 | 1025 | assert.ok(completionList); 1026 | assert.strictEqual(completionList.items[0].label, expectedAbbr); 1027 | assert.strictEqual(completionList.items[0].documentation, expectedExpansion); 1028 | assert.strictEqual(completionList.items[0].filterText, expectedFilterText); 1029 | }); 1030 | }); 1031 | 1032 | it('should provide both custom and default snippet completion when partial match with custom snippet', async () => { 1033 | await updateExtensionsPath(extensionsPath); 1034 | const expandOptions = { 1035 | preferences: {}, 1036 | showExpandedAbbreviation: 'always', 1037 | showAbbreviationSuggestions: false, 1038 | syntaxProfiles: {}, 1039 | variables: {} 1040 | }; 1041 | 1042 | const completionList1 = doComplete(TextDocument.create('test://test/test.css', 'css', 0, 'm'), Position.create(0, 1), 'css', expandOptions); 1043 | const completionList2 = doComplete(TextDocument.create('test://test/test.css', 'css', 0, 'mr'), Position.create(0, 2), 'css', expandOptions); 1044 | 1045 | assert.ok(completionList1); 1046 | assert.strictEqual(completionList1.items.some(x => x.label === 'margin: ;'), true); 1047 | assert.strictEqual(completionList1.items.some(x => x.label === 'mrgstart'), true); 1048 | 1049 | assert.ok(completionList2); 1050 | assert.strictEqual(completionList2.items.some(x => x.label === 'margin-right: ;'), true); 1051 | assert.strictEqual(completionList2.items.some(x => x.label === 'mrgstart'), true); 1052 | }); 1053 | 1054 | it('should not provide completions as they would noise when typing (html)', async () => { 1055 | await updateExtensionsPath([]); 1056 | const testCases: [string, number, number][] = [ 1057 | ['
    abc
    ', 0, 8], 1058 | ['
    Abc
    ', 0, 8], 1059 | ['
    abc12
    ', 0, 10], 1060 | ['
    abc.
    ', 0, 9], 1061 | ['
    (div)
    ', 0, 10], 1062 | ['
    ($db)
    ', 0, 10], 1063 | ['
    ($db.)
    ', 0, 11], 1064 | ['
    ul::l
    ', 0, 10], 1065 | ['ul:', 0, 8] // https://github.com/Microsoft/vscode/issues/49376 1067 | ]; 1068 | testCases.forEach(([content, positionLine, positionChar]) => { 1069 | const document = TextDocument.create('test://test/test.html', 'html', 0, content); 1070 | const position = Position.create(positionLine, positionChar); 1071 | const completionList = doComplete(document, position, 'html', { 1072 | preferences: {}, 1073 | showExpandedAbbreviation: 'always', 1074 | showAbbreviationSuggestions: false, 1075 | syntaxProfiles: {}, 1076 | variables: {} 1077 | }); 1078 | 1079 | assert.strictEqual(!completionList, true, (completionList && completionList.items.length > 0) ? completionList.items[0].label + ' should not show up' : 'All good'); 1080 | }); 1081 | }); 1082 | 1083 | it('should provide completions for pascal-case tags when typing (jsx)', async () => { 1084 | await updateExtensionsPath([]); 1085 | const testCases: [string, number, number, string, string][] = [ 1086 | ['
    Router
    ', 0, 11, 'Router', '|',], 1087 | ['
    MyAwesomeComponent
    ', 0, 23, 'MyAwesomeComponent', '|'], 1088 | ]; 1089 | testCases.forEach(([content, positionLine, positionChar, expectedAbbr, expectedExpansion]) => { 1090 | const document = TextDocument.create('test://test/test.jsx', 'jsx', 0, content); 1091 | const position = Position.create(positionLine, positionChar); 1092 | const completionList = doComplete(document, position, 'jsx', { 1093 | preferences: {}, 1094 | showExpandedAbbreviation: 'always', 1095 | showAbbreviationSuggestions: false, 1096 | syntaxProfiles: {}, 1097 | variables: {} 1098 | }); 1099 | 1100 | assert.ok(completionList); 1101 | assert.strictEqual(completionList.items[0].label, expectedAbbr); 1102 | assert.strictEqual(completionList.items[0].documentation, expectedExpansion); 1103 | }); 1104 | }) 1105 | 1106 | it('should not provide completions as they would noise when typing (css)', async () => { 1107 | await updateExtensionsPath([]); 1108 | const testCases: [string, number, number][] = [ 1109 | ['background', 0, 10], 1110 | ['font-family', 0, 11], 1111 | ['width', 0, 5], 1112 | ['background:u', 0, 12], 1113 | ['text-overflo', 0, 12] // Partial match with property name 1114 | ]; 1115 | testCases.forEach(([content, positionLine, positionChar]) => { 1116 | const document = TextDocument.create('test://test/test.css', 'css', 0, content); 1117 | const position = Position.create(positionLine, positionChar); 1118 | const completionList = doComplete(document, position, 'css', { 1119 | preferences: {}, 1120 | showExpandedAbbreviation: 'always', 1121 | showAbbreviationSuggestions: false, 1122 | syntaxProfiles: {}, 1123 | variables: {} 1124 | }); 1125 | 1126 | assert.strictEqual(!completionList || !completionList.items || !completionList.items.length, true, (completionList && completionList.items.length > 0) ? completionList.items[0].label + ' should not show up' : 'All good'); 1127 | }); 1128 | }); 1129 | 1130 | it('should provide completions for loremn with n words', async () => { 1131 | await updateExtensionsPath([]); 1132 | const document = TextDocument.create('test://test/test.html', 'html', 0, '.item>lorem10'); 1133 | const position = Position.create(0, 13); 1134 | const completionList = doComplete(document, position, 'html', { 1135 | preferences: {}, 1136 | showExpandedAbbreviation: 'always', 1137 | showAbbreviationSuggestions: false, 1138 | syntaxProfiles: {}, 1139 | variables: {} 1140 | }); 1141 | assert.ok(completionList); 1142 | 1143 | const expandedText = completionList.items[0].documentation; 1144 | if (typeof expandedText !== 'string') { 1145 | return; 1146 | } 1147 | const matches = expandedText.match(/
    (.*)<\/div>/); 1148 | 1149 | assert.strictEqual(completionList.items[0].label, '.item>lorem10'); 1150 | assert.ok(matches); 1151 | assert.strictEqual(matches[1].split(' ').length, 10); 1152 | assert.strictEqual(matches[1].startsWith('Lorem'), true); 1153 | }); 1154 | 1155 | it('should provide completions for lorem*n with n lines', async () => { 1156 | await updateExtensionsPath([]); 1157 | const document = TextDocument.create('test://test/test.html', 'html', 0, 'lorem*3'); 1158 | const position = Position.create(0, 12); 1159 | const completionList = doComplete(document, position, 'html', { 1160 | preferences: {}, 1161 | showExpandedAbbreviation: 'always', 1162 | showAbbreviationSuggestions: false, 1163 | syntaxProfiles: {}, 1164 | variables: {} 1165 | }); 1166 | assert.ok(completionList); 1167 | 1168 | const expandedText = completionList.items[0].documentation; 1169 | if (typeof expandedText !== 'string') { 1170 | return; 1171 | } 1172 | 1173 | assert.strictEqual(completionList.items[0].label, 'lorem*3'); 1174 | assert.strictEqual(expandedText.split('\n').length, 3); 1175 | assert.strictEqual(expandedText.startsWith('Lorem'), true); 1176 | }); 1177 | 1178 | it('should provide completions for lorem*2 with 2 lines', async () => { 1179 | // https://github.com/microsoft/vscode/issues/52345 1180 | await updateExtensionsPath([]); 1181 | const document = TextDocument.create('test://test/test.html', 'html', 0, 'lorem*2'); 1182 | const position = Position.create(0, 12); 1183 | const completionList = doComplete(document, position, 'html', { 1184 | preferences: {}, 1185 | showExpandedAbbreviation: 'always', 1186 | showAbbreviationSuggestions: false, 1187 | syntaxProfiles: {}, 1188 | variables: {} 1189 | }); 1190 | assert.ok(completionList); 1191 | 1192 | const expandedText = completionList.items[0].documentation; 1193 | if (typeof expandedText !== 'string') { 1194 | return; 1195 | } 1196 | 1197 | assert.strictEqual(completionList.items[0].label, 'lorem*2'); 1198 | assert.strictEqual(expandedText.split('\n').length, 2); 1199 | assert.strictEqual(expandedText.startsWith('Lorem'), true); 1200 | }); 1201 | 1202 | it.skip('should provide completions using vendor prefixes', async () => { 1203 | await updateExtensionsPath(extensionsPath); 1204 | const testCases: [string, number, number, string, string, string][] = [ 1205 | ['brs', 0, 3, 'border-radius: ;', 'border-radius: |;', 'brs'], 1206 | ['brs5', 0, 4, 'border-radius: 5px;', 'border-radius: 5px;', 'brs5'], 1207 | ['-brs', 0, 4, 'border-radius: ;', '-webkit-border-radius: |;\n-moz-border-radius: |;\nborder-radius: |;', '-brs'], 1208 | ['-mo-brs', 0, 7, 'border-radius: ;', '-moz-border-radius: |;\n-o-border-radius: |;\nborder-radius: |;', '-mo-brs'], 1209 | ['-om-brs', 0, 7, 'border-radius: ;', '-o-border-radius: |;\n-moz-border-radius: |;\nborder-radius: |;', '-om-brs'], 1210 | ['-brs10', 0, 6, 'border-radius: 10px;', '-webkit-border-radius: 10px;\n-moz-border-radius: 10px;\nborder-radius: 10px;', '-brs10'], 1211 | ['-bdts', 0, 5, 'border-top-style: ;', '-webkit-border-top-style: |;\n-moz-border-top-style: |;\n-ms-border-top-style: |;\n-o-border-top-style: |;\nborder-top-style: |;', '-bdts'], 1212 | ['-p', 0, 2, 'padding: ;', '-webkit-padding: |;\n-moz-padding: |;\n-ms-padding: |;\n-o-padding: |;\npadding: |;', '-p'], 1213 | ['-p10-20p', 0, 8, 'padding: 10px 20%;', '-webkit-padding: 10px 20%;\n-moz-padding: 10px 20%;\n-ms-padding: 10px 20%;\n-o-padding: 10px 20%;\npadding: 10px 20%;', '-p10-20p'], 1214 | ]; 1215 | testCases.forEach(([content, positionLine, positionChar, expectedLabel, expectedExpansion, expectedFilterText]) => { 1216 | const document = TextDocument.create('test://test/test.css', 'css', 0, content); 1217 | const position = Position.create(positionLine, positionChar); 1218 | const completionList = doComplete(document, position, 'css', { 1219 | preferences: {}, 1220 | showExpandedAbbreviation: 'always', 1221 | showAbbreviationSuggestions: false, 1222 | syntaxProfiles: {}, 1223 | variables: {} 1224 | }); 1225 | 1226 | assert.ok(completionList); 1227 | assert.strictEqual(completionList.items[0].label, expectedLabel); 1228 | assert.strictEqual(completionList.items[0].documentation, expectedExpansion); 1229 | assert.strictEqual(completionList.items[0].filterText, expectedFilterText); 1230 | }); 1231 | }); 1232 | 1233 | it.skip('should provide completions using vendor prefixes with custom preferences', async () => { 1234 | await updateExtensionsPath(extensionsPath); 1235 | const testCases: [string, number, number, string, string, string][] = [ 1236 | ['brs', 0, 3, 'border-radius: ;', 'border-radius: |;', 'brs'], 1237 | ['brs5', 0, 4, 'border-radius: 5px;', 'border-radius: 5px;', 'brs5'], 1238 | ['-brs', 0, 4, 'border-radius: ;', '-webkit-border-radius: |;\nborder-radius: |;', '-brs'], 1239 | ['-mo-brs', 0, 7, 'border-radius: ;', '-moz-border-radius: |;\n-o-border-radius: |;\nborder-radius: |;', '-mo-brs'], 1240 | ['-bdts', 0, 5, 'border-top-style: ;', '-o-border-top-style: |;\nborder-top-style: |;', '-bdts'], 1241 | ['-bdi', 0, 4, 'border-image: url();', '-webkit-border-image: url(|);\n-moz-border-image: url(|);\n-ms-border-image: url(|);\n-o-border-image: url(|);\nborder-image: url(|);', '-bdi'] 1242 | ]; 1243 | testCases.forEach(([content, positionLine, positionChar, expectedLabel, expectedExpansion, expectedFilterText]) => { 1244 | const document = TextDocument.create('test://test/test.css', 'css', 0, content); 1245 | const position = Position.create(positionLine, positionChar); 1246 | const completionList = doComplete(document, position, 'css', { 1247 | preferences: { 1248 | 'css.webkitProperties': 'foo, bar,padding , border-radius', 1249 | 'css.mozProperties': '', 1250 | 'css.oProperties': 'border-top-style', 1251 | }, 1252 | showExpandedAbbreviation: 'always', 1253 | showAbbreviationSuggestions: false, 1254 | syntaxProfiles: {}, 1255 | variables: {} 1256 | }); 1257 | 1258 | assert.ok(completionList); 1259 | assert.strictEqual(completionList.items[0].label, expectedLabel); 1260 | assert.strictEqual(completionList.items[0].documentation, expectedExpansion); 1261 | assert.strictEqual(completionList.items[0].filterText, expectedFilterText); 1262 | }); 1263 | }); 1264 | 1265 | it.skip('should expand with multiple vendor prefixes', async () => { 1266 | await updateExtensionsPath([]); 1267 | assert.strictEqual(expandAbbreviation('brs', getExpandOptions('css', {})), 'border-radius: ${0};'); 1268 | assert.strictEqual(expandAbbreviation('brs5', getExpandOptions('css', {})), 'border-radius: 5px;'); 1269 | assert.strictEqual(expandAbbreviation('brs10px', getExpandOptions('css', {})), 'border-radius: 10px;'); 1270 | assert.strictEqual(expandAbbreviation('-brs', getExpandOptions('css', {})), '-webkit-border-radius: ${0};\n-moz-border-radius: ${0};\nborder-radius: ${0};'); 1271 | assert.strictEqual(expandAbbreviation('-brs10', getExpandOptions('css', {})), '-webkit-border-radius: 10px;\n-moz-border-radius: 10px;\nborder-radius: 10px;'); 1272 | assert.strictEqual(expandAbbreviation('-bdts', getExpandOptions('css', {})), '-webkit-border-top-style: ${0};\n-moz-border-top-style: ${0};\n-ms-border-top-style: ${0};\n-o-border-top-style: ${0};\nborder-top-style: ${0};'); 1273 | assert.strictEqual(expandAbbreviation('-bdts2px', getExpandOptions('css', {})), '-webkit-border-top-style: 2px;\n-moz-border-top-style: 2px;\n-ms-border-top-style: 2px;\n-o-border-top-style: 2px;\nborder-top-style: 2px;'); 1274 | assert.strictEqual(expandAbbreviation('-p10-20', getExpandOptions('css', {})), '-webkit-padding: 10px 20px;\n-moz-padding: 10px 20px;\n-ms-padding: 10px 20px;\n-o-padding: 10px 20px;\npadding: 10px 20px;'); 1275 | assert.strictEqual(expandAbbreviation('-p10p20', getExpandOptions('css', {})), '-webkit-padding: 10% 20px;\n-moz-padding: 10% 20px;\n-ms-padding: 10% 20px;\n-o-padding: 10% 20px;\npadding: 10% 20px;'); 1276 | assert.strictEqual(expandAbbreviation('-mo-brs', getExpandOptions('css', {})), '-moz-border-radius: ${0};\n-o-border-radius: ${0};\nborder-radius: ${0};'); 1277 | }); 1278 | 1279 | it.skip('should expand with default vendor prefixes in properties', async () => { 1280 | await updateExtensionsPath([]); 1281 | assert.strictEqual(expandAbbreviation('-p', getExpandOptions('css', { preferences: { 'css.webkitProperties': 'foo, bar, padding' } })), '-webkit-padding: ${0};\npadding: ${0};'); 1282 | assert.strictEqual(expandAbbreviation('-p', getExpandOptions('css', { preferences: { 'css.oProperties': 'padding', 'css.webkitProperties': 'padding' } })), '-webkit-padding: ${0};\n-o-padding: ${0};\npadding: ${0};'); 1283 | assert.strictEqual(expandAbbreviation('-brs', getExpandOptions('css', { preferences: { 'css.oProperties': 'padding', 'css.webkitProperties': 'padding', 'css.mozProperties': '', 'css.msProperties': '' } })), '-webkit-border-radius: ${0};\n-moz-border-radius: ${0};\n-ms-border-radius: ${0};\n-o-border-radius: ${0};\nborder-radius: ${0};'); 1284 | assert.strictEqual(expandAbbreviation('-o-p', getExpandOptions('css', { preferences: { 'css.oProperties': 'padding', 'css.webkitProperties': 'padding' } })), '-o-padding: ${0};\npadding: ${0};'); 1285 | }); 1286 | 1287 | it('should not provide completions for excludedLanguages', async () => { 1288 | await updateExtensionsPath([]); 1289 | const document = TextDocument.create('test://test/test.html', 'html', 0, 'ul>li'); 1290 | const position = Position.create(0, 5); 1291 | const completionList = doComplete(document, position, 'html', { 1292 | preferences: {}, 1293 | showExpandedAbbreviation: 'always', 1294 | showAbbreviationSuggestions: false, 1295 | syntaxProfiles: {}, 1296 | variables: {}, 1297 | excludeLanguages: ['html'] 1298 | }); 1299 | 1300 | assert.strictEqual(!completionList, true); 1301 | }); 1302 | 1303 | it('should provide completions with kind snippet when showSuggestionsAsSnippets is enabled', async () => { 1304 | await updateExtensionsPath([]); 1305 | const document = TextDocument.create('test://test/test.html', 'html', 0, 'ul>li'); 1306 | const position = Position.create(0, 5); 1307 | const completionList = doComplete(document, position, 'html', { 1308 | preferences: {}, 1309 | showExpandedAbbreviation: 'always', 1310 | showAbbreviationSuggestions: false, 1311 | syntaxProfiles: {}, 1312 | variables: {}, 1313 | showSuggestionsAsSnippets: true 1314 | }); 1315 | 1316 | assert.ok(completionList); 1317 | assert.strictEqual(completionList.items[0].kind, CompletionItemKind.Snippet); 1318 | assert.strictEqual(completionList.items[0].detail, 'Emmet Abbreviation'); 1319 | }); 1320 | 1321 | it('should not provide double completions for commonly used tags that are also snippets', async () => { 1322 | await updateExtensionsPath([]); 1323 | const document = TextDocument.create('test://test/test.html', 'html', 0, 'abb'); 1324 | const position = Position.create(0, 3); 1325 | const completionList = doComplete(document, position, 'html', { 1326 | preferences: {}, 1327 | showExpandedAbbreviation: 'always', 1328 | showAbbreviationSuggestions: true, 1329 | syntaxProfiles: {}, 1330 | variables: {}, 1331 | excludeLanguages: [] 1332 | }); 1333 | 1334 | assert.ok(completionList); 1335 | assert.strictEqual(completionList.items.length, 1); 1336 | assert.strictEqual(completionList.items[0].label, 'abbr'); 1337 | }); 1338 | 1339 | it('should complete JSX tags with attribute overrides', async () => { 1340 | await updateExtensionsPath([]); 1341 | const options = { 1342 | 'syntaxProfiles': { 1343 | 'jsx': { 1344 | 'markup.attributes': { 1345 | 'class': 'classPlainName', 1346 | 'class*': 'classStarName' 1347 | }, 1348 | 'markup.valuePrefix': { 1349 | 'class': 'classPlainPrefix', 1350 | 'class*': 'classStarPrefix' 1351 | } 1352 | } 1353 | } 1354 | }; 1355 | const expanded = expandAbbreviation('..test', getExpandOptions('jsx', options)); 1356 | assert.strictEqual(expanded, '
    ${0}
    '); 1357 | const expandedSecond = expandAbbreviation('.test', getExpandOptions('jsx', options)); 1358 | assert.strictEqual(expandedSecond, '
    ${0}
    '); 1359 | }); 1360 | 1361 | it('should complete JSX tags with empty string value prefix', async () => { 1362 | await updateExtensionsPath([]); 1363 | const options = { 1364 | 'syntaxProfiles': { 1365 | 'jsx': { 1366 | 'markup.attributes': { 1367 | 'class': 'classPlainName', 1368 | 'class*': 'classStarName' 1369 | }, 1370 | 'markup.valuePrefix': { 1371 | 'class': '', 1372 | 'class*': '' 1373 | } 1374 | } 1375 | } 1376 | }; 1377 | const expanded = expandAbbreviation('..test', getExpandOptions('jsx', options)); 1378 | assert.strictEqual(expanded, '
    ${0}
    '); 1379 | const expandedSecond = expandAbbreviation('.test', getExpandOptions('jsx', options)); 1380 | assert.strictEqual(expandedSecond, '
    ${0}
    '); 1381 | }); 1382 | 1383 | it('should complete JSX tags with partial attribute overrides', async () => { 1384 | const options = { 1385 | 'syntaxProfiles': { 1386 | 'jsx': { 1387 | 'markup.attributes': { 1388 | 'class*': 'classStarName' 1389 | }, 1390 | 'markup.valuePrefix': { 1391 | 'class': 'classPlainPrefix', 1392 | } 1393 | } 1394 | } 1395 | }; 1396 | const expanded = expandAbbreviation('.test', getExpandOptions('jsx', options)); 1397 | assert.strictEqual(expanded, '
    ${0}
    '); 1398 | const expandedSecond = expandAbbreviation('..test', getExpandOptions('jsx', options)); 1399 | assert.strictEqual(expandedSecond, '
    ${0}
    '); 1400 | }); 1401 | }) 1402 | -------------------------------------------------------------------------------- /src/test/expand.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Options, UserConfig } from 'emmet'; 3 | import { describe, it } from 'mocha'; 4 | import { TextDocument, TextEdit } from 'vscode-languageserver-textdocument'; 5 | import { Position } from 'vscode-languageserver-types' 6 | import { doComplete, expandAbbreviation, getSyntaxType, VSCodeEmmetConfig } from '../emmetHelper'; 7 | 8 | const COMPLETE_OPTIONS = { 9 | preferences: {}, 10 | showExpandedAbbreviation: 'always', 11 | showAbbreviationSuggestions: false, 12 | syntaxProfiles: {}, 13 | variables: {} 14 | } 15 | 16 | function testExpandWithCompletion(syntax: string, abbrev: string, expanded: string, options?: VSCodeEmmetConfig) { 17 | it(`should expand ${abbrev} to\n${expanded}`, async () => { 18 | const document = TextDocument.create(`test://test/test.${syntax}`, syntax, 0, abbrev); 19 | const position = Position.create(0, abbrev.length); 20 | 21 | const completionList = doComplete(document, position, syntax, options ?? COMPLETE_OPTIONS); 22 | 23 | assert.ok(completionList && completionList.items, `completion list exists for ${abbrev}`); 24 | assert.ok(completionList.items.length > 0, `completion list is not empty for ${abbrev}`); 25 | 26 | assert.strictEqual(expanded, TextDocument.applyEdits(document, [completionList.items[0].textEdit])); 27 | }); 28 | } 29 | 30 | function testCountCompletions(syntax: string, abbrev: string, expectedNumCompletions: number) { 31 | it(`should expand ${abbrev} with ${expectedNumCompletions} completions`, async () => { 32 | const document = TextDocument.create(`test://test/test.${syntax}`, syntax, 0, abbrev); 33 | const position = Position.create(0, abbrev.length); 34 | 35 | const completionList = doComplete(document, position, syntax, COMPLETE_OPTIONS); 36 | 37 | if (expectedNumCompletions) { 38 | assert.ok(completionList && completionList.items, `completion list exists for ${abbrev}`); 39 | assert.strictEqual(completionList.items.length, expectedNumCompletions); 40 | } else { 41 | assert.strictEqual(completionList, undefined); 42 | } 43 | }); 44 | } 45 | 46 | function testExpand(syntax: string, abbrev: string, expanded: string, options?: Partial) { 47 | it(`should expand ${abbrev} to\n${expanded}`, async () => { 48 | const type = getSyntaxType(syntax); 49 | const config: UserConfig = { 50 | type, 51 | syntax, 52 | options 53 | } 54 | const expandedRes = expandAbbreviation(abbrev, config); 55 | assert.strictEqual(expanded, expandedRes); 56 | }); 57 | } 58 | 59 | function testWrap(abbrev: string, text: string | string[], expanded: string, options?: Partial, language: 'html' | 'jsx' = 'html') { 60 | it(`should wrap ${text} with ${abbrev} to obtain\n${expanded}`, async () => { 61 | const syntax = language; 62 | const type = getSyntaxType(syntax); 63 | const config: UserConfig = { 64 | type, 65 | syntax, 66 | text, 67 | options 68 | }; 69 | const expandedRes = expandAbbreviation(abbrev, config); 70 | assert.strictEqual(expanded, expandedRes); 71 | }); 72 | } 73 | 74 | describe('Expand Abbreviations', () => { 75 | testExpandWithCompletion('html', 'ul>li', '
      \n\t
    • ${0}
    • \n
    '); 76 | 77 | // https://github.com/microsoft/vscode/issues/59951 78 | testExpandWithCompletion('scss', 'fsz18', 'font-size: 18px;'); 79 | 80 | // https://github.com/microsoft/vscode/issues/63703 81 | testExpandWithCompletion('jsx', 'button[onClick={props.onClick}]', ''); 82 | 83 | // https://github.com/microsoft/vscode/issues/65464 84 | testExpand('css', 'bd1#s', 'border: 1px #000 solid;'); 85 | 86 | // https://github.com/microsoft/vscode/issues/65904 87 | testExpand('html', '(div>div.aaa{$}+div.bbb{$})*2', '
    \n\t
    1
    \n\t
    1
    \n
    \n
    \n\t
    2
    \n\t
    2
    \n
    ') 88 | 89 | // https://github.com/microsoft/vscode/issues/67971 90 | testExpandWithCompletion('html', 'div>p+lorem3', '
    \n\t

    ${0}

    \n\tLorem, ipsum dolor.\n
    '); 91 | 92 | // https://github.com/microsoft/vscode/issues/69168 93 | testExpandWithCompletion('html', 'ul>li{my list $@-}*3', '
      \n\t
    • my list 3
    • \n\t
    • my list 2
    • \n\t
    • my list 1
    • \n
    '); 94 | 95 | // https://github.com/microsoft/vscode/issues/72594 96 | testExpand('css', 'c#1', 'color: #111;', { "stylesheet.shortHex": true }); 97 | testExpand('css', 'c#1', 'color: #111111;', { "stylesheet.shortHex": false }); 98 | 99 | // https://github.com/microsoft/vscode/issues/74505 100 | testExpandWithCompletion('css', '@f', '@font-face {\n\tfont-family: ${1};\n\tsrc: url(${0});\n}'); 101 | testExpandWithCompletion('css', '@i', '@import url(${0});'); 102 | testExpandWithCompletion('css', '@import', '@import url(${0});'); 103 | testExpandWithCompletion('css', '@kf', '@keyframes ${1:identifier} {\n\t${0}\n}'); 104 | testExpandWithCompletion('css', '@', '@media ${1:screen} {\n\t${0}\n}'); 105 | testExpandWithCompletion('css', '@m', '@media ${1:screen} {\n\t${0}\n}'); 106 | 107 | // https://github.com/microsoft/vscode/issues/84608 108 | // testExpandWithCompletion('css', 'bg:n', 'background: none;'); 109 | 110 | // https://github.com/microsoft/vscode/issues/92120 111 | testExpandWithCompletion('css', 'd', 'display: ${1:block};'); 112 | 113 | // https://github.com/microsoft/vscode/issues/92231 114 | testExpandWithCompletion('html', 'div[role=tab]>(div>div)+div', '
    \n\t
    \n\t\t
    ${1}
    \n\t
    \n\t
    ${0}
    \n
    '); 115 | 116 | // https://github.com/microsoft/vscode/issues/105697 117 | testExpandWithCompletion('css', 'opa.', 'opacity: .;'); 118 | testExpandWithCompletion('css', 'opa.1', 'opacity: 0.1;'); 119 | testExpandWithCompletion('css', 'opa1', 'opacity: 1;'); 120 | testExpandWithCompletion('css', 'opa.a', 'opacity: .a;'); 121 | 122 | // https://github.com/microsoft/vscode/issues/114923 123 | testExpandWithCompletion('html', 'figcaption', '
    ${0}
    '); 124 | 125 | // https://github.com/microsoft/vscode/issues/115623 126 | testCountCompletions('html', 'html', 1); 127 | testCountCompletions('html', 'body', 1); 128 | 129 | // https://github.com/microsoft/vscode/issues/115839 130 | testExpandWithCompletion('css', 'bgc', 'background-color: ${1:#fff};'); 131 | testExpandWithCompletion('sass', 'bgc', 'background-color: ${1:#fff}'); 132 | 133 | // https://github.com/microsoft/vscode/issues/115854 134 | testCountCompletions('sass', 'bkco', 0); 135 | testCountCompletions('sass', 'bgc', 1); 136 | 137 | // https://github.com/microsoft/vscode/issues/115946 138 | testExpandWithCompletion('html', '{test}*3', 'test\ntest\ntest'); 139 | 140 | // https://github.com/microsoft/vscode/issues/117154 141 | testExpandWithCompletion('html', 'hgroup', '
    ${0}
    '); 142 | 143 | // https://github.com/microsoft/vscode/issues/117648 144 | testExpandWithCompletion('css', 'gtc', 'grid-template-columns: repeat(${0});'); 145 | testExpandWithCompletion('sass', 'gtc', 'grid-template-columns: repeat(${0})'); 146 | 147 | // https://github.com/microsoft/vscode/issues/118363 148 | testCountCompletions('jsx', '{test}', 0); 149 | testCountCompletions('jsx', '{test}*2', 0); 150 | // this case shouldn't come up in everyday coding, but including it here for reference 151 | testExpandWithCompletion('jsx', 'import{test}*2', 'test\ntest'); 152 | 153 | // https://github.com/microsoft/vscode/issues/119088 154 | testExpand('html', 'span*3', '', { "output.inlineBreak": 0 }); 155 | testExpand('html', 'span*3', '\n\n', { "output.inlineBreak": 1 }); 156 | 157 | // https://github.com/microsoft/vscode/issues/119937 158 | testExpandWithCompletion('html', 'div[a. b.]', '
    ${0}
    ', { "preferences": { "profile.allowCompactBoolean": true }}); 159 | // testExpandWithCompletion('jsx', 'div[a. b.]', '
    ${0}
    ', { "preferences": { "profile.allowCompactBoolean": true }}); 160 | testExpandWithCompletion('html', 'div[a. b.]', '
    ${0}
    ', { "preferences": { "profile.allowCompactBoolean": false }}); 161 | testExpandWithCompletion('jsx', 'div[a. b.]', '
    ${0}
    ', { "preferences": { "profile.allowCompactBoolean": false }}); 162 | 163 | // https://github.com/microsoft/vscode/issues/120356 164 | testExpandWithCompletion('jsx', 'MyComponent/', ''); 165 | testExpandWithCompletion('html', 'MyComponent/', ''); 166 | 167 | // https://github.com/microsoft/vscode/issues/120417 168 | testExpandWithCompletion('html', 'input', '', { "preferences": { "output.selfClosingStyle": "xhtml" }}); 169 | 170 | // https://github.com/microsoft/vscode/issues/124247 171 | testExpandWithCompletion('html', 'detai', '
    ${0}
    '); 172 | testExpandWithCompletion('html', 'summar', '${0}'); 173 | 174 | // https://github.com/microsoft/vscode/issues/126780 175 | // testExpandWithCompletion('html', 'a[href=#]>p>a[href=#]', '\n\t

    \n'); 176 | 177 | // https://github.com/microsoft/vscode/issues/127919 178 | // testExpandWithCompletion('html', 'div{{{test}}}', '
    {{test}}
    '); 179 | 180 | // https://github.com/microsoft/vscode/issues/131966 181 | testExpandWithCompletion('html', 'span[onclick="alert();"]', '${0}'); 182 | testExpandWithCompletion('html', 'span[onclick="hi(1)(2);"]', '${0}'); 183 | testExpandWithCompletion('html', 'span[onclick="hi;"]>(span)*2', '${1}${0}'); 184 | testExpandWithCompletion('html', '(span[onclick="hi;"]>span)*2', '${1}${0}'); 185 | 186 | // https://github.com/microsoft/vscode/issues/165933 187 | it(`should not mention X-UA-Compatible`, async () => { 188 | const type = getSyntaxType('html'); 189 | const config: UserConfig = { 190 | type, 191 | syntax: 'html' 192 | } 193 | let expandedRes = expandAbbreviation('!', config); 194 | assert.ok(!expandedRes.includes('X-UA-Compatible')); 195 | expandedRes = expandAbbreviation('html:5', config); 196 | assert.ok(!expandedRes.includes('X-UA-Compatible')); 197 | }); 198 | 199 | // https://github.com/microsoft/vscode/issues/180689 200 | testExpandWithCompletion('jsx', '.bar', '
    ${0}
    ', { 'syntaxProfiles': { 'jsx': { 'jsx.enabled': true }}}); 201 | testExpandWithCompletion('jsx', '..bar', '
    ${0}
    ', { 'syntaxProfiles': { 'jsx': { 'jsx.enabled': true }}}); 202 | testExpandWithCompletion( 203 | 'vue', 204 | '..bar', 205 | '
    ${0}
    ', 206 | { 207 | syntaxProfiles: { 208 | vue: { 209 | 'markup.valuePrefix': { 210 | "class*": "$style" 211 | } 212 | } 213 | } 214 | } 215 | ); 216 | // https://github.com/microsoft/vscode/issues/137240 217 | // testExpandWithCompletion('css', 'dn!important', 'display: none !important;'); 218 | 219 | // https://github.com/microsoft/vscode-emmet-helper/issues/37 220 | testExpandWithCompletion('xsl', 'cp/', ''); 221 | 222 | // https://github.com/microsoft/vscode-emmet-helper/issues/58 223 | testExpandWithCompletion('css', '!', '!important'); 224 | testExpandWithCompletion('css', '!imp', '!important'); 225 | testCountCompletions('css', '!importante', 0); 226 | 227 | 228 | testExpandWithCompletion('html', 'vid', ''); 229 | testExpandWithCompletion('html', 'dlg', '${0}'); 230 | testExpandWithCompletion('html', 'datal', '${0}'); 231 | testExpandWithCompletion('html', 'prog', '${0}'); 232 | 233 | // escaped dollar signs should not change after going through Emmet expansion only 234 | // VS Code automatically removes the backslashes after the expansion 235 | testExpand('html', 'span{\\$5}', '\\$5'); 236 | testExpand('html', 'span{\\$hello}', '\\$hello'); 237 | testExpand('html', 'ul>li.item$*2{test\\$}', '
      \n\t
    • test\\$
    • \n\t
    • test\\$
    • \n
    '); 238 | 239 | // `output.reverseAttributes` emmet option 240 | testExpand('html', 'a.dropdown-item[href=#]{foo}', 'foo', { "output.reverseAttributes": false }); 241 | testExpand('html', 'a.dropdown-item[href=#]{foo}', 'foo', { "output.reverseAttributes": true }); 242 | }); 243 | 244 | describe('Wrap Abbreviations (basic)', () => { 245 | // basic cases 246 | testWrap('ul>li', 'test', '
      \n\t
    • test
    • \n
    '); 247 | testWrap('ul>li', ['test'], '
      \n\t
    • test
    • \n
    '); 248 | testWrap('ul>li', ['test1', 'test2'], '
      \n\t
    • \n\t\ttest1\n\t\ttest2\n\t
    • \n
    '); 249 | 250 | // dollar signs should be escaped when wrapped (specific to VS Code) 251 | testWrap('ul>li*', ['test$', 'test$'], '
      \n\t
    • test\\$
    • \n\t
    • test\\$
    • \n
    '); 252 | testWrap('ul>li*', ['$1', '$2'], '
      \n\t
    • \\$1
    • \n\t
    • \\$2
    • \n
    '); 253 | testWrap('ul>li.item$*', ['test$', 'test$'], '
      \n\t
    • test\\$
    • \n\t
    • test\\$
    • \n
    '); 254 | 255 | // https://github.com/emmetio/expand-abbreviation/issues/17 256 | testWrap('ul', '
  • test1
  • \n
  • test2
  • ', '
      \n\t
    • test1
    • \n\t
    • test2
    • \n
    '); 257 | }); 258 | 259 | describe('Wrap Abbreviations (with internal nodes)', () => { 260 | // wrapping elements where the internals contain nodes should result in proper indentation 261 | testWrap('ul', '
  • test
  • ', '
      \n\t
    • test
    • \n
    '); 262 | testWrap('ul', ['
  • test1
  • ', '
  • test2
  • '], '
      \n\t
    • test1
    • \n\t
    • test2
    • \n
    '); 263 | testWrap('ul>li', 'test', '
      \n\t
    • test
    • \n
    '); 264 | testWrap('ul>li', '

    test

    ', '
      \n\t
    • \n\t\t

      test

      \n\t
    • \n
    '); 265 | testWrap('ul>li>div', '

    test

    ', '
      \n\t
    • \n\t\t
      \n\t\t\t

      test

      \n\t\t
      \n\t
    • \n
    '); 266 | testWrap('ul*', ['
  • test1
  • ', '
  • test2
  • '], '
      \n\t
    • test1
    • \n
    \n
      \n\t
    • test2
    • \n
    '); 267 | testWrap('div', 'teststring', '
    teststring
    '); 268 | testWrap('div', 'test\nstring', '
    \n\ttest\n\tstring\n
    '); 269 | }); 270 | 271 | describe('Wrap Abbreviations (more advanced)', () => { 272 | // https://github.com/microsoft/vscode/issues/45724 273 | testWrap('ul>li{hello}', 'Hello world', '
      \n\t
    • helloHello world
    • \n
    '); 274 | testWrap('ul>li{hello}+li.bye', 'Hello world', '
      \n\t
    • hello
    • \n\t
    • Hello world
    • \n
    '); 275 | 276 | // https://github.com/microsoft/vscode/issues/65469 277 | testWrap('p*', ['first line', '', 'second line'], '

    first line

    \n

    second line

    '); 278 | testWrap('p', ['first line', '', 'second line'], '

    \n\tfirst line\n\t\n\tsecond line\n

    '); 279 | 280 | // https://github.com/microsoft/vscode/issues/78015 281 | testWrap('ul>li*', ['one', 'two'], '
    • one
    • two
    ', { "output.format": false }); 282 | 283 | // issue where wrapping with link was causing text nodes to repeat twice 284 | testExpand('html', 'a[href="https://example.com"]>div>b{test here}', '\n\t
    test here
    \n
    '); 285 | testExpand('html', 'a[href="https://example.com"]>div>p{test here}', '\n\t
    \n\t\t

    test here

    \n\t
    \n
    '); 286 | testWrap('a[href="https://example.com"]>div', 'test here', '\n\t
    test here
    \n
    '); 287 | testWrap('a[href="https://example.com"]>div', '

    test here

    ', '\n\t
    \n\t\t

    test here

    \n\t
    \n
    '); 288 | 289 | // these are technically supposed to collapse into a single line, but 290 | // as per 78015 we'll assert that this is the proper behaviour 291 | testWrap('h1', '
    test
    ', '

    \n\t
    test
    \n

    ', { 'output.format': false }); 292 | testWrap('h1', '
    \n\ttest\n
    ', '

    \n\t
    \n\t\ttest\n\t
    \n

    ', { 'output.format': false }); 293 | 294 | // https://github.com/microsoft/vscode/issues/54711 295 | // https://github.com/microsoft/vscode/issues/107592 296 | testWrap('a', 'www.google.it', 'www.google.it'); 297 | testWrap('a', 'http://example.com', 'http://example.com'); 298 | testWrap('a.link[test=here]', 'http://example.com', 'http://example.com'); 299 | testWrap('a', 'http://www.site.com/en-us/download/details.aspx?id=12345', 'http://www.site.com/en-us/download/details.aspx?id=12345'); 300 | testWrap('a[href=]', 'test@example.com', 'test@example.com'); 301 | 302 | // stranger cases involving elements within the a 303 | testWrap('a[href=http://example.com]>div', 'test', 304 | '\n\t
    test
    \n
    '); 305 | testWrap('a[href=http://example.com]>div', 'test', 306 | '\n\t
    test
    \n
    '); 307 | testWrap('a[href=http://example.com]>div', '

    test

    ', 308 | '\n\t
    \n\t\t

    test

    \n\t
    \n
    '); 309 | testWrap('a[href=http://example.com]>div', '
      \n\t
    • Hello world
    • \n
    ', 310 | '\n\t
    \n\t\t
      \n\t\t\t
    • Hello world
    • \n\t\t
    \n\t
    \n
    '); 311 | 312 | // https://github.com/microsoft/vscode/issues/122231 313 | testWrap('div', '\'\'', 314 | '
    \'\'
    ', undefined, 'jsx'); 315 | 316 | // https://github.com/microsoft/vscode/issues/179422 317 | testCountCompletions('html', '{% if value is prime %}', 0); 318 | testCountCompletions('html', '{# comment #}', 0); 319 | testCountCompletions('html', '{{ value }}', 0); 320 | 321 | // https://github.com/microsoft/vscode/issues/179422#issuecomment-1504099693 322 | testCountCompletions('html', '..', 0); 323 | testExpandWithCompletion('html', '.', '
    ${0}
    '); 324 | 325 | }); 326 | -------------------------------------------------------------------------------- /src/test/fileService.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { describe, it } from 'mocha'; 3 | import { isAbsolutePath } from '../fileService'; 4 | 5 | describe('Check if a path is an absolute path', () => { 6 | function testIsAbsolutePath(path: string, expected: boolean) { 7 | it(`should ensure ${path} is${expected ? ' ' : ' not '}an absolute path`, async () => { 8 | assert.strictEqual(isAbsolutePath(path), expected); 9 | }) 10 | } 11 | 12 | testIsAbsolutePath('/home/test', true); 13 | testIsAbsolutePath('C:/home/test', true); 14 | testIsAbsolutePath('c:/home/test', true); 15 | testIsAbsolutePath('/c:/home/test', true); 16 | testIsAbsolutePath('C:\\home\\test', true); 17 | testIsAbsolutePath('\\\\home\\test', true); 18 | testIsAbsolutePath('//home\\test', true); 19 | testIsAbsolutePath('~/home/test', false); 20 | testIsAbsolutePath('./home/test', false); 21 | testIsAbsolutePath('../home/test', false); 22 | testIsAbsolutePath('home/test', false); 23 | }) 24 | -------------------------------------------------------------------------------- /src/typings/thenable.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | interface Thenable extends PromiseLike {} -------------------------------------------------------------------------------- /testData/custom-snippets-invalid-json/snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "scss": { 3 | "snippets": { 4 | "ch": "color:hsl(${1:0}, ${2:100}%, ${3:50}%);" 5 | } 6 | } 7 | 8 | "xml": { 9 | "snippets": { 10 | "hey": "ul>li*2>span.hello" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testData/custom-snippets-profile/snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "css": { 3 | "snippets": { 4 | "ch": "color:hsl(${1:0}, ${2:100}%, ${3:50}%);", 5 | "hello": "margin: 10px", 6 | "mrgstart": "margin-start: 10px" 7 | } 8 | }, 9 | "variables": { 10 | "lang": "fr" 11 | }, 12 | "html": { 13 | "snippets": { 14 | "hey": "ul>li*2>span.hello" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testData/custom-snippets-profile/syntaxProfiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "html": { 3 | "tag_case": "upper" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /testData/custom-snippets-without-inheritance/snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "css": { 3 | "snippets": { 4 | "hello": "margin: 100px;" 5 | } 6 | }, 7 | "scss": { 8 | "snippets": { 9 | "ch": "color:hsl(${1:0}, ${2:100}%, ${3:50}%);" 10 | } 11 | }, 12 | "xml": { 13 | "snippets": { 14 | //This comment should not throw an error while parsing. 15 | "hey": "ul>li*2>span.hello" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /thirdpartynotices.txt: -------------------------------------------------------------------------------- 1 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 2 | 3 | vscode-emmet-helper incorporates third party material from the projects listed below. The original copyright 4 | notice and the license under which Microsoft received such third party material are set forth below. Microsoft 5 | reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. 6 | 7 | 1. expand-abbreivation (https://github.com/emmetio/expand-abbreivation) 8 | 9 | MIT License 10 | 11 | Copyright (c) 2017 Emmet.io 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | 31 | 2. extract-abbreivation (https://github.com/emmetio/extract-abbreivation) 32 | 33 | MIT License 34 | 35 | Copyright (c) 2017 Emmet.io 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy 38 | of this software and associated documentation files (the "Software"), to deal 39 | in the Software without restriction, including without limitation the rights 40 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 41 | copies of the Software, and to permit persons to whom the Software is 42 | furnished to do so, subject to the following conditions: 43 | 44 | The above copyright notice and this permission notice shall be included in all 45 | copies or substantial portions of the Software. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 53 | SOFTWARE. -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es2016"], 5 | "module": "es6", 6 | "moduleResolution": "node", 7 | "outDir": "./lib/esm", 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "declaration": true, 11 | "esModuleInterop": true 12 | }, 13 | "exclude": ["node_modules", "lib"] 14 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es2016"], 5 | "module": "commonjs", 6 | "outDir": "./lib/cjs", 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "strict": true 12 | }, 13 | "exclude": ["node_modules", "lib"] 14 | } 15 | --------------------------------------------------------------------------------