├── .github └── workflows │ └── main.yml ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images ├── bloop.png ├── book.jpg ├── logo.png ├── stepsize.png ├── tabnine.png └── usage.gif ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── extension-test │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── basic │ │ │ ├── autoRenameTag.test.ts │ │ │ ├── basic-workspace │ │ │ │ └── .vscode │ │ │ │ │ └── settings.json │ │ │ └── suite.ts │ │ ├── extensionTestMain.ts │ │ └── test-utils.ts │ └── tsconfig.json ├── extension │ ├── package-lock.json │ ├── package.json │ ├── playground │ │ └── .vscode │ │ │ └── settings.json │ ├── src │ │ ├── createLanguageClientProxy.ts │ │ └── extensionMain.ts │ └── tsconfig.json ├── server │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── autoRenameTag.ts │ │ ├── errorHandlingAndLogging.ts │ │ └── serverMain.ts │ └── tsconfig.json └── service │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── benchmark │ │ ├── doAutoRenameTagBenchmark.ts │ │ └── file.txt │ ├── doAutoRenameTag.ts │ ├── getMatchingTagPairs.ts │ ├── htmlScanner │ │ ├── MultiLineStream.ts │ │ └── htmlScannerFast.ts │ ├── isSelfClosingTag.ts │ ├── serviceMain.ts │ └── util │ │ ├── getIndent.ts │ │ ├── getNextClosingTagName.ts │ │ └── getPreviousOpenTagName.ts │ └── tsconfig.json ├── scripts └── package.js ├── tsconfig-base.json ├── tsconfig-noncomposite-base.json ├── tsconfig.json └── webpack ├── client.webpack.config.js ├── server.webpack.config.js └── shared.webpack.config.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [macos-latest, ubuntu-latest, windows-latest] 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | - name: Install Node.js 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: 16.x 26 | - run: npm install 27 | - run: xvfb-run -a npm run e2e 28 | if: runner.os == 'Linux' 29 | - run: npm run e2e 30 | if: runner.os != 'Linux' 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .vscode-test 9 | out 10 | stats 11 | packages/extension/playground/* 12 | !packages/extension/playground/.vscode 13 | !packages/extension/playground/.vscode/* 14 | 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 13 5 | 6 | os: 7 | - osx 8 | - linux 9 | 10 | install: 11 | - | 12 | npm i 13 | - | 14 | if [ $TRAVIS_OS_NAME == "linux" ]; then 15 | export DISPLAY=':99.0' 16 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 17 | fi 18 | 19 | script: 20 | - npm run e2e 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceFolder}/packages/extension", 12 | "--disable-extensions", 13 | "${workspaceFolder}/packages/extension/playground" 14 | ], 15 | "outFiles": ["${workspaceFolder}/packages/extension/dist/**/*.js"], 16 | "preLaunchTask": "npm: dev" 17 | }, 18 | { 19 | "name": "Launch Extension (with Extensions)", 20 | "type": "extensionHost", 21 | "request": "launch", 22 | "runtimeExecutable": "${execPath}", 23 | "args": [ 24 | "--extensionDevelopmentPath=${workspaceFolder}/packages/extension", 25 | "${workspaceFolder}/packages/extension/playground" 26 | ], 27 | "outFiles": ["${workspaceFolder}/packages/extension/dist/**/*.js"], 28 | "preLaunchTask": "npm: dev" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/dist/**": true // set this to false to include "dist" folders in search results 5 | }, 6 | "cSpell.words": [ 7 | "Säätze", 8 | "Tagg", 9 | "Tanmay", 10 | "Vieww", 11 | "Viewww", 12 | "Viewwww", 13 | "blabla", 14 | "bodyy", 15 | "buttonn", 16 | "divx", 17 | "headd", 18 | "plaîît", 19 | "spann", 20 | "studentt", 21 | "tton", 22 | "vous", 23 | "xmll", 24 | "ΚΑΛΗ", 25 | "ΚΑΛΗΣΠΕΡΑ", 26 | "ΚΑΛΗΣΣΠΕΡΑ", 27 | "ΣΠΕΡΑ" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "dev", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "silent" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.1.10 (2022-02-08) 2 | * Update dependencies 3 | * Fix Windows CO 4 | * Add sponsor for Duckly 5 | 6 | ### 0.1.9 (2021-10-12) 7 | * Fix potential security vulnerabilities 8 | 9 | ### 0.1.8 (2021-07-31) 10 | * Fix potential security vulnerabilities 11 | 12 | ### 0.1.7 (2021-07-09) 13 | * Support new vscode apis 14 | * Fix bugs 15 | 16 | ### 0.1.6 (2021-01-06) 17 | * Also check linkedEditing setting 18 | * Fix #562 19 | 20 | ### 0.1.5 (2020-10-11) 21 | * [#556](https://github.com/formulahendry/vscode-auto-rename-tag/pull/556): Improve auto renaming with React 22 | 23 | ### 0.1.4 (2020-07-03) 24 | * [#541](https://github.com/formulahendry/vscode-auto-rename-tag/pull/541): fix tag starting with $ 25 | * [#544](https://github.com/formulahendry/vscode-auto-rename-tag/pull/544): fix script and style rename 26 | 27 | ### 0.1.3 (2020-05-16) 28 | * Skip HTML and Handlebars files when the setting `editor.renameOnType` is enabled 29 | 30 | ### 0.1.2 (2020-04-21) 31 | * Kudos to [@SimonSiefke](https://github.com/SimonSiefke) for [rewriting the logic of this extension](https://github.com/formulahendry/vscode-auto-rename-tag/pull/511) 32 | 33 | ### 0.1.1 (2019-10-27) 34 | * Add notice about performance issue 35 | 36 | ### 0.1.0 (2019-06-02) 37 | * [#105](https://github.com/formulahendry/vscode-auto-rename-tag/pull/105): Bug fixes 38 | 39 | ### 0.0.15 (2017-11-04) 40 | * Add support for Multi Root Workspace 41 | 42 | ### 0.0.14 (2017-06-19) 43 | * Fix [#30](https://github.com/formulahendry/vscode-auto-rename-tag/issues/30) 44 | 45 | ### 0.0.13 (2017-06-18) 46 | * Fix [#24](https://github.com/formulahendry/vscode-auto-rename-tag/issues/24) and [#29](https://github.com/formulahendry/vscode-auto-rename-tag/issues/29) 47 | 48 | ### 0.0.12 (2017-05-21) 49 | * Fix [#15](https://github.com/formulahendry/vscode-auto-rename-tag/issues/15) and [#21](https://github.com/formulahendry/vscode-auto-rename-tag/issues/21): Undo and redo are broken 50 | 51 | ### 0.0.11 52 | * Fix [GitHub issue#12](https://github.com/formulahendry/vscode-auto-rename-tag/issues/12): Not work when using OPTION+DELETE (or CTRL+BACKSPACE) 53 | 54 | ### 0.0.10 55 | * Fix [GitHub issue#17](https://github.com/formulahendry/vscode-auto-rename-tag/issues/17): Unexpected renaming when moving row with "alt+down" 56 | 57 | ### 0.0.9 58 | * Fix [GitHub issue#14](https://github.com/formulahendry/vscode-auto-rename-tag/issues/14): Avoid renaming tag when moving rows with "alt+up/down" 59 | 60 | ### 0.0.8 61 | * Fix [GitHub issue#11](https://github.com/formulahendry/vscode-auto-rename-tag/issues/11) 62 | 63 | ### 0.0.7 64 | * Fix [GitHub issue#8](https://github.com/formulahendry/vscode-auto-rename-tag/issues/8) 65 | 66 | ### 0.0.6 67 | * Merge [PR#7](https://github.com/formulahendry/vscode-auto-rename-tag/pull/7): Remove console.log 68 | * Update README.md to clarify the configuration for `auto-rename-tag.activationOnLanguage` 69 | 70 | ### 0.0.5 71 | * Fix [GitHub issue#6](https://github.com/formulahendry/vscode-auto-rename-tag/issues/6) 72 | 73 | ### 0.0.4 74 | * Add support for tag name that contains ```- _ : .``` 75 | 76 | ### 0.0.3 77 | * Fix paired tags not updated when there are void elements or self-closing tags between paired tags 78 | * Fix [GitHub issue#2](https://github.com/formulahendry/vscode-auto-rename-tag/issues/2) 79 | * Fix [GitHub issue#3](https://github.com/formulahendry/vscode-auto-rename-tag/issues/3) 80 | * Parse document independently instead of using SAXParser of parse5 npm package to avoid uncontrollable parse behavior 81 | 82 | ### 0.0.2 83 | * Update logo 84 | 85 | ### 0.0.1 86 | * Initial Release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jun Han 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 | # Auto Rename Tag 2 | 3 | [![Marketplace Version](https://vsmarketplacebadge.apphb.com/version/formulahendry.auto-rename-tag.svg)](https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag) [![Installs](https://vsmarketplacebadge.apphb.com/installs/formulahendry.auto-rename-tag.svg)](https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag) [![Rating](https://vsmarketplacebadge.apphb.com/rating/formulahendry.auto-rename-tag.svg)](https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag) [![Build Status](https://travis-ci.org/formulahendry/vscode-auto-rename-tag.svg?branch=master)](https://travis-ci.org/formulahendry/vscode-auto-rename-tag) 4 | 5 | Automatically rename paired HTML/XML tag, same as Visual Studio IDE does. 6 | 7 | ## Sponsors 8 | 9 | [![Tabnine](images/tabnine.png)](https://bit.ly/2LZsrQ9)
10 | Increase your coding productivity with Tabnine’s AI code completions! Tabnine is a free powerful Artificial Intelligence assistant designed to help you code faster, reduce mistakes, and discover best coding practices - without ever leaving the comfort of VS Code.
11 | Tabnine is trusted by more than a million developers worldwide. [Get it now](https://bit.ly/2LZsrQ9). 12 | 13 |


14 | Eliminate context switching and costly distractions. Create and merge PRs and perform code reviews from inside your IDE while using jump-to-definition, your keybindings, and other IDE favorites.
Learn more

15 | 16 | [![Stepsize](images/stepsize.png)](https://bit.ly/3wmQ72y)
17 | Track and prioritise tech debt and maintenance issues, straight from your IDE. Bookmark code while you work, organise TODOs and share codebase knowledge with your team. [Try it out for free today](https://bit.ly/3wmQ72y). 18 | 19 | [![Bloop](images/bloop.png)](https://bloop.ai/?utm_source=vscmarket&utm_campaign=formulahendry&utm_medium=banner)
20 | Bored of trawling through the docs? Get JS and TS code examples from documentation and Open Source right in your IDE. [Learn more](https://bloop.ai/?utm_source=vscmarket&utm_campaign=formulahendry&utm_medium=banner). 21 | 22 | [![Duckly](https://storage.googleapis.com/gitduck/img/duckly-sponsor-vsc-opt.png)](https://bit.ly/3gvmQ00)
23 | Easy pair programming with any IDE. Duckly enables you to talk, share your code in real-time, server and terminal with people using different IDEs. [Try it out for free](https://bit.ly/3gvmQ00). 24 | 25 | ## Book for VS Code 26 | 27 | [《Visual Studio Code 权威指南》](https://union-click.jd.com/jdc?e=jdext-1261348777639735296-0&p=AyIGZRhbHQsWAVIaXxEyEgRdG1sRBxU3EUQDS10iXhBeGlcJDBkNXg9JHUlSSkkFSRwSBF0bWxEHFRgMXgdIMkRxFAUJD1RQZT0cBnwKDE4%2BaDpgB2ILWStbHAIQD1QaWxIBIgdUGlsRBxEEUxprJQIXNwd1g6O0yqLkB4%2B%2FjcePwitaJQIWD1cfWhwKGwVSG1wlAhoDZc31gdeauIyr%2FsOovNLYq46cqca50ytrJQEiXABPElAeEgRSG1kQCxQBUxxZHQQQA1YTXAkDIgdUGlscChECXRs1FGwSD1UbWRALFwRWK1slASJZOxoLRlUXU1NONU9QEkdXWRlJbBUDVB9TFgAVN1caWhcA):带你深入浅出 VS Code! 28 | 29 | ![Book](images/book.jpg) 30 | 31 | ## ❤️ Contributors 32 | 33 | Kudos to [@SimonSiefke](https://github.com/SimonSiefke) for rewriting the logic of this extension! 34 | 35 | ## Features 36 | 37 | - When you rename one HTML/XML tag, automatically rename the paired HTML/XML tag 38 | 39 | ## Usages 40 | 41 | ![Usage](images/usage.gif) 42 | 43 | ## Configuration 44 | 45 | Add entry into `auto-rename-tag.activationOnLanguage` to set the languages that the extension will be activated. 46 | By default, it is `["*"]` and will be activated for all languages. 47 | 48 | ```json 49 | { 50 | "auto-rename-tag.activationOnLanguage": ["html", "xml", "php", "javascript"] 51 | } 52 | ``` 53 | 54 | The setting should be set with language id defined in [VS Code](https://github.com/Microsoft/vscode/tree/master/extensions). Taking [javascript definition](https://github.com/Microsoft/vscode/blob/master/extensions/javascript/package.json) as an example, we need to use `javascript` for `.js` and `.es6`, use `javascriptreact` for `.jsx`. So, if you want to enable this extension on `.js` file, you need to add `javascript` in settings.json. 55 | 56 | ## Note 57 | 58 | From 1.44, VS Code offers the built-in [auto update tags](https://code.visualstudio.com/docs/languages/html#_auto-update-tags) support for HTML and Handlebars that can be enabled with the setting `editor.linkedEditing`. If this setting is enabled, this extension will skip HTML and Handlebars files regardless of the languages listed in `auto-rename-tag.activationOnLanguage` 59 | ## Change Log 60 | 61 | See Change Log [here](CHANGELOG.md) 62 | 63 | ## Issues 64 | 65 | Submit the [issues](https://github.com/formulahendry/vscode-auto-rename-tag/issues) if you find any bug or have any suggestion. 66 | 67 | ## Contribution 68 | 69 | Fork the [repo](https://github.com/formulahendry/vscode-auto-rename-tag) and submit pull requests. 70 | 71 | 76 | -------------------------------------------------------------------------------- /images/bloop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formulahendry/vscode-auto-rename-tag/f3039ed7263c5ab94c6e2fa9995d3ad265ebc822/images/bloop.png -------------------------------------------------------------------------------- /images/book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formulahendry/vscode-auto-rename-tag/f3039ed7263c5ab94c6e2fa9995d3ad265ebc822/images/book.jpg -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formulahendry/vscode-auto-rename-tag/f3039ed7263c5ab94c6e2fa9995d3ad265ebc822/images/logo.png -------------------------------------------------------------------------------- /images/stepsize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formulahendry/vscode-auto-rename-tag/f3039ed7263c5ab94c6e2fa9995d3ad265ebc822/images/stepsize.png -------------------------------------------------------------------------------- /images/tabnine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formulahendry/vscode-auto-rename-tag/f3039ed7263c5ab94c6e2fa9995d3ad265ebc822/images/tabnine.png -------------------------------------------------------------------------------- /images/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formulahendry/vscode-auto-rename-tag/f3039ed7263c5ab94c6e2fa9995d3ad265ebc822/images/usage.gif -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.4.0", 3 | "packages": ["packages/*"], 4 | "version": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "postinstall": "lerna bootstrap && tsc -b", 4 | "dev": "tsc -b -w", 5 | "e2e": "cd packages/extension-test && npm run e2e", 6 | "clean": "rimraf dist", 7 | "bundle:client": "webpack --mode production --config ./webpack/client.webpack.config.js", 8 | "bundle:server": "webpack --mode production --config ./webpack/server.webpack.config.js", 9 | "package": "npm run clean && npm run bundle:client && npm run bundle:server && node scripts/package.js && cd dist && vsce package", 10 | "publish": "npm run clean && npm run bundle:client && npm run bundle:server && node scripts/package.js && cd dist && vsce publish", 11 | "prettier": "prettier --write \"packages/*/src/**/*.ts\"", 12 | "benchmark": "nodemon --watch packages/**/*.js --exec \"node packages/service/dist/benchmark/doAutoRenameTagBenchmark.js\"" 13 | }, 14 | "devDependencies": { 15 | "fs-extra": "^10.0.1", 16 | "lerna": "^4.0.0", 17 | "merge-options": "^3.0.4", 18 | "prettier": "^2.6.1", 19 | "rimraf": "^3.0.2", 20 | "ts-loader": "^9.2.8", 21 | "typescript": "^4.6.3", 22 | "webpack": "^5.70.0", 23 | "webpack-bundle-analyzer": "^4.5.0", 24 | "webpack-cli": "^4.9.2" 25 | }, 26 | "prettier": { 27 | "semi": true, 28 | "singleQuote": true, 29 | "trailingComma": "none", 30 | "arrowParens": "avoid" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/extension-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension-test", 3 | "version": "1.0.0-dev", 4 | "scripts": { 5 | "dev": "tsc -b -w", 6 | "build": "tsc -b", 7 | "e2e": "rimraf dist tsconfig.tsbuildinfo && npm run build && node dist/extensionTestMain" 8 | }, 9 | "devDependencies": { 10 | "@types/fs-extra": "^9.0.13", 11 | "@types/mocha": "^9.1.0", 12 | "@types/node": "^17.0.23", 13 | "@types/vscode": "^1.65.0", 14 | "@vscode/test-electron": "^2.1.3", 15 | "fs-extra": "^10.0.1", 16 | "mocha": "^9.2.2", 17 | "rimraf": "^3.0.2", 18 | "typescript": "^4.6.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/extension-test/src/basic/autoRenameTag.test.ts: -------------------------------------------------------------------------------- 1 | import { before, test } from 'mocha'; 2 | import { 3 | activateExtension, 4 | createTestFile, 5 | run, 6 | TestCase, 7 | slowTimeout, 8 | slowSpeed 9 | } from '../test-utils'; 10 | 11 | suite('Auto Rename Tag', () => { 12 | before(async () => { 13 | await createTestFile('auto-rename-tag.html'); 14 | await activateExtension(); 15 | }); 16 | 17 | test('Cursor is at the back of a start tag', async () => { 18 | const testCases: TestCase[] = [ 19 | { 20 | input: 'test', 21 | type: 's', 22 | expect: 'test' 23 | }, 24 | { 25 | input: 'test', 26 | type: '{backspace}', 27 | expect: 'test' 28 | }, 29 | { 30 | input: 'test', 31 | type: '{backspace}{backspace}{backspace}', 32 | expect: '<>test' 33 | }, 34 | { 35 | input: 'test', 36 | type: ' ', 37 | expect: '
test
' 38 | }, 39 | { 40 | input: 'test', 41 | type: ' c', 42 | expect: '
test
' 43 | }, 44 | { 45 | input: 'test', 46 | type: '{backspace}{backspace}{backspace} ', 47 | expect: '< >test' 48 | }, 49 | { 50 | input: 'test', 51 | type: 'v{undo}', 52 | expect: '
test
' 53 | }, 54 | { 55 | input: 'test', 56 | type: 'v{undo}{redo}', 57 | expect: 'test' 58 | } 59 | ]; 60 | await run(testCases, { 61 | speed: slowSpeed, 62 | timeout: slowTimeout 63 | }); 64 | }); 65 | 66 | test('Cursor at the front of a start tag', async () => { 67 | const testCases: TestCase[] = [ 68 | { 69 | input: '<|div>test', 70 | type: 's', 71 | expect: 'test' 72 | } 73 | ]; 74 | await run(testCases, { 75 | timeout: slowTimeout 76 | }); 77 | }); 78 | 79 | test('tag with class', async () => { 80 | const testCases: TestCase[] = [ 81 | { 82 | input: 'test', 83 | type: 'v', 84 | expect: 'test' 85 | }, 86 | { 87 | input: 'test', 88 | type: '{backspace}{backspace}{backspace}', 89 | expect: '< class="css">test', 90 | skip: true 91 | }, 92 | { 93 | input: '
test
', 94 | type: '{backspace}v', 95 | expect: 'test' 96 | } 97 | ]; 98 | await run(testCases, { speed: slowSpeed, timeout: slowTimeout }); 99 | }); 100 | 101 | test('multiple lines', async () => { 102 | const testCases: TestCase[] = [ 103 | { 104 | input: '\n test\n', 105 | type: '{backspace}{backspace}{backspace}h3', 106 | expect: '

\n test\n

' 107 | } 108 | ]; 109 | await run(testCases, { 110 | speed: slowSpeed, 111 | timeout: slowTimeout 112 | }); 113 | }); 114 | 115 | test('div and a nested span', async () => { 116 | const testCases: TestCase[] = [ 117 | { 118 | input: '\n test\n', 119 | type: '{backspace}{backspace}{backspace}h3', 120 | expect: '

\n test\n

' 121 | }, 122 | { 123 | input: '
\n test\n
', 124 | type: '{backspace}{backspace}{backspace}{backspace}b', 125 | expect: '
\n test\n
' 126 | }, 127 | { 128 | input: '
\n test\n
', 129 | type: 'n', 130 | expect: '
\n test\n
' 131 | } 132 | ]; 133 | await run(testCases, { speed: slowSpeed, timeout: slowTimeout }); 134 | }); 135 | 136 | test('nested div tags', async () => { 137 | const testCases: TestCase[] = [ 138 | { 139 | input: '\n
test
\n', 140 | type: '{backspace}{backspace}{backspace}h3', 141 | expect: '

\n
test
\n

' 142 | }, 143 | { 144 | input: '
\n
test\n
', 145 | type: '{backspace}{backspace}{backspace}p', 146 | expect: '
\n

test

\n
' 147 | } 148 | ]; 149 | await run(testCases, { speed: slowSpeed, timeout: slowTimeout }); 150 | }); 151 | 152 | test('dashed tag', async () => { 153 | const testCases: TestCase[] = [ 154 | { 155 | input: 'test', 156 | type: '{backspace}{backspace}{backspace}{backspace}-span', 157 | expect: 'test' 158 | } 159 | ]; 160 | await run(testCases, { speed: slowSpeed, timeout: slowTimeout }); 161 | }); 162 | 163 | test('uppercase tag', async () => { 164 | const testCases: TestCase[] = [ 165 | { 166 | input: 'test
', 167 | type: 'S', 168 | expect: 'test' 169 | } 170 | ]; 171 | await run(testCases, { speed: slowSpeed, timeout: slowTimeout }); 172 | }); 173 | 174 | test('with class on second line', async () => { 175 | const testCases: TestCase[] = [ 176 | { 177 | input: 'foobar', 178 | type: '{backspace}', 179 | expect: 'foobar' 180 | } 181 | ]; 182 | await run(testCases, { 183 | timeout: slowTimeout 184 | }); 185 | }); 186 | 187 | test('quotes', async () => { 188 | const testCases: TestCase[] = [ 189 | { 190 | input: `it's

`, 191 | type: '1', 192 | expect: `it's` 193 | }, 194 | { 195 | input: `

it's`, 196 | type: '1', 197 | expect: `it's` 198 | }, 199 | { 200 | input: `quote "

`, 201 | type: '1', 202 | expect: `quote "` 203 | }, 204 | { 205 | input: `

quote "`, 206 | type: '1', 207 | expect: `quote "` 208 | }, 209 | { 210 | input: ` 211 | W3C's 212 |

`, 213 | type: 'p', 214 | expect: ` 215 | W3C's 216 | ` 217 | }, 218 | { 219 | input: `

220 | W3C's 221 | `, 222 | type: 'p', 223 | expect: ` 224 | W3C's 225 | ` 226 | }, 227 | { 228 | input: `(type="image")

`, 229 | type: 'p', 230 | expect: '(type="image")' 231 | }, 232 | { 233 | input: `

(type="image")`, 234 | type: 'p', 235 | expect: '(type="image")' 236 | } 237 | ]; 238 | await run(testCases, { 239 | speed: slowSpeed, 240 | timeout: slowTimeout 241 | }); 242 | }); 243 | 244 | test('weird chars at start tag', async () => { 245 | const testCases: TestCase[] = [ 246 | { 247 | input: '', 248 | type: 'ä', 249 | expect: '' 250 | }, 251 | { 252 | input: '', 253 | type: '|', 254 | expect: '', 255 | skip: true 256 | }, 257 | { 258 | input: '<你|早>', 259 | type: '早', 260 | expect: '<你早早>', 261 | skip: true 262 | }, 263 | { 264 | input: '', 265 | type: 'î', 266 | expect: '' 267 | }, 268 | { 269 | input: '<ΚΑΛΗ|ΣΠΕΡΑ>', 270 | type: 'Σ', 271 | expect: '<ΚΑΛΗΣΣΠΕΡΑ>', 272 | skip: true 273 | }, 274 | { 275 | input: 'foobar', 276 | type: 's', 277 | expect: 'foobar' 278 | }, 279 | { 280 | input: 'foobar', 281 | type: 's', 282 | expect: 'foobar', 283 | skip: true 284 | }, 285 | { 286 | input: 'foobar', 287 | type: '{backspace}', 288 | expect: 'foobar', 289 | skip: true 290 | } 291 | ]; 292 | await run(testCases, { speed: slowSpeed, timeout: slowTimeout }); 293 | }); 294 | 295 | test('with incomplete inner tag', async () => { 296 | const testCases: TestCase[] = [ 297 | { 298 | input: '\n', 299 | type: 'b', 300 | expect: '\n' 301 | } 302 | ]; 303 | await run(testCases); 304 | }); 305 | 306 | test('end tag with inline div tag', async () => { 307 | const testCases: TestCase[] = [ 308 | { 309 | input: '

test', 310 | type: 's', 311 | expect: 'test' 312 | } 313 | ]; 314 | await run(testCases, { 315 | speed: slowSpeed, 316 | timeout: slowTimeout 317 | }); 318 | }); 319 | 320 | test('with comments', async () => { 321 | const testCases: TestCase[] = [ 322 | { 323 | input: '', 324 | type: 'v', 325 | expect: '' 326 | }, 327 | { 328 | input: '
', 334 | type: 'v', 335 | expect: ' ' 336 | }, 337 | { 338 | input: '
', 339 | type: 'v', 340 | expect: ' ' 341 | }, 342 | { 343 | input: '
', 344 | type: 'v', 345 | expect: ' ' 346 | }, 347 | { 348 | input: '
', 349 | type: 'v', 350 | expect: '
' 351 | }, 352 | { 353 | input: '
', 354 | type: 'v', 355 | expect: '
' 356 | } 357 | ]; 358 | await run(testCases, { timeout: slowTimeout, speed: slowSpeed }); 359 | }); 360 | 361 | test('bug 2', async () => { 362 | const testCases: TestCase[] = [ 363 | { 364 | input: ` 365 | 366 | 369 | 370 | 371 | 372 | 373 | `, 374 | type: '\n', 375 | expect: ` 376 | 377 | 380 | 381 | 382 | 383 | 385 | ` 386 | } 387 | ]; 388 | 389 | await run(testCases); 390 | }); 391 | 392 | test('bug 3', async () => { 393 | const testCases: TestCase[] = [ 394 | { 395 | input: `
396 |
397 | `, 398 | type: 'v', 399 | expect: ` 400 |
401 |
` 402 | } 403 | ]; 404 | await run(testCases, { 405 | speed: slowSpeed, 406 | timeout: slowTimeout 407 | }); 408 | }); 409 | 410 | test('bug 4', async () => { 411 | const testCases: TestCase[] = [ 412 | { 413 | input: `
414 |
<| 415 |
`, 416 | type: '/', 417 | expect: `
418 |
` 420 | } 421 | ]; 422 | await run(testCases, { 423 | speed: slowSpeed, 424 | timeout: slowTimeout 425 | }); 426 | }); 427 | 428 | test('bugs with href=/', async () => { 429 | const testCases: TestCase[] = [ 430 | { 431 | input: ` 432 | One-Page Version html.spec.whatwg.org 433 |
`, 434 | type: 'v', 435 | expect: ` 436 | One-Page Version html.spec.whatwg.org 437 | ` 438 | }, 439 | { 440 | input: ` 441 | Multipage Version /multipage 442 |
`, 443 | type: 'v', 444 | expect: ` 445 | Multipage Version /multipage 446 | ` 447 | } 448 | ]; 449 | await run(testCases, { 450 | speed: slowSpeed, 451 | timeout: slowTimeout 452 | }); 453 | }); 454 | 455 | test('type space after bu', async () => { 456 | const testCases: TestCase[] = [ 457 | { 458 | input: ``, 461 | type: ' ', 462 | expect: `` 465 | } 466 | ]; 467 | await run(testCases, { 468 | speed: slowSpeed, 469 | timeout: slowTimeout 470 | }); 471 | }); 472 | 473 | test('language plaintext', async () => { 474 | await createTestFile('auto-rename-tag.language.txt'); 475 | const testCases: TestCase[] = [ 476 | { 477 | input: `this is a button`, 478 | type: '2', 479 | expect: `this is a button` 480 | } 481 | ]; 482 | await run(testCases, { 483 | speed: slowSpeed, 484 | timeout: slowTimeout 485 | }); 486 | }); 487 | 488 | test('language xml', async () => { 489 | await createTestFile('auto-rename-tag.xml'); 490 | const testCases: TestCase[] = [ 491 | { 492 | input: ` 493 | 494 | 495 | Tanmay 496 | A 497 | 498 | `, 499 | type: '2', 500 | expect: ` 501 | 502 | 503 | Tanmay 504 | A 505 | 506 | ` 507 | } 508 | ]; 509 | await run(testCases, { 510 | speed: slowSpeed, 511 | timeout: slowTimeout 512 | }); 513 | }); 514 | 515 | test('multiple cursors', async () => { 516 | const testCases: TestCase[] = [ 517 | { 518 | input: ` 519 | 520 | 521 | 522 | 523 | `, 524 | type: 'i', 525 | expect: ` 526 | 527 | 528 | 529 | 530 | ` 531 | }, 532 | { 533 | input: `

534 |

535 |

536 |

537 |

538 |
`, 539 | type: 'i', 540 | expect: ` 541 | 542 | 543 | 544 | 545 | ` 546 | }, 547 | { 548 | input: `
549 | 550 | 551 | 552 | 553 | `, 554 | type: '{backspace}', 555 | expect: ` 556 | 557 | 558 | 559 | 560 | ` 561 | }, 562 | { 563 | input: ``, 564 | type: 'b', 565 | expect: `` 566 | }, 567 | { 568 | input: ``, 569 | type: 'b', 570 | expect: `` 571 | } 572 | ]; 573 | await run(testCases, { 574 | timeout: slowTimeout, 575 | speed: slowSpeed 576 | }); 577 | }); 578 | 579 | test('self closing tags', async () => { 580 | await createTestFile('self-closing-tags.html'); 581 | const testCases: TestCase[] = [ 582 | { 583 | input: ``, 584 | type: 'd', 585 | expect: '' 586 | }, 587 | { 588 | input: ``, 589 | type: 'd', 590 | expect: '' 591 | } 592 | ]; 593 | await run(testCases, { 594 | speed: slowSpeed, 595 | timeout: slowTimeout * 5 596 | }); 597 | }); 598 | 599 | test('language angular', async () => { 600 | await createTestFile('auto-rename-tag.component.html'); 601 | 602 | const testCases: TestCase[] = [ 603 | { 604 | input: `

Products

605 | 606 | 607 | 608 |

609 | 610 | {{ product.name }} 611 | 612 |

613 | 614 |
`, 615 | type: 'v', 616 | expect: `

Products

617 | 618 | 619 | 620 |

621 | 622 | {{ product.name }} 623 | 624 |

625 | 626 |
` 627 | } 628 | ]; 629 | await run(testCases, { 630 | timeout: slowTimeout, 631 | speed: slowSpeed 632 | }); 633 | }); 634 | 635 | test('language javascriptreact', async () => { 636 | await createTestFile('auto-rename-tag.jsx'); 637 | 638 | const testCases: TestCase[] = [ 639 | { 640 | input: `const Button = () => {}}>click me`, 641 | type: 'n', 642 | expect: `const Button = () => {}}>click me` 643 | }, 644 | { 645 | input: `const Button = () => */};`, 788 | type: 'n', 789 | expect: `const button = {/* */};` 790 | }, 791 | { 792 | input: `const button = ;`, 793 | type: 'n', 794 | expect: `const button = ;` 795 | }, 796 | { 797 | input: `const button = */};`, 798 | type: 'n', 799 | expect: `const button = {/* */};` 800 | }, 801 | { 802 | input: 'const button = {/* ', 803 | type: 'n', 804 | expect: 'const button = {/* ', 808 | type: 'n', 809 | expect: 'const button = ' 810 | }, 811 | { 812 | input: 'const button = 846 | \`\`\``, 847 | type: 'n', 848 | expect: `\`\`\`html 849 | 850 | 851 | \`\`\`` 852 | }, 853 | { 854 | input: `\`\`\`html 855 | 856 | \`\`\` 857 | 858 | \`\`\`html 859 | 860 | \`\`\``, 861 | type: 'n', 862 | expect: `\`\`\`html 863 | 864 | \`\`\` 865 | 866 | \`\`\`html 867 | 868 | \`\`\`` 869 | }, 870 | { 871 | input: `\`\`\`html 872 |
`, 902 | type: 'v', 903 | expect: ` 904 | 905 | 906 | ` 907 | }, 908 | { 909 | input: `"> 910 |
`, 911 | type: 'v', 912 | expect: `"> 913 | ` 914 | }, 915 | { 916 | input: ` 917 | `, 918 | type: 'n', 919 | expect: ` 920 | ` 921 | }, 922 | { 923 | input: ` 924 | `, 925 | type: 'n', 926 | expect: ` 927 | ` 928 | }, 929 | { 930 | input: ` 931 | `, 932 | type: 'n', 933 | expect: ` 934 | `, 935 | skip: true 936 | }, 937 | { 938 | input: ` 939 | `, 940 | type: 'n', 941 | expect: ` 942 | `, 943 | skip: true 944 | }, 945 | { 946 | input: '', 947 | type: 'n', 948 | expect: '' 949 | }, 950 | { 951 | input: '', 952 | type: 'n', 953 | expect: '' 954 | }, 955 | { 956 | input: ` 957 | @foreach ($users as $user) 958 | @if ($loop->first) 959 | This is the first iteration. 960 | @endif 961 | 962 | @if ($loop->last) 963 | This is the last iteration. 964 | @endif 965 | 966 |

This is user {{ $user->id }}

967 | @endforeach 968 |
`, 969 | type: 'v', 970 | expect: ` 971 | @foreach ($users as $user) 972 | @if ($loop->first) 973 | This is the first iteration. 974 | @endif 975 | 976 | @if ($loop->last) 977 | This is the last iteration. 978 | @endif 979 | 980 |

This is user {{ $user->id }}

981 | @endforeach 982 |
` 983 | } 984 | ]; 985 | await run(testCases, { 986 | speed: slowSpeed, 987 | timeout: slowTimeout 988 | }); 989 | }); 990 | 991 | test('language razor', async () => { 992 | await createTestFile('auto-rename-tag.cshtml'); 993 | const testCases: TestCase[] = [ 994 | { 995 | input: `Last week this time: @(DateTime.Now - TimeSpan.FromDays(7))

`, 996 | type: 'p', 997 | expect: `Last week this time: @(DateTime.Now - TimeSpan.FromDays(7))` 998 | } 999 | ]; 1000 | await run(testCases, { 1001 | speed: slowSpeed, 1002 | timeout: slowTimeout 1003 | }); 1004 | }); 1005 | 1006 | test('language svelte', async () => { 1007 | await createTestFile('auto-rename-tag.svelte'); 1008 | const testCases: TestCase[] = [ 1009 | { 1010 | input: ` 1017 | 1018 | 1019 | Count: {count} 1020 | `, 1021 | type: '2', 1022 | expect: ` 1029 | 1030 | 1031 | Count: {count} 1032 | ` 1033 | } 1034 | ]; 1035 | await run(testCases, { 1036 | speed: slowSpeed, 1037 | timeout: slowTimeout 1038 | }); 1039 | }); 1040 | 1041 | test('language svg', async () => { 1042 | await createTestFile('auto-rename-tag.svg'); 1043 | const testCases: TestCase[] = [ 1044 | { 1045 | input: ` 1046 | 1047 | 1048 | 1049 | `, 1050 | type: '2', 1051 | expect: ` 1052 | 1053 | 1054 | 1055 | ` 1056 | } 1057 | ]; 1058 | await run(testCases, { 1059 | speed: slowSpeed, 1060 | timeout: slowTimeout 1061 | }); 1062 | }); 1063 | 1064 | test('language erb', async () => { 1065 | await createTestFile('auto-rename-tag.erb'); 1066 | const testCases: TestCase[] = [ 1067 | { 1068 | input: `|<%= project.logo_tag %> <%= project.name %>`, 1069 | type: '{backspace}{backspace}{backspace}', 1070 | expect: ` <%= project.name %>` 1071 | }, 1072 | { 1073 | input: `<%= project.logo_tag %> <%= project.name %>`, 1074 | type: 'n', 1075 | expect: `<%= project.logo_tag %> <%= project.name %>` 1076 | } 1077 | ]; 1078 | await run(testCases, { 1079 | timeout: slowTimeout 1080 | }); 1081 | }); 1082 | 1083 | test('language typescriptreact', async () => { 1084 | await createTestFile('auto-rename-tag.tsx'); 1085 | const testCases: TestCase[] = [ 1086 | { 1087 | input: `interface Props { 1088 | readonly dispatch: Dispatch<() => void>; 1089 | } 1090 | 1091 | const Link = 1092 | Bla Bla 1093 | `, 1094 | selection: [47, 57], 1095 | type: 'any', 1096 | expect: `interface Props { 1097 | readonly dispatch: Dispatch; 1098 | } 1099 | 1100 | const Link = 1101 | Bla Bla 1102 | ` 1103 | }, 1104 | { 1105 | input: `interface Props { 1106 | readonly dispatch: Dispatch<() => void>; 1107 | } 1108 | 1109 | const Link = 1110 | Bla Bla 1111 | `, 1112 | type: 'a', 1113 | expect: `interface Props { 1114 | readonly dispatch: Dispatch<() => void>; 1115 | } 1116 | 1117 | const Link = 1118 | Bla Bla 1119 | ` 1120 | }, 1121 | { 1122 | input: `interface Props { 1123 | readonly dispatch: Dispatch<() => void>; 1124 | } 1125 | 1126 | const Link = 1127 | Bla Bla 1128 | `, 1129 | type: 'a', 1130 | expect: `interface Props { 1131 | readonly dispatch: Dispatch<() => void>; 1132 | } 1133 | 1134 | const Link = 1135 | Bla Bla 1136 | ` 1137 | } 1138 | ]; 1139 | await run(testCases, { 1140 | timeout: slowTimeout 1141 | }); 1142 | }); 1143 | 1144 | test('language vue', async () => { 1145 | await createTestFile('auto-rename-tag.vue'); 1146 | const testCases: TestCase[] = [ 1147 | { 1148 | input: ` 1153 | 1154 | `, 1162 | type: 'v', 1163 | expect: ` 1168 | 1169 | ` 1177 | } 1178 | ]; 1179 | await run(testCases, { 1180 | speed: slowSpeed, 1181 | timeout: slowTimeout 1182 | }); 1183 | }); 1184 | 1185 | test('language xml', async () => { 1186 | await createTestFile('auto-rename-tag.xml'); 1187 | const testCases: TestCase[] = [ 1188 | { 1189 | input: ` 1190 | 1191 | 1192 | Tanmay 1193 | A 1194 | 1195 | `, 1196 | type: 'l', 1197 | expect: ` 1198 | 1199 | 1200 | Tanmay 1201 | A 1202 | 1203 | ` 1204 | }, 1205 | { 1206 | input: ` 1207 | 1208 | 1209 | Tanmay 1210 | A 1211 | 1212 | `, 1213 | type: 't', 1214 | expect: ` 1215 | 1216 | 1217 | Tanmay 1218 | A 1219 | 1220 | ` 1221 | } 1222 | ]; 1223 | await run(testCases, { 1224 | timeout: slowTimeout 1225 | }); 1226 | }); 1227 | 1228 | test('invalid code', async () => { 1229 | const testCases: TestCase[] = [ 1230 | { 1231 | input: ` 1232 | 1233 |
1234 | `, 1235 | type: 'n', 1236 | expect: ` 1237 | 1238 |
1239 | ` 1240 | }, 1241 | { 1242 | input: `
1245 | `, 1246 | type: 'n', 1247 | expect: `
1250 | ` 1251 | } 1252 | ]; 1253 | await run(testCases, { 1254 | timeout: slowTimeout 1255 | }); 1256 | }); 1257 | 1258 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/502', async () => { 1259 | await createTestFile('502.html'); 1260 | const testCases: TestCase[] = [ 1261 | { 1262 | input: `
1263 |
`, 1264 | type: 'b-row', 1265 | selection: [1, 16], 1266 | expect: ` 1267 | ` 1268 | } 1269 | ]; 1270 | await run(testCases, { 1271 | timeout: slowTimeout 1272 | }); 1273 | }); 1274 | 1275 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/488', async () => { 1276 | await createTestFile('488.html'); 1277 | const testCases: TestCase[] = [ 1278 | { 1279 | input: `
  • 1280 | {nickname} 1281 |

    1285 | 1286 | {sign} 1287 |

    1288 |
  • `, 1289 | selection: [1, 3], 1290 | type: 'modified', 1291 | expect: ` 1292 | {nickname} 1293 |

    1297 | 1298 | {sign} 1299 |

    1300 |
    ` 1301 | } 1302 | ]; 1303 | await run(testCases, { 1304 | speed: slowSpeed, 1305 | timeout: slowTimeout 1306 | }); 1307 | }); 1308 | 1309 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/19', async () => { 1310 | const testCases: TestCase[] = [ 1311 | { 1312 | input: ` 1315 | 1316 | `, 1317 | type: 'w', 1318 | undoStops: true, 1319 | expect: ` 1322 | 1323 | ` 1324 | }, 1325 | { 1326 | type: 'w', 1327 | undoStops: true, 1328 | expect: ` 1331 | 1332 | ` 1333 | }, 1334 | { 1335 | type: 'w', 1336 | undoStops: true, 1337 | expect: ` 1340 | 1341 | ` 1342 | }, 1343 | { 1344 | type: '', 1345 | afterTypeCommands: ['undo'], 1346 | expect: ` 1349 | 1350 | ` 1351 | }, 1352 | { 1353 | type: 'w', 1354 | expect: ` 1357 | 1358 | ` 1359 | } 1360 | ]; 1361 | await run(testCases, { 1362 | speed: slowSpeed, 1363 | timeout: slowTimeout 1364 | }); 1365 | }); 1366 | 1367 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/472', async () => { 1368 | const testCases: TestCase[] = [ 1369 | { 1370 | input: ` 1371 |
    1372 | 1373 | 1374 | 1375 | 1376 |
    1377 |
    1378 | x1 1379 |
    1380 |
    1381 |
    1382 |
    1383 | x2 1384 |
    1385 |
    1386 |
    1387 |
    1388 | x3 1389 | 1390 |
    1391 |
    1392 |
    1393 | 1394 | `, 1395 | type: 'x', 1396 | undoStops: true, 1397 | expect: ` 1398 |
    1399 | 1400 | 1401 | 1402 | 1403 |
    1404 |
    1405 | x1 1406 |
    1407 |
    1408 |
    1409 |
    1410 | x2 1411 |
    1412 |
    1413 |
    1414 |
    1415 | x3 1416 | 1417 |
    1418 |
    1419 |
    1420 |
    1421 | ` 1422 | } 1423 | ]; 1424 | await run(testCases, { 1425 | speed: slowSpeed, 1426 | timeout: slowTimeout 1427 | }); 1428 | }); 1429 | 1430 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/250', async () => { 1431 | const testCases: TestCase[] = [ 1432 | { 1433 | input: `<|form>`, 1434 | type: ' ', 1435 | expect: `< form>` 1436 | }, 1437 | { 1438 | type: '{backspace}', 1439 | expect: `
    ` 1440 | } 1441 | ]; 1442 | await run(testCases, { 1443 | speed: slowSpeed, 1444 | timeout: slowTimeout 1445 | }); 1446 | }); 1447 | 1448 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/179', async () => { 1449 | const testCases: TestCase[] = [ 1450 | { 1451 | input: ` 1452 | 1453 |
    1454 | 1464 | `, 1465 | type: 'y', 1466 | expect: ` 1467 | 1468 |
    1469 | 1479 |
    ` 1480 | } 1481 | ]; 1482 | await run(testCases, { 1483 | speed: slowSpeed, 1484 | timeout: slowTimeout 1485 | }); 1486 | }); 1487 | 1488 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/93', async () => { 1489 | const testCases: TestCase[] = [ 1490 | { 1491 | input: `
    1492 | ', 1497 | expect: `
    1498 | 1501 |
    ` 1502 | } 1503 | ]; 1504 | await run(testCases, { 1505 | speed: slowSpeed, 1506 | timeout: slowTimeout 1507 | }); 1508 | }); 1509 | 1510 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/510', async () => { 1511 | const testCases: TestCase[] = [ 1512 | { 1513 | input: ` 1514 | {person.tags.map(tag => ( 1515 | 1520 | {tag} 1521 | 1522 | ))} 1523 | `, 1524 | type: 's', 1525 | expect: ` 1526 | {person.tags.map(tag => ( 1527 | 1532 | {tag} 1533 | 1534 | ))} 1535 | ` 1536 | }, 1537 | { 1538 | input: ` 1539 | {person.tags.map(tag => ( 1540 | 1545 | {tag} 1546 | 1547 | ))} 1548 | `, 1549 | type: 's', 1550 | expect: ` 1551 | {person.tags.map(tag => ( 1552 | 1557 | {tag} 1558 | 1559 | ))} 1560 | ` 1561 | } 1562 | ]; 1563 | await run(testCases, { 1564 | speed: slowSpeed, 1565 | timeout: slowTimeout 1566 | }); 1567 | }); 1568 | 1569 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/528', async () => { 1570 | await createTestFile('tag-starts-with-$.js'); 1571 | const testCases: TestCase[] = [ 1572 | { 1573 | input: `<$Label|>Label`, 1574 | type: '2', 1575 | expect: `<$Label2>Label` 1576 | } 1577 | ]; 1578 | await run(testCases, { 1579 | speed: slowSpeed, 1580 | timeout: slowTimeout 1581 | }); 1582 | }); 1583 | 1584 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/535', async () => { 1585 | await createTestFile('fragments-mistaken-as-tags-bug.js'); 1586 | const testCases: TestCase[] = [ 1587 | { 1588 | input: `function Item() { 1589 | return ( 1590 | <> 1591 |
      1592 | <| 1593 |
    1594 | 1595 | Test 1596 | 1597 | ) 1598 | }`, 1599 | type: 'li', 1600 | expect: `function Item() { 1601 | return ( 1602 | <> 1603 |
      1604 |
    • 1606 | 1607 | Test 1608 | 1609 | ) 1610 | }` 1611 | } 1612 | ]; 1613 | await run(testCases, { 1614 | speed: slowSpeed, 1615 | timeout: slowTimeout 1616 | }); 1617 | }); 1618 | 1619 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/542', async () => { 1620 | await createTestFile('script-and-style-bug.html'); 1621 | const testCases: TestCase[] = [ 1622 | { 1623 | input: ` 1624 | `, 1625 | type: 't', 1626 | expect: ` 1627 | ` 1628 | }, 1629 | { 1630 | input: ` 1631 | `, 1632 | type: 't', 1633 | expect: ` 1634 | ` 1635 | }, 1636 | { 1637 | input: ``, 1685 | type: '{backspace}abc', 1686 | expect: `` 1687 | } 1688 | ]; 1689 | await run(testCases, { 1690 | speed: slowSpeed, 1691 | timeout: slowTimeout 1692 | }); 1693 | }); 1694 | 1695 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/598', async () => { 1696 | await createTestFile('php-bug.php'); 1697 | const testCases: TestCase[] = [ 1698 | { 1699 | input: `if $variable < 0 { 1700 | 1701 | } 1702 | $table .= ''; 1703 | `, 1704 | type: 'table', 1705 | expect: `if $variable < 0 { 1706 | 1707 | } 1708 | $table .= ''; 1709 | ` 1710 | } 1711 | ]; 1712 | await run(testCases, { 1713 | timeout: slowTimeout 1714 | }); 1715 | }); 1716 | 1717 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/568', async () => { 1718 | await createTestFile('bug-568.txt'); 1719 | const testCases: TestCase[] = [ 1720 | { 1721 | input: `

      `, 1722 | type: '{backspace}h2', 1723 | expect: `

      ` 1724 | } 1725 | ]; 1726 | for (let i = 0; i < 100; i++) { 1727 | await run(testCases, { 1728 | timeout: slowTimeout 1729 | }); 1730 | } 1731 | }); 1732 | 1733 | test('bug https://github.com/formulahendry/vscode-auto-rename-tag/issues/567#issuecomment-1083228518', async () => { 1734 | await createTestFile('bug-567.html'); 1735 | const testCases: TestCase[] = [ 1736 | { 1737 | input: `
      1738 |
      | 1739 |
      1740 |
      `, 1741 | type: '{backspace}{backspace}{backspace}{backspace}span>', 1742 | expect: `
      1743 | 1744 |
      1745 |
      ` 1746 | }, 1747 | { 1748 | input: `
      1749 | 1750 |
      1751 |
      `, 1752 | type: '{backspace}{backspace}{backspace}span', 1753 | expect: `
      1754 | 1755 |
      1756 |
      ` 1757 | } 1758 | ]; 1759 | await run(testCases, {}); 1760 | }); 1761 | }); 1762 | -------------------------------------------------------------------------------- /packages/extension-test/src/basic/basic-workspace/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.renameOnType": false, 3 | "editor.formatOnSave": false, 4 | "html.autoClosingTags": false, 5 | "files.eol": "\n" 6 | } 7 | -------------------------------------------------------------------------------- /packages/extension-test/src/basic/suite.ts: -------------------------------------------------------------------------------- 1 | import { createRunner } from '../test-utils'; 2 | export const run = createRunner(__dirname); 3 | -------------------------------------------------------------------------------- /packages/extension-test/src/extensionTestMain.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import * as path from 'path'; 3 | import { downloadAndUnzipVSCode, runTests } from '@vscode/test-electron'; 4 | 5 | const root = path.join(__dirname, '../../../'); 6 | const vscodeVersion = '1.64.0'; 7 | const extensionDevelopmentPath = path.join(root, 'packages/extension'); 8 | 9 | interface Test { 10 | path: string; 11 | } 12 | 13 | const run = async (test: Test) => { 14 | try { 15 | const workspacePathSrc = path.join( 16 | __dirname.replace('dist', 'src'), 17 | `${test.path}/${test.path}-workspace` 18 | ); 19 | const workspacePathDist = path.join( 20 | __dirname, 21 | `${test.path}/${test.path}-workspace-dist` 22 | ); 23 | await fs.copy(workspacePathSrc, workspacePathDist); 24 | const extensionTestsPath = path.join(__dirname, test.path, 'suite'); 25 | const vscodeExecutablePath = await downloadAndUnzipVSCode(vscodeVersion); 26 | 27 | const launchArgs: string[] = ['--disable-extensions', workspacePathDist]; 28 | await runTests({ 29 | vscodeExecutablePath, 30 | extensionDevelopmentPath, 31 | extensionTestsPath, 32 | launchArgs, 33 | extensionTestsEnv: { 34 | extensionPath: extensionDevelopmentPath, 35 | NODE_ENV: 'test' 36 | } 37 | }); 38 | } catch (err) { 39 | console.error(err); 40 | console.error('Failed to run tests'); 41 | process.exit(1); 42 | } 43 | }; 44 | 45 | run({ path: 'basic' }); 46 | -------------------------------------------------------------------------------- /packages/extension-test/src/test-utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as assert from 'assert'; 3 | import * as path from 'path'; 4 | import * as Mocha from 'mocha'; 5 | import * as fs from 'fs'; 6 | 7 | const packageJSON = JSON.parse( 8 | fs.readFileSync(path.join(process.env.extensionPath, 'package.json'), 'utf-8') 9 | ) as { name: string; publisher: string }; 10 | 11 | const extension = vscode.extensions.getExtension( 12 | `${packageJSON.publisher}.${packageJSON.name}` 13 | ) as vscode.Extension; 14 | 15 | assert.ok(extension); 16 | 17 | export const activateExtension = () => extension.activate(); 18 | 19 | export interface TestCase { 20 | input?: string; 21 | literalInput?: string; 22 | offset?: number; 23 | type?: string; 24 | expect?: string; 25 | expectOtherFiles?: { [key: string]: string }; 26 | only?: boolean; 27 | speed?: number; 28 | skip?: boolean; 29 | timeout?: 'never' | number; 30 | debug?: boolean; 31 | waitForAutoComplete?: 1; 32 | selection?: [number, number]; 33 | afterTypeCommands?: string[]; 34 | undoStops?: boolean; 35 | } 36 | 37 | export async function createTestFile( 38 | fileName: string, 39 | content: string = '' 40 | ): Promise { 41 | const filePath = path.join( 42 | vscode.workspace.workspaceFolders![0].uri.fsPath, 43 | fileName 44 | ); 45 | fs.writeFileSync(filePath, content); 46 | const uri = vscode.Uri.file(filePath); 47 | await vscode.window.showTextDocument(uri); 48 | } 49 | 50 | export async function removeTestFile(): Promise { 51 | const uri = vscode.window.activeTextEditor?.document.uri as vscode.Uri; 52 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 53 | await vscode.workspace.fs.delete(uri); 54 | } 55 | 56 | export async function setText(text: string): Promise { 57 | const document = vscode.window.activeTextEditor!.document; 58 | const all = new vscode.Range( 59 | document.positionAt(0), 60 | document.positionAt(document.getText().length) 61 | ); 62 | await vscode.window.activeTextEditor!.edit(editBuilder => 63 | editBuilder.replace(all, text) 64 | ); 65 | } 66 | 67 | function setCursorPositions(offsets: number[]): void { 68 | const positions = offsets.map(offset => 69 | vscode.window.activeTextEditor!.document.positionAt(offset) 70 | ); 71 | const selections = positions.map( 72 | position => new vscode.Selection(position, position) 73 | ); 74 | vscode.window.activeTextEditor!.selections = selections; 75 | } 76 | 77 | async function typeLiteral(text: string, undoStops = false): Promise { 78 | await vscode.window.activeTextEditor!.insertSnippet( 79 | new vscode.SnippetString(text), 80 | vscode.window.activeTextEditor!.selections, 81 | { 82 | undoStopAfter: undoStops, 83 | undoStopBefore: undoStops 84 | } 85 | ); 86 | } 87 | 88 | async function typeDelete(times: number = 1): Promise { 89 | const offsets = vscode.window.activeTextEditor!.selections.map(selection => 90 | vscode.window.activeTextEditor!.document.offsetAt(selection.active) 91 | ); 92 | await new Promise(async resolve => { 93 | await vscode.window.activeTextEditor!.edit(editBuilder => { 94 | for (const offset of offsets) { 95 | editBuilder.delete( 96 | new vscode.Range( 97 | vscode.window.activeTextEditor!.document.positionAt(offset - times), 98 | vscode.window.activeTextEditor!.document.positionAt(offset) 99 | ) 100 | ); 101 | } 102 | }); 103 | resolve(); 104 | }); 105 | } 106 | async function type( 107 | text: string, 108 | speed = 150, 109 | undoStops = false 110 | ): Promise { 111 | for (let i = 0; i < text.length; i++) { 112 | if (i === 0) { 113 | await new Promise(resolve => setTimeout(resolve, speed / 2)); 114 | } else { 115 | await new Promise(resolve => setTimeout(resolve, speed)); 116 | } 117 | if (text.slice(i).startsWith('{backspace}')) { 118 | await typeDelete(); 119 | i += '{backspace}'.length - 1; 120 | } else if (text.slice(i).startsWith('{undo}')) { 121 | await vscode.commands.executeCommand('undo'); 122 | i += '{undo}'.length - 1; 123 | } else if (text.slice(i).startsWith('{redo}')) { 124 | await vscode.commands.executeCommand('redo'); 125 | i += '{redo}'.length - 1; 126 | } else if (text.slice(i).startsWith('{tab}')) { 127 | await vscode.commands.executeCommand('html-expand-abbreviation'); 128 | i += '{tab}'.length - 1; 129 | } else if (text.slice(i).startsWith('{end}')) { 130 | await vscode.commands.executeCommand('cursorEnd'); 131 | i += '{end}'.length - 1; 132 | } else if (text.slice(i).startsWith('{down}')) { 133 | await vscode.commands.executeCommand('cursorDown'); 134 | i += '{down}'.length - 1; 135 | } else if (text.slice(i).startsWith('{copyLineDown}')) { 136 | await vscode.commands.executeCommand( 137 | 'editor!.action.copyLinesDownAction' 138 | ); 139 | i += '{copyLineDown}'.length - 1; 140 | } else { 141 | await typeLiteral(text[i], undoStops); 142 | } 143 | } 144 | } 145 | 146 | async function waitForAutoComplete(timeout: 'never' | number) { 147 | return new Promise(resolve => { 148 | const disposable = vscode.workspace.onDidChangeTextDocument(() => { 149 | disposable.dispose(); 150 | resolve(); 151 | }); 152 | if (timeout !== 'never') { 153 | setTimeout(resolve, timeout); 154 | } 155 | }); 156 | } 157 | 158 | export function getText(): string { 159 | return vscode.window.activeTextEditor!.document.getText(); 160 | } 161 | 162 | export async function run( 163 | testCases: TestCase[], 164 | { speed = 0, timeout = 40, afterCommands = [] as any[] } = {} 165 | ) { 166 | await setText(''); 167 | const only = testCases.filter(testCase => testCase.only); 168 | const applicableTestCases = only.length ? only : testCases; 169 | for (const testCase of applicableTestCases) { 170 | if (testCase.skip) { 171 | continue; 172 | } 173 | if (testCase.literalInput !== undefined) { 174 | await setText(testCase.literalInput); 175 | } 176 | if (testCase.offset !== undefined) { 177 | setCursorPositions([testCase.offset]); 178 | } 179 | if (testCase.input !== undefined) { 180 | const cursorOffsets = []; 181 | for (let i = 0; i < testCase.input.length; i++) { 182 | if (testCase.input[i] === '|') { 183 | cursorOffsets.push(i - cursorOffsets.length); 184 | } 185 | } 186 | const input = testCase.input.replace(/\|/g, ''); 187 | await setText(input); 188 | if (cursorOffsets.length > 0) { 189 | setCursorPositions(cursorOffsets); 190 | } 191 | } 192 | if (testCase.selection) { 193 | const [start, end] = testCase.selection; 194 | vscode.window.activeTextEditor!.selection = new vscode.Selection( 195 | vscode.window.activeTextEditor!.document.positionAt(start), 196 | vscode.window.activeTextEditor!.document.positionAt(end) 197 | ); 198 | } 199 | if (testCase.type) { 200 | await type( 201 | testCase.type, 202 | testCase.speed || speed, 203 | testCase.undoStops || false 204 | ); 205 | const autoCompleteTimeout = testCase.timeout || timeout; 206 | await waitForAutoComplete(autoCompleteTimeout); 207 | } 208 | 209 | const resolvedAfterCommands = testCase.afterTypeCommands || afterCommands; 210 | for (const afterCommand of resolvedAfterCommands) { 211 | await vscode.commands.executeCommand(afterCommand); 212 | const autoCompleteTimeout = testCase.timeout || timeout; 213 | await waitForAutoComplete(autoCompleteTimeout); 214 | } 215 | // await vscode.commands.executeCommand('type', { text: 'Hello' }) 216 | const result = getText(); 217 | if (testCase.debug) { 218 | await new Promise(() => {}); 219 | } 220 | if (testCase.expect !== undefined) { 221 | assert.equal(result, testCase.expect); 222 | } 223 | if (testCase.expectOtherFiles) { 224 | for (const [relativePath, expectedContent] of Object.entries( 225 | testCase.expectOtherFiles 226 | )) { 227 | const absolutePath = path.join( 228 | vscode.workspace.workspaceFolders[0].uri.fsPath, 229 | relativePath 230 | ); 231 | const document = await vscode.workspace.openTextDocument( 232 | vscode.Uri.file(absolutePath) 233 | ); 234 | const actualContent = document.getText(); 235 | assert.equal(actualContent, expectedContent); 236 | } 237 | } 238 | } 239 | } 240 | 241 | export const slowSpeed = 300; 242 | 243 | export const slowTimeout = 8000; 244 | 245 | export const createRunner: (dirname: string) => () => Promise = 246 | dirname => () => { 247 | const mocha = new Mocha({ 248 | ui: 'tdd', 249 | timeout: 1000000, 250 | color: true, 251 | bail: true 252 | }); 253 | 254 | return new Promise((resolve, reject) => { 255 | const fileNames = fs 256 | .readdirSync(dirname) 257 | .filter(fileName => fileName.endsWith('.test.js')); 258 | for (const fileName of fileNames) { 259 | mocha.addFile(path.join(dirname, fileName)); 260 | } 261 | try { 262 | mocha.run(failures => { 263 | if (failures > 0) { 264 | reject(new Error(`${failures} tests failed.`)); 265 | } else { 266 | resolve(); 267 | } 268 | }); 269 | } catch (err) { 270 | reject(err); 271 | } 272 | }); 273 | }; 274 | -------------------------------------------------------------------------------- /packages/extension-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-noncomposite-base", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "types": ["node"], 7 | "noUnusedLocals": false, 8 | "noUnusedParameters": false, 9 | "allowUnreachableCode": true, 10 | "noImplicitAny": false, 11 | "strict": false, 12 | "strictNullChecks": false, 13 | "strictPropertyInitialization": false 14 | }, 15 | "include": ["src"], 16 | "references": [] 17 | } 18 | -------------------------------------------------------------------------------- /packages/extension/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-rename-tag", 3 | "version": "0.1.10", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "auto-rename-tag", 9 | "version": "0.1.10", 10 | "license": "MIT", 11 | "dependencies": { 12 | "source-map-support": "^0.5.21", 13 | "vscode-languageclient": "^7.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/vscode": "^1.65.0", 17 | "typescript": "^4.6.3" 18 | }, 19 | "engines": { 20 | "vscode": "^1.41.1" 21 | } 22 | }, 23 | "node_modules/@types/vscode": { 24 | "version": "1.65.0", 25 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.65.0.tgz", 26 | "integrity": "sha512-wQhExnh2nEzpjDMSKhUvnNmz3ucpd3E+R7wJkOhBNK3No6fG3VUdmVmMOKD0A8NDZDDDiQcLNxe3oGmX5SjJ5w==", 27 | "dev": true 28 | }, 29 | "node_modules/balanced-match": { 30 | "version": "1.0.2", 31 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 32 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 33 | }, 34 | "node_modules/brace-expansion": { 35 | "version": "1.1.11", 36 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 37 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 38 | "dependencies": { 39 | "balanced-match": "^1.0.0", 40 | "concat-map": "0.0.1" 41 | } 42 | }, 43 | "node_modules/buffer-from": { 44 | "version": "1.1.1", 45 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 46 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 47 | }, 48 | "node_modules/concat-map": { 49 | "version": "0.0.1", 50 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 51 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 52 | }, 53 | "node_modules/lru-cache": { 54 | "version": "6.0.0", 55 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 56 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 57 | "dependencies": { 58 | "yallist": "^4.0.0" 59 | }, 60 | "engines": { 61 | "node": ">=10" 62 | } 63 | }, 64 | "node_modules/minimatch": { 65 | "version": "3.0.5", 66 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", 67 | "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", 68 | "dependencies": { 69 | "brace-expansion": "^1.1.7" 70 | }, 71 | "engines": { 72 | "node": "*" 73 | } 74 | }, 75 | "node_modules/semver": { 76 | "version": "7.3.5", 77 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 78 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 79 | "dependencies": { 80 | "lru-cache": "^6.0.0" 81 | }, 82 | "bin": { 83 | "semver": "bin/semver.js" 84 | }, 85 | "engines": { 86 | "node": ">=10" 87 | } 88 | }, 89 | "node_modules/source-map": { 90 | "version": "0.6.1", 91 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 92 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 93 | "engines": { 94 | "node": ">=0.10.0" 95 | } 96 | }, 97 | "node_modules/source-map-support": { 98 | "version": "0.5.21", 99 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 100 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 101 | "dependencies": { 102 | "buffer-from": "^1.0.0", 103 | "source-map": "^0.6.0" 104 | } 105 | }, 106 | "node_modules/typescript": { 107 | "version": "4.6.3", 108 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", 109 | "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", 110 | "dev": true, 111 | "bin": { 112 | "tsc": "bin/tsc", 113 | "tsserver": "bin/tsserver" 114 | }, 115 | "engines": { 116 | "node": ">=4.2.0" 117 | } 118 | }, 119 | "node_modules/vscode-jsonrpc": { 120 | "version": "6.0.0", 121 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", 122 | "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", 123 | "engines": { 124 | "node": ">=8.0.0 || >=10.0.0" 125 | } 126 | }, 127 | "node_modules/vscode-languageclient": { 128 | "version": "7.0.0", 129 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", 130 | "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", 131 | "dependencies": { 132 | "minimatch": "^3.0.4", 133 | "semver": "^7.3.4", 134 | "vscode-languageserver-protocol": "3.16.0" 135 | }, 136 | "engines": { 137 | "vscode": "^1.52.0" 138 | } 139 | }, 140 | "node_modules/vscode-languageserver-protocol": { 141 | "version": "3.16.0", 142 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", 143 | "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", 144 | "dependencies": { 145 | "vscode-jsonrpc": "6.0.0", 146 | "vscode-languageserver-types": "3.16.0" 147 | } 148 | }, 149 | "node_modules/vscode-languageserver-types": { 150 | "version": "3.16.0", 151 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", 152 | "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" 153 | }, 154 | "node_modules/yallist": { 155 | "version": "4.0.0", 156 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 157 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 158 | } 159 | }, 160 | "dependencies": { 161 | "@types/vscode": { 162 | "version": "1.65.0", 163 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.65.0.tgz", 164 | "integrity": "sha512-wQhExnh2nEzpjDMSKhUvnNmz3ucpd3E+R7wJkOhBNK3No6fG3VUdmVmMOKD0A8NDZDDDiQcLNxe3oGmX5SjJ5w==", 165 | "dev": true 166 | }, 167 | "balanced-match": { 168 | "version": "1.0.2", 169 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 170 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 171 | }, 172 | "brace-expansion": { 173 | "version": "1.1.11", 174 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 175 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 176 | "requires": { 177 | "balanced-match": "^1.0.0", 178 | "concat-map": "0.0.1" 179 | } 180 | }, 181 | "buffer-from": { 182 | "version": "1.1.1", 183 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 184 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 185 | }, 186 | "concat-map": { 187 | "version": "0.0.1", 188 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 189 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 190 | }, 191 | "lru-cache": { 192 | "version": "6.0.0", 193 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 194 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 195 | "requires": { 196 | "yallist": "^4.0.0" 197 | } 198 | }, 199 | "minimatch": { 200 | "version": "3.0.5", 201 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", 202 | "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", 203 | "requires": { 204 | "brace-expansion": "^1.1.7" 205 | } 206 | }, 207 | "semver": { 208 | "version": "7.3.5", 209 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 210 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 211 | "requires": { 212 | "lru-cache": "^6.0.0" 213 | } 214 | }, 215 | "source-map": { 216 | "version": "0.6.1", 217 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 218 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 219 | }, 220 | "source-map-support": { 221 | "version": "0.5.21", 222 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 223 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 224 | "requires": { 225 | "buffer-from": "^1.0.0", 226 | "source-map": "^0.6.0" 227 | } 228 | }, 229 | "typescript": { 230 | "version": "4.6.3", 231 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", 232 | "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", 233 | "dev": true 234 | }, 235 | "vscode-jsonrpc": { 236 | "version": "6.0.0", 237 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", 238 | "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==" 239 | }, 240 | "vscode-languageclient": { 241 | "version": "7.0.0", 242 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", 243 | "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", 244 | "requires": { 245 | "minimatch": "^3.0.4", 246 | "semver": "^7.3.4", 247 | "vscode-languageserver-protocol": "3.16.0" 248 | } 249 | }, 250 | "vscode-languageserver-protocol": { 251 | "version": "3.16.0", 252 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", 253 | "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", 254 | "requires": { 255 | "vscode-jsonrpc": "6.0.0", 256 | "vscode-languageserver-types": "3.16.0" 257 | } 258 | }, 259 | "vscode-languageserver-types": { 260 | "version": "3.16.0", 261 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", 262 | "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" 263 | }, 264 | "yallist": { 265 | "version": "4.0.0", 266 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 267 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /packages/extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-rename-tag", 3 | "displayName": "Auto Rename Tag", 4 | "description": "Auto rename paired HTML/XML tag", 5 | "version": "0.1.10", 6 | "publisher": "formulahendry", 7 | "license": "MIT", 8 | "icon": "images/logo.png", 9 | "engines": { 10 | "vscode": "^1.41.1" 11 | }, 12 | "categories": [ 13 | "Other", 14 | "Programming Languages" 15 | ], 16 | "keywords": [ 17 | "AutoComplete", 18 | "rename", 19 | "tag", 20 | "html", 21 | "xml", 22 | "multi-root ready" 23 | ], 24 | "bugs": { 25 | "url": "https://github.com/formulahendry/vscode-auto-rename-tag/issues", 26 | "email": "formulahendry@gmail.com" 27 | }, 28 | "homepage": "https://github.com/formulahendry/vscode-auto-rename-tag/blob/master/README.md", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/formulahendry/vscode-auto-rename-tag.git" 32 | }, 33 | "activationEvents": [ 34 | "*" 35 | ], 36 | "main": "dist/extensionMain.js", 37 | "capabilities": { 38 | "untrustedWorkspaces": { 39 | "supported": true 40 | }, 41 | "virtualWorkspaces": true 42 | }, 43 | "contributes": { 44 | "configuration": { 45 | "type": "object", 46 | "title": "Auto Rename Tag configuration", 47 | "properties": { 48 | "auto-rename-tag.activationOnLanguage": { 49 | "type": "array", 50 | "default": [ 51 | "*" 52 | ], 53 | "description": "Set the languages that the extension will be activated. e.g. [\"html\",\"xml\",\"php\"] By default, it is [\"*\"] and will be activated for all languages.", 54 | "scope": "resource" 55 | } 56 | } 57 | } 58 | }, 59 | "extensionKind": [ 60 | "ui", 61 | "workspace" 62 | ], 63 | "scripts": { 64 | "build": "tsc -b" 65 | }, 66 | "devDependencies": { 67 | "@types/vscode": "^1.65.0", 68 | "typescript": "^4.6.3" 69 | }, 70 | "dependencies": { 71 | "source-map-support": "^0.5.21", 72 | "vscode-languageclient": "^7.0.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/extension/playground/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "html.mirrorCursorOnMatchingTag": false, 3 | "editor.linkedEditing": false, 4 | "editor.formatOnSave": false 5 | } 6 | -------------------------------------------------------------------------------- /packages/extension/src/createLanguageClientProxy.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | Code2ProtocolConverter, 4 | LanguageClient, 5 | LanguageClientOptions, 6 | RequestType, 7 | ServerOptions, 8 | TransportKind 9 | } from 'vscode-languageclient/node'; 10 | 11 | type VslSendRequest = ( 12 | type: RequestType, 13 | params: P 14 | ) => Thenable; 15 | 16 | export interface LanguageClientProxy { 17 | readonly code2ProtocolConverter: Code2ProtocolConverter; 18 | readonly sendRequest: VslSendRequest; 19 | } 20 | 21 | export const createLanguageClientProxy: ( 22 | context: vscode.ExtensionContext, 23 | id: string, 24 | name: string, 25 | clientOptions: LanguageClientOptions 26 | ) => Promise = async ( 27 | context, 28 | id, 29 | name, 30 | clientOptions 31 | ) => { 32 | const serverModule = context.asAbsolutePath('../server/dist/serverMain.js'); 33 | const serverOptions: ServerOptions = { 34 | run: { module: serverModule, transport: TransportKind.ipc }, 35 | debug: { 36 | module: serverModule, 37 | transport: TransportKind.ipc, 38 | options: { execArgv: ['--nolazy', '--inspect=6009'] } 39 | } 40 | }; 41 | const outputChannel = vscode.window.createOutputChannel(name); 42 | clientOptions.outputChannel = { 43 | name: outputChannel.name, 44 | append() {}, 45 | appendLine(value: string) { 46 | try { 47 | const message = JSON.parse(value); 48 | if (!message.isLSPMessage) { 49 | outputChannel.appendLine(value); 50 | } 51 | } catch (error) { 52 | if (typeof value !== 'object') { 53 | outputChannel.appendLine(value); 54 | } 55 | } 56 | }, 57 | replace(value) { 58 | outputChannel.replace(value); 59 | }, 60 | clear() { 61 | outputChannel.clear(); 62 | }, 63 | show() { 64 | outputChannel.show(); 65 | }, 66 | hide() { 67 | outputChannel.hide(); 68 | }, 69 | dispose() { 70 | outputChannel.dispose(); 71 | } 72 | }; 73 | 74 | const languageClient = new LanguageClient( 75 | id, 76 | name, 77 | serverOptions, 78 | clientOptions 79 | ); 80 | 81 | languageClient.registerProposedFeatures(); 82 | context.subscriptions.push(languageClient.start()); 83 | await languageClient.onReady(); 84 | const languageClientProxy: LanguageClientProxy = { 85 | code2ProtocolConverter: languageClient.code2ProtocolConverter, 86 | sendRequest: (type, params) => languageClient.sendRequest(type, params) 87 | }; 88 | return languageClientProxy; 89 | }; 90 | -------------------------------------------------------------------------------- /packages/extension/src/extensionMain.ts: -------------------------------------------------------------------------------- 1 | import { AssertionError } from 'assert'; 2 | import 'source-map-support/register'; 3 | import * as vscode from 'vscode'; 4 | import { 5 | Disposable, 6 | LanguageClientOptions, 7 | RequestType, 8 | VersionedTextDocumentIdentifier 9 | } from 'vscode-languageclient'; 10 | import { 11 | createLanguageClientProxy, 12 | LanguageClientProxy 13 | } from './createLanguageClientProxy'; 14 | 15 | interface Tag { 16 | word: string; 17 | offset: number; 18 | oldWord: string; 19 | previousOffset: number; 20 | } 21 | 22 | interface Params { 23 | readonly textDocument: VersionedTextDocumentIdentifier; 24 | readonly tags: Tag[]; 25 | } 26 | 27 | interface Result { 28 | readonly originalOffset: number; 29 | readonly originalWord: string; 30 | readonly startOffset: number; 31 | readonly endOffset: number; 32 | readonly tagName: string; 33 | } 34 | 35 | const assertDefined: (value: T) => asserts value is NonNullable = val => { 36 | if (val === undefined || val === null) { 37 | throw new AssertionError({ 38 | message: `Expected 'value' to be defined, but received ${val}` 39 | }); 40 | } 41 | }; 42 | 43 | const autoRenameTagRequestType = new RequestType( 44 | '$/auto-rename-tag' 45 | ); 46 | 47 | // TODO implement max concurrent requests 48 | 49 | const askServerForAutoCompletionsElementRenameTag: ( 50 | languageClientProxy: LanguageClientProxy, 51 | document: vscode.TextDocument, 52 | tags: Tag[] 53 | ) => Promise = async (languageClientProxy, document, tags) => { 54 | const params: Params = { 55 | textDocument: 56 | languageClientProxy.code2ProtocolConverter.asVersionedTextDocumentIdentifier( 57 | document 58 | ), 59 | tags 60 | }; 61 | return languageClientProxy.sendRequest(autoRenameTagRequestType, params); 62 | }; 63 | 64 | /** 65 | * Utility variable that stores the last changed version (document.uri.fsPath and document.version) 66 | * When a change was caused by auto-rename-tag, we can ignore that change, which is a simple performance improvement. One thing to take care of is undo, but that works now (and there are test cases). 67 | */ 68 | let lastChangeByAutoRenameTag: { fsPath: string; version: number } = { 69 | fsPath: '', 70 | version: -1 71 | }; 72 | 73 | const applyResults: (results: Result[]) => Promise = async results => { 74 | assertDefined(vscode.window.activeTextEditor); 75 | const prev = vscode.window.activeTextEditor.document.version; 76 | const applied = await vscode.window.activeTextEditor.edit( 77 | editBuilder => { 78 | assertDefined(vscode.window.activeTextEditor); 79 | for (const result of results) { 80 | const startPosition = 81 | vscode.window.activeTextEditor.document.positionAt( 82 | result.startOffset 83 | ); 84 | const endPosition = vscode.window.activeTextEditor.document.positionAt( 85 | result.endOffset 86 | ); 87 | const range = new vscode.Range(startPosition, endPosition); 88 | editBuilder.replace(range, result.tagName); 89 | } 90 | }, 91 | { 92 | undoStopBefore: false, 93 | undoStopAfter: false 94 | } 95 | ); 96 | 97 | const next = vscode.window.activeTextEditor.document.version; 98 | if (!applied) { 99 | return; 100 | } 101 | lastChangeByAutoRenameTag = { 102 | fsPath: vscode.window.activeTextEditor.document.uri.fsPath, 103 | version: vscode.window.activeTextEditor.document.version 104 | }; 105 | if (prev + 1 !== next) { 106 | return; 107 | } 108 | for (const result of results) { 109 | const oldWordAtOffset = wordsAtOffsets[result.originalOffset]; 110 | delete wordsAtOffsets[result.originalOffset]; 111 | 112 | let moved = 0; 113 | if (result.originalWord.startsWith('\s\\\/\'\"\(\)\`\{\}\[\]]*$/; 126 | const tagNameRERight = /^[^<>\s\\\/\'\"\(\)\`\{\}\[\]]*/; 127 | 128 | let wordsAtOffsets: { 129 | [offset: string]: { 130 | oldWord: string; 131 | newWord: string; 132 | }; 133 | } = {}; 134 | 135 | const updateWordsAtOffset: (tags: Tag[]) => void = tags => { 136 | const keys = Object.keys(wordsAtOffsets); 137 | if (keys.length > 0) { 138 | if (keys.length !== tags.length) { 139 | wordsAtOffsets = {}; 140 | } 141 | for (const tag of tags) { 142 | if (!wordsAtOffsets.hasOwnProperty(tag.previousOffset)) { 143 | wordsAtOffsets = {}; 144 | break; 145 | } 146 | } 147 | } 148 | for (const tag of tags) { 149 | wordsAtOffsets[tag.offset] = { 150 | oldWord: 151 | (wordsAtOffsets[tag.previousOffset] && 152 | wordsAtOffsets[tag.previousOffset].oldWord) || 153 | tag.oldWord, 154 | newWord: tag.word 155 | }; 156 | if (tag.previousOffset !== tag.offset) { 157 | delete wordsAtOffsets[tag.previousOffset]; 158 | } 159 | tag.oldWord = wordsAtOffsets[tag.offset].oldWord; 160 | } 161 | }; 162 | const doAutoCompletionElementRenameTag: ( 163 | languageClientProxy: LanguageClientProxy, 164 | tags: Tag[] 165 | ) => Promise = async (languageClientProxy, tags) => { 166 | if (latestCancelTokenSource) { 167 | latestCancelTokenSource.cancel(); 168 | } 169 | const cancelTokenSource = new vscode.CancellationTokenSource(); 170 | latestCancelTokenSource = cancelTokenSource; 171 | if (!vscode.window.activeTextEditor) { 172 | return; 173 | } 174 | const beforeVersion = vscode.window.activeTextEditor.document.version; 175 | // the change event is fired before we can update the version of the last change by auto rename tag, therefore we wait for that 176 | await new Promise(resolve => setTimeout(resolve, 0)); 177 | if (!vscode.window.activeTextEditor) { 178 | return; 179 | } 180 | if ( 181 | lastChangeByAutoRenameTag.fsPath === 182 | vscode.window.activeTextEditor.document.uri.fsPath && 183 | lastChangeByAutoRenameTag.version === 184 | vscode.window.activeTextEditor.document.version 185 | ) { 186 | return; 187 | } 188 | 189 | if (cancelTokenSource.token.isCancellationRequested) { 190 | return; 191 | } 192 | 193 | const results = await askServerForAutoCompletionsElementRenameTag( 194 | languageClientProxy, 195 | vscode.window.activeTextEditor.document, 196 | tags 197 | ); 198 | if (cancelTokenSource.token.isCancellationRequested) { 199 | return; 200 | } 201 | if (latestCancelTokenSource === cancelTokenSource) { 202 | latestCancelTokenSource = undefined; 203 | cancelTokenSource.dispose(); 204 | } 205 | if (results.length === 0) { 206 | wordsAtOffsets = {}; 207 | return; 208 | } 209 | if (!vscode.window.activeTextEditor) { 210 | return; 211 | } 212 | const afterVersion = vscode.window.activeTextEditor.document.version; 213 | if (beforeVersion !== afterVersion) { 214 | return; 215 | } 216 | await applyResults(results); 217 | }; 218 | 219 | const setPreviousText: ( 220 | textEditor: vscode.TextEditor | undefined 221 | ) => void = textEditor => { 222 | if (textEditor) { 223 | previousText = textEditor.document.getText(); 224 | } else { 225 | previousText = undefined; 226 | } 227 | }; 228 | 229 | export const activate: ( 230 | context: vscode.ExtensionContext 231 | ) => Promise = async context => { 232 | vscode.workspace 233 | .getConfiguration('auto-rename-tag') 234 | .get('activationOnLanguage'); 235 | const isEnabled = (document: vscode.TextDocument | undefined) => { 236 | if (!document) { 237 | return false; 238 | } 239 | 240 | const languageId = document.languageId; 241 | 242 | if (languageId === 'html' || languageId === 'handlebars') { 243 | const editorSettings = vscode.workspace.getConfiguration( 244 | 'editor', 245 | document 246 | ); 247 | if ( 248 | editorSettings.get('renameOnType') || 249 | editorSettings.get('linkedEditing') 250 | ) { 251 | return false; 252 | } 253 | } 254 | 255 | const config = vscode.workspace.getConfiguration( 256 | 'auto-rename-tag', 257 | document.uri 258 | ); 259 | 260 | const languages = config.get('activationOnLanguage', ['*']); 261 | return languages.includes('*') || languages.includes(languageId); 262 | }; 263 | context.subscriptions.push( 264 | vscode.workspace.onDidChangeConfiguration(event => { 265 | // purges cache for `vscode.workspace.getConfiguration` 266 | if (!event.affectsConfiguration('auto-rename-tag')) { 267 | return; 268 | } 269 | }) 270 | ); 271 | const clientOptions: LanguageClientOptions = { 272 | documentSelector: [ 273 | { 274 | scheme: '*' 275 | } 276 | ] 277 | }; 278 | const languageClientProxy = await createLanguageClientProxy( 279 | context, 280 | 'auto-rename-tag', 281 | 'Auto Rename Tag', 282 | clientOptions 283 | ); 284 | let activeTextEditor: vscode.TextEditor | undefined = 285 | vscode.window.activeTextEditor; 286 | let changeListener: Disposable | undefined; 287 | context.subscriptions.push({ 288 | dispose() { 289 | if (changeListener) { 290 | changeListener.dispose(); 291 | changeListener = undefined; 292 | } 293 | } 294 | }); 295 | const setupChangeListener = () => { 296 | if (changeListener) { 297 | return; 298 | } 299 | changeListener = vscode.workspace.onDidChangeTextDocument(async event => { 300 | if (event.document !== activeTextEditor?.document) { 301 | return; 302 | } 303 | 304 | if (!isEnabled(event.document)) { 305 | changeListener?.dispose(); 306 | changeListener = undefined; 307 | return; 308 | } 309 | 310 | if (event.contentChanges.length === 0) { 311 | return; 312 | } 313 | 314 | const currentText = event.document.getText(); 315 | const tags: Tag[] = []; 316 | let totalInserted = 0; 317 | const sortedChanges = event.contentChanges 318 | .slice() 319 | .sort((a, b) => a.rangeOffset - b.rangeOffset); 320 | const keys = Object.keys(wordsAtOffsets); 321 | for (const change of sortedChanges) { 322 | for (const key of keys) { 323 | const parsedKey = parseInt(key, 10); 324 | if ( 325 | change.rangeOffset <= parsedKey && 326 | parsedKey <= change.rangeOffset + change.rangeLength 327 | ) { 328 | delete wordsAtOffsets[key]; 329 | } 330 | } 331 | assertDefined(previousText); 332 | const line = event.document.lineAt(change.range.start.line); 333 | const lineStart = event.document.offsetAt(line.range.start); 334 | const lineChangeOffset = change.rangeOffset - lineStart; 335 | const lineLeft = line.text.slice(0, lineChangeOffset + totalInserted); 336 | const lineRight = line.text.slice(lineChangeOffset + totalInserted); 337 | const lineTagNameLeft = lineLeft.match(tagNameReLeft); 338 | const lineTagNameRight = lineRight.match(tagNameRERight); 339 | const previousTextRight = previousText.slice(change.rangeOffset); 340 | const previousTagNameRight = previousTextRight.match(tagNameRERight); 341 | let newWord: string; 342 | let oldWord: string; 343 | if (!lineTagNameLeft) { 344 | totalInserted += change.text.length - change.rangeLength; 345 | continue; 346 | } 347 | newWord = lineTagNameLeft[0]; 348 | oldWord = lineTagNameLeft[0]; 349 | if (lineTagNameRight) { 350 | newWord += lineTagNameRight[0]; 351 | } 352 | if (previousTagNameRight) { 353 | oldWord += previousTagNameRight[0]; 354 | } 355 | const offset = 356 | change.rangeOffset - lineTagNameLeft[0].length + totalInserted; 357 | tags.push({ 358 | oldWord, 359 | word: newWord, 360 | offset, 361 | previousOffset: offset - totalInserted 362 | }); 363 | totalInserted += change.text.length - change.rangeLength; 364 | } 365 | updateWordsAtOffset(tags); 366 | if (tags.length === 0) { 367 | previousText = currentText; 368 | return; 369 | } 370 | assertDefined(vscode.window.activeTextEditor); 371 | previousText = currentText; 372 | doAutoCompletionElementRenameTag(languageClientProxy, tags); 373 | }); 374 | }; 375 | setPreviousText(vscode.window.activeTextEditor); 376 | setupChangeListener(); 377 | context.subscriptions.push( 378 | vscode.window.onDidChangeActiveTextEditor(textEditor => { 379 | activeTextEditor = textEditor; 380 | const doument = activeTextEditor?.document; 381 | if (!isEnabled(doument)) { 382 | if (changeListener) { 383 | changeListener.dispose(); 384 | changeListener = undefined; 385 | } 386 | return; 387 | } 388 | setPreviousText(textEditor); 389 | setupChangeListener(); 390 | }) 391 | ); 392 | }; 393 | -------------------------------------------------------------------------------- /packages/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-noncomposite-base", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "types": ["node"], 7 | "noUnusedLocals": false, 8 | "noUnusedParameters": false 9 | }, 10 | "include": ["src"], 11 | "references": [] 12 | } 13 | -------------------------------------------------------------------------------- /packages/server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0-dev", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "server", 9 | "version": "1.0.0-dev", 10 | "dependencies": { 11 | "@babel/code-frame": "^7.16.7", 12 | "source-map-support": "^0.5.21", 13 | "typescript": "^4.6.3", 14 | "vscode-languageserver": "^7.0.0", 15 | "vscode-languageserver-textdocument": "^1.0.4" 16 | }, 17 | "devDependencies": { 18 | "@types/babel__code-frame": "^7.0.3" 19 | } 20 | }, 21 | "../service": { 22 | "version": "1.0.0-dev", 23 | "extraneous": true, 24 | "devDependencies": { 25 | "typescript": "^4.5.5" 26 | } 27 | }, 28 | "node_modules/@babel/code-frame": { 29 | "version": "7.16.7", 30 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", 31 | "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", 32 | "dependencies": { 33 | "@babel/highlight": "^7.16.7" 34 | }, 35 | "engines": { 36 | "node": ">=6.9.0" 37 | } 38 | }, 39 | "node_modules/@babel/helper-validator-identifier": { 40 | "version": "7.16.7", 41 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", 42 | "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", 43 | "engines": { 44 | "node": ">=6.9.0" 45 | } 46 | }, 47 | "node_modules/@babel/highlight": { 48 | "version": "7.16.10", 49 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", 50 | "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", 51 | "dependencies": { 52 | "@babel/helper-validator-identifier": "^7.16.7", 53 | "chalk": "^2.0.0", 54 | "js-tokens": "^4.0.0" 55 | }, 56 | "engines": { 57 | "node": ">=6.9.0" 58 | } 59 | }, 60 | "node_modules/@types/babel__code-frame": { 61 | "version": "7.0.3", 62 | "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.3.tgz", 63 | "integrity": "sha512-2TN6oiwtNjOezilFVl77zwdNPwQWaDBBCCWWxyo1ctiO3vAtd7H/aB/CBJdw9+kqq3+latD0SXoedIuHySSZWw==", 64 | "dev": true 65 | }, 66 | "node_modules/ansi-styles": { 67 | "version": "3.2.1", 68 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 69 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 70 | "dependencies": { 71 | "color-convert": "^1.9.0" 72 | }, 73 | "engines": { 74 | "node": ">=4" 75 | } 76 | }, 77 | "node_modules/buffer-from": { 78 | "version": "1.1.1", 79 | "license": "MIT" 80 | }, 81 | "node_modules/chalk": { 82 | "version": "2.4.2", 83 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 84 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 85 | "dependencies": { 86 | "ansi-styles": "^3.2.1", 87 | "escape-string-regexp": "^1.0.5", 88 | "supports-color": "^5.3.0" 89 | }, 90 | "engines": { 91 | "node": ">=4" 92 | } 93 | }, 94 | "node_modules/color-convert": { 95 | "version": "1.9.3", 96 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 97 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 98 | "dependencies": { 99 | "color-name": "1.1.3" 100 | } 101 | }, 102 | "node_modules/color-name": { 103 | "version": "1.1.3", 104 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 105 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 106 | }, 107 | "node_modules/escape-string-regexp": { 108 | "version": "1.0.5", 109 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 110 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 111 | "engines": { 112 | "node": ">=0.8.0" 113 | } 114 | }, 115 | "node_modules/has-flag": { 116 | "version": "3.0.0", 117 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 118 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 119 | "engines": { 120 | "node": ">=4" 121 | } 122 | }, 123 | "node_modules/js-tokens": { 124 | "version": "4.0.0", 125 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 126 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 127 | }, 128 | "node_modules/source-map": { 129 | "version": "0.6.1", 130 | "license": "BSD-3-Clause", 131 | "engines": { 132 | "node": ">=0.10.0" 133 | } 134 | }, 135 | "node_modules/source-map-support": { 136 | "version": "0.5.21", 137 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 138 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 139 | "dependencies": { 140 | "buffer-from": "^1.0.0", 141 | "source-map": "^0.6.0" 142 | } 143 | }, 144 | "node_modules/supports-color": { 145 | "version": "5.5.0", 146 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 147 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 148 | "dependencies": { 149 | "has-flag": "^3.0.0" 150 | }, 151 | "engines": { 152 | "node": ">=4" 153 | } 154 | }, 155 | "node_modules/typescript": { 156 | "version": "4.6.3", 157 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", 158 | "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", 159 | "bin": { 160 | "tsc": "bin/tsc", 161 | "tsserver": "bin/tsserver" 162 | }, 163 | "engines": { 164 | "node": ">=4.2.0" 165 | } 166 | }, 167 | "node_modules/vscode-jsonrpc": { 168 | "version": "6.0.0", 169 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", 170 | "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", 171 | "engines": { 172 | "node": ">=8.0.0 || >=10.0.0" 173 | } 174 | }, 175 | "node_modules/vscode-languageserver": { 176 | "version": "7.0.0", 177 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", 178 | "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", 179 | "dependencies": { 180 | "vscode-languageserver-protocol": "3.16.0" 181 | }, 182 | "bin": { 183 | "installServerIntoExtension": "bin/installServerIntoExtension" 184 | } 185 | }, 186 | "node_modules/vscode-languageserver-protocol": { 187 | "version": "3.16.0", 188 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", 189 | "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", 190 | "dependencies": { 191 | "vscode-jsonrpc": "6.0.0", 192 | "vscode-languageserver-types": "3.16.0" 193 | } 194 | }, 195 | "node_modules/vscode-languageserver-textdocument": { 196 | "version": "1.0.4", 197 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz", 198 | "integrity": "sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==" 199 | }, 200 | "node_modules/vscode-languageserver-types": { 201 | "version": "3.16.0", 202 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", 203 | "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" 204 | } 205 | }, 206 | "dependencies": { 207 | "@babel/code-frame": { 208 | "version": "7.16.7", 209 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", 210 | "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", 211 | "requires": { 212 | "@babel/highlight": "^7.16.7" 213 | } 214 | }, 215 | "@babel/helper-validator-identifier": { 216 | "version": "7.16.7", 217 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", 218 | "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==" 219 | }, 220 | "@babel/highlight": { 221 | "version": "7.16.10", 222 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", 223 | "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", 224 | "requires": { 225 | "@babel/helper-validator-identifier": "^7.16.7", 226 | "chalk": "^2.0.0", 227 | "js-tokens": "^4.0.0" 228 | } 229 | }, 230 | "@types/babel__code-frame": { 231 | "version": "7.0.3", 232 | "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.3.tgz", 233 | "integrity": "sha512-2TN6oiwtNjOezilFVl77zwdNPwQWaDBBCCWWxyo1ctiO3vAtd7H/aB/CBJdw9+kqq3+latD0SXoedIuHySSZWw==", 234 | "dev": true 235 | }, 236 | "ansi-styles": { 237 | "version": "3.2.1", 238 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 239 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 240 | "requires": { 241 | "color-convert": "^1.9.0" 242 | } 243 | }, 244 | "buffer-from": { 245 | "version": "1.1.1" 246 | }, 247 | "chalk": { 248 | "version": "2.4.2", 249 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 250 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 251 | "requires": { 252 | "ansi-styles": "^3.2.1", 253 | "escape-string-regexp": "^1.0.5", 254 | "supports-color": "^5.3.0" 255 | } 256 | }, 257 | "color-convert": { 258 | "version": "1.9.3", 259 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 260 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 261 | "requires": { 262 | "color-name": "1.1.3" 263 | } 264 | }, 265 | "color-name": { 266 | "version": "1.1.3", 267 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 268 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 269 | }, 270 | "escape-string-regexp": { 271 | "version": "1.0.5", 272 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 273 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 274 | }, 275 | "has-flag": { 276 | "version": "3.0.0", 277 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 278 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 279 | }, 280 | "js-tokens": { 281 | "version": "4.0.0", 282 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 283 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 284 | }, 285 | "source-map": { 286 | "version": "0.6.1" 287 | }, 288 | "source-map-support": { 289 | "version": "0.5.21", 290 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 291 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 292 | "requires": { 293 | "buffer-from": "^1.0.0", 294 | "source-map": "^0.6.0" 295 | } 296 | }, 297 | "supports-color": { 298 | "version": "5.5.0", 299 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 300 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 301 | "requires": { 302 | "has-flag": "^3.0.0" 303 | } 304 | }, 305 | "typescript": { 306 | "version": "4.6.3", 307 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", 308 | "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==" 309 | }, 310 | "vscode-jsonrpc": { 311 | "version": "6.0.0", 312 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", 313 | "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==" 314 | }, 315 | "vscode-languageserver": { 316 | "version": "7.0.0", 317 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", 318 | "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", 319 | "requires": { 320 | "vscode-languageserver-protocol": "3.16.0" 321 | } 322 | }, 323 | "vscode-languageserver-protocol": { 324 | "version": "3.16.0", 325 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", 326 | "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", 327 | "requires": { 328 | "vscode-jsonrpc": "6.0.0", 329 | "vscode-languageserver-types": "3.16.0" 330 | } 331 | }, 332 | "vscode-languageserver-textdocument": { 333 | "version": "1.0.4", 334 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz", 335 | "integrity": "sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==" 336 | }, 337 | "vscode-languageserver-types": { 338 | "version": "3.16.0", 339 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", 340 | "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0-dev", 4 | "main": "dist/serverMain.js", 5 | "scripts": { 6 | "dev": "tsc -b -w" 7 | }, 8 | "dependencies": { 9 | "@babel/code-frame": "^7.16.7", 10 | "service": "^1.0.0-dev", 11 | "source-map-support": "^0.5.21", 12 | "typescript": "^4.6.3", 13 | "vscode-languageserver": "^7.0.0", 14 | "vscode-languageserver-textdocument": "^1.0.4" 15 | }, 16 | "devDependencies": { 17 | "@types/babel__code-frame": "^7.0.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/src/autoRenameTag.ts: -------------------------------------------------------------------------------- 1 | import { doAutoRenameTag } from 'service'; 2 | import { 3 | RequestType, 4 | // TODO 5 | TextDocuments, 6 | VersionedTextDocumentIdentifier 7 | } from 'vscode-languageserver'; 8 | import { TextDocument } from 'vscode-languageserver-textdocument'; 9 | 10 | interface Tag { 11 | readonly word: string; 12 | readonly oldWord: string; 13 | readonly offset: number; 14 | } 15 | interface Params { 16 | readonly textDocument: VersionedTextDocumentIdentifier; 17 | readonly tags: Tag[]; 18 | } 19 | 20 | interface Result { 21 | readonly startOffset: number; 22 | readonly endOffset: number; 23 | readonly tagName: string; 24 | readonly originalWord: string; 25 | readonly originalOffset: number; 26 | } 27 | 28 | export const autoRenameTagRequestType = new RequestType( 29 | '$/auto-rename-tag' 30 | ); 31 | 32 | const NULL_AUTO_RENAME_TAG_RESULT: Result[] = []; 33 | 34 | export const autoRenameTag: ( 35 | documents: TextDocuments 36 | ) => (params: Params) => Promise = 37 | documents => 38 | async ({ textDocument, tags }) => { 39 | await new Promise(r => setTimeout(r, 20)); 40 | const document = documents.get(textDocument.uri); 41 | if (!document) { 42 | return NULL_AUTO_RENAME_TAG_RESULT; 43 | } 44 | if (textDocument.version !== document.version) { 45 | return NULL_AUTO_RENAME_TAG_RESULT; 46 | } 47 | const text = document.getText(); 48 | const results: Result[] = tags 49 | .map(tag => { 50 | const result = doAutoRenameTag( 51 | text, 52 | tag.offset, 53 | tag.word, 54 | tag.oldWord, 55 | document.languageId 56 | ); 57 | if (!result) { 58 | return result; 59 | } 60 | (result as any).originalOffset = tag.offset; 61 | (result as any).originalWord = tag.word; 62 | return result as Result; 63 | }) 64 | .filter(Boolean) as Result[]; 65 | return results; 66 | }; 67 | -------------------------------------------------------------------------------- /packages/server/src/errorHandlingAndLogging.ts: -------------------------------------------------------------------------------- 1 | import { codeFrameColumns } from '@babel/code-frame'; 2 | import * as fs from 'fs'; 3 | import 'source-map-support/register'; 4 | import { Connection } from 'vscode-languageserver'; 5 | 6 | export const handleError: (error: Error) => void = error => { 7 | console.error(error.stack); 8 | const lines = error.stack?.split('\n') || []; 9 | let file = lines[1]; 10 | if (file) { 11 | let match = file.match(/\((.*):(\d+):(\d+)\)$/); 12 | if (!match) { 13 | match = file.match(/at (.*):(\d+):(\d+)$/); 14 | } 15 | if (match) { 16 | const [_, path, line, column] = match; 17 | const rawLines = fs.readFileSync(path, 'utf-8'); 18 | const location = { 19 | start: { 20 | line: parseInt(line), 21 | column: parseInt(column) 22 | } 23 | }; 24 | 25 | const result = codeFrameColumns(rawLines, location); 26 | console.log('\n' + result + '\n'); 27 | } 28 | } 29 | let relevantStack = (error as Error).stack?.split('\n').slice(1).join('\n'); 30 | if (relevantStack?.includes('at CallbackList.invoke')) { 31 | relevantStack = relevantStack.slice( 32 | 0, 33 | relevantStack.indexOf('at CallbackList.invoke') 34 | ); 35 | } 36 | console.log(relevantStack); 37 | }; 38 | 39 | const useConnectionConsole: ( 40 | connection: Connection, 41 | { trace }: { trace?: boolean } 42 | ) => (method: 'log' | 'info' | 'error') => (...args: any[]) => void = 43 | (connection, { trace = false } = {}) => 44 | method => 45 | (...args) => { 46 | if (trace) { 47 | const stack = new Error().stack || ''; 48 | let file = stack.split('\n')[2]; 49 | file = file.slice(file.indexOf('at') + 'at'.length, -1); 50 | const match = file.match(/(.*):(\d+):(\d+)$/); 51 | if (match) { 52 | const [_, path, line, column] = match; 53 | connection.console[method]('at ' + path + ':' + line); 54 | } 55 | } 56 | const stringify: (arg: any) => string = arg => { 57 | if (arg && arg.toString) { 58 | if (arg.toString() === '[object Promise]') { 59 | return JSON.stringify(arg); 60 | } 61 | if (arg.toString() === '[object Object]') { 62 | return JSON.stringify(arg); 63 | } 64 | return arg; 65 | } 66 | return JSON.stringify(arg); 67 | }; 68 | connection.console[method](args.map(stringify).join('')); 69 | }; 70 | 71 | /** 72 | * Enables better stack traces for errors and logging. 73 | */ 74 | export const enableBetterErrorHandlingAndLogging: ( 75 | connection: Connection 76 | ) => void = connection => { 77 | const connectionConsole = useConnectionConsole(connection, { trace: false }); 78 | console.log = connectionConsole('log'); 79 | console.info = connectionConsole('info'); 80 | console.error = connectionConsole('error'); 81 | process.on('uncaughtException', handleError); 82 | process.on('unhandledRejection', handleError); 83 | }; 84 | -------------------------------------------------------------------------------- /packages/server/src/serverMain.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from 'vscode-languageserver-textdocument'; 2 | import { 3 | createConnection, 4 | TextDocuments, 5 | TextDocumentSyncKind 6 | } from 'vscode-languageserver/node'; 7 | import { autoRenameTag, autoRenameTagRequestType } from './autoRenameTag'; 8 | import { 9 | enableBetterErrorHandlingAndLogging, 10 | handleError 11 | } from './errorHandlingAndLogging'; 12 | 13 | const connection = createConnection(); 14 | const documents = new TextDocuments(TextDocument); 15 | 16 | enableBetterErrorHandlingAndLogging(connection); 17 | 18 | connection.onInitialize(() => ({ 19 | capabilities: { 20 | textDocumentSync: TextDocumentSyncKind.Incremental 21 | } 22 | })); 23 | 24 | connection.onInitialized(() => { 25 | console.log('Auto Rename Tag has been initialized.'); 26 | }); 27 | 28 | const handleRequest: ( 29 | fn: (params: Params) => Result 30 | ) => (params: Params) => Result = fn => params => { 31 | try { 32 | return fn(params); 33 | } catch (error) { 34 | handleError(error); 35 | throw error; 36 | } 37 | }; 38 | 39 | connection.onRequest( 40 | autoRenameTagRequestType, 41 | handleRequest(autoRenameTag(documents)) 42 | ); 43 | 44 | documents.listen(connection); 45 | connection.listen(); 46 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-noncomposite-base", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "types": ["node"], 7 | "allowUnusedLabels": true, 8 | "allowUnreachableCode": true, 9 | "noUnusedParameters": false, 10 | "noUnusedLocals": false 11 | }, 12 | "include": ["src"], 13 | "references": [ 14 | { 15 | "path": "../service/tsconfig.json" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/service/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service", 3 | "version": "1.0.0-dev", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "service", 9 | "version": "1.0.0-dev", 10 | "devDependencies": { 11 | "typescript": "^4.6.3" 12 | } 13 | }, 14 | "node_modules/typescript": { 15 | "version": "4.6.3", 16 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", 17 | "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", 18 | "dev": true, 19 | "bin": { 20 | "tsc": "bin/tsc", 21 | "tsserver": "bin/tsserver" 22 | }, 23 | "engines": { 24 | "node": ">=4.2.0" 25 | } 26 | } 27 | }, 28 | "dependencies": { 29 | "typescript": { 30 | "version": "4.6.3", 31 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", 32 | "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", 33 | "dev": true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service", 3 | "version": "1.0.0-dev", 4 | "main": "dist/serviceMain.js", 5 | "description": "", 6 | "sideEffects": false, 7 | "scripts": { 8 | "dev": "tsc -b -w", 9 | "test": "jest" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^4.6.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/service/src/benchmark/doAutoRenameTagBenchmark.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { doAutoRenameTag } from '../doAutoRenameTag'; 4 | import { isSelfClosingTagInLanguage } from '../isSelfClosingTag'; 5 | import { getMatchingTagPairs } from '../getMatchingTagPairs'; 6 | 7 | const file = fs 8 | .readFileSync(path.join(__dirname, '../../src/benchmark/file.txt')) 9 | .toString(); 10 | 11 | const measure: any = (name: string, fn: any, runs: number) => { 12 | const NS_PER_MS = 1e6; 13 | const NS_PER_SEC = 1e9; 14 | const start = process.hrtime(); 15 | for (let i = 0; i < runs; i++) { 16 | fn(); 17 | } 18 | console.log(runs + ' runs'); 19 | const elapsedTime = process.hrtime(start); 20 | const elapsedTimeMs = 21 | (elapsedTime[0] * NS_PER_SEC + elapsedTime[1]) / NS_PER_MS / runs; 22 | console.log(name + ' took ' + elapsedTimeMs + 'ms'); 23 | }; 24 | 25 | measure( 26 | 'rename', 27 | () => { 28 | doAutoRenameTag(file, 0, ' { 36 | const whitespaceSet = new Set([' ', '\n', '\t', '\r', '\f']); 37 | let whitespaceCount = 0; 38 | for (let i = 0; i < file.length; i++) { 39 | const j = file[i]; 40 | if (whitespaceSet.has(j)) { 41 | whitespaceCount++; 42 | } 43 | } 44 | }, 45 | 10 46 | ); //? 47 | -------------------------------------------------------------------------------- /packages/service/src/doAutoRenameTag.ts: -------------------------------------------------------------------------------- 1 | import { getMatchingTagPairs } from './getMatchingTagPairs'; 2 | import { 3 | createScannerFast, 4 | ScannerStateFast 5 | } from './htmlScanner/htmlScannerFast'; 6 | import { isSelfClosingTagInLanguage } from './isSelfClosingTag'; 7 | import { getNextClosingTagName } from './util/getNextClosingTagName'; 8 | import { getPreviousOpeningTagName } from './util/getPreviousOpenTagName'; 9 | 10 | export const doAutoRenameTag: ( 11 | text: string, 12 | offset: number, 13 | newWord: string, 14 | oldWord: string, 15 | languageId: string 16 | ) => 17 | | { 18 | startOffset: number; 19 | endOffset: number; 20 | tagName: string; 21 | } 22 | | undefined = (text, offset, newWord, oldWord, languageId) => { 23 | const matchingTagPairs = getMatchingTagPairs(languageId); 24 | const isSelfClosingTag = isSelfClosingTagInLanguage(languageId); 25 | const isReact = 26 | languageId === 'javascript' || 27 | languageId === 'typescript' || 28 | languageId === 'javascriptreact' || 29 | languageId === 'typescriptreact'; 30 | const scanner = createScannerFast({ 31 | input: text, 32 | initialOffset: 0, 33 | initialState: ScannerStateFast.WithinContent, 34 | matchingTagPairs 35 | }); 36 | if (newWord.startsWith(''], 91 | true, 92 | isReact 93 | ); 94 | if (!hasAdvanced) { 95 | return undefined; 96 | } 97 | const match = text 98 | .slice(scanner.stream.position) 99 | .match(new RegExp(`'], 112 | true, 113 | isReact 114 | ); 115 | // if start tag is not closed, return undefined 116 | if (scanner.stream.peekRight(0) === '<') { 117 | return undefined; 118 | } 119 | if (!hasAdvanced) { 120 | return undefined; 121 | } 122 | if (scanner.stream.peekLeft(1) === '/') { 123 | return undefined; 124 | } 125 | const possibleEndOfStartTag = scanner.stream.position; 126 | // check if we might be at an end tag 127 | while (scanner.stream.peekLeft(1).match(/[a-zA-Z\-\:]/)) { 128 | scanner.stream.goBack(1); 129 | if (scanner.stream.peekLeft(1) === '/') { 130 | return undefined; 131 | } 132 | } 133 | scanner.stream.goTo(possibleEndOfStartTag); 134 | scanner.stream.advance(1); 135 | const nextClosingTag = getNextClosingTagName( 136 | scanner, 137 | scanner.stream.position, 138 | isSelfClosingTag, 139 | isReact 140 | ); 141 | if (!nextClosingTag) { 142 | return undefined; 143 | } 144 | if (nextClosingTag.tagName === tagName) { 145 | return undefined; 146 | } 147 | if (nextClosingTag.tagName !== oldTagName) { 148 | return undefined; 149 | } 150 | const previousOpenTag = getPreviousOpeningTagName( 151 | scanner, 152 | offset, 153 | isSelfClosingTag, 154 | isReact 155 | ); 156 | 157 | if ( 158 | previousOpenTag && 159 | previousOpenTag.tagName === oldTagName && 160 | previousOpenTag.indent === nextClosingTag.indent 161 | ) { 162 | return undefined; 163 | } 164 | 165 | const startOffset = nextClosingTag.offset; 166 | const endOffset = nextClosingTag.offset + nextClosingTag.tagName.length; 167 | 168 | return { 169 | startOffset, 170 | endOffset, 171 | tagName 172 | }; 173 | } 174 | }; 175 | 176 | // const testCase = { 177 | // text: '
      \n \n
      \n
      ', 178 | // offset: 8, 179 | // newWord: ' 192 | //
      193 | //
      194 | //
      `, 195 | // 9, 196 | // '']], 8 | ruby: [ 9 | ['<%=', '%>'], 10 | ['"', '"'], 11 | ["'", "'"] 12 | ], 13 | html: [ 14 | [''], 15 | ['"', '"'], 16 | ["'", "'"], 17 | [''], 18 | [''] // support for html-webpack-plugin 20 | ], 21 | markdown: [ 22 | [''], 23 | ['"', '"'], 24 | ["'", "'"], 25 | ['```', '```'], 26 | [''] 27 | ], 28 | marko: [ 29 | [''], 30 | ['${', '}'], 31 | ['', ''] 32 | ], 33 | nunjucks: [ 34 | ['{%', '%}'], 35 | ['{{', '}}'], 36 | ['{#', '#}'] 37 | ], 38 | plaintext: [ 39 | [''], 40 | ['"', '"'], 41 | ["'", "'"] 42 | ], 43 | php: [ 44 | [''], 45 | [''], 46 | ['"', '"'], 47 | ["'", "'"] 48 | ], 49 | javascript: [ 50 | [''], 51 | ['{/*', '*/}'], 52 | ["'", "'"], 53 | ['"', '"'], 54 | ['`', '`'] 55 | ], 56 | javascriptreact: [ 57 | ['{/*', '*/}'], 58 | ["'", "'"], 59 | ['"', '"'], 60 | ['`', '`'] 61 | ], 62 | mustache: [['{{', '}}']], 63 | razor: [ 64 | [''], 65 | ['@{', '}'], 66 | ['"', '"'], 67 | ["'", "'"] 68 | ], 69 | svelte: [ 70 | [''], 71 | ['"', '"'], 72 | ["'", "'"] 73 | ], 74 | svg: [ 75 | [''], 76 | ['"', '"'], 77 | ["'", "'"] 78 | ], 79 | typescript: [ 80 | [''], 81 | ['{/*', '*/}'], 82 | ["'", "'"], 83 | ['"', '"'], 84 | ['`', '`'] 85 | ], 86 | typescriptreact: [ 87 | ['{/*', '*/}'], 88 | ["'", "'"], 89 | ['"', '"'], 90 | ['`', '`'] 91 | ], 92 | twig: [ 93 | [''], 94 | ['"', '"'], 95 | ["'", "'"], 96 | ['{{', '}}'], 97 | ['{%', '%}'] 98 | ], 99 | volt: [ 100 | ['{#', '#}'], 101 | ['{%', '%}'], 102 | ['{{', '}}'] 103 | ], 104 | vue: [ 105 | [''], 106 | ['"', '"'], 107 | ["'", "'"], 108 | ['{{', '}}'] 109 | ], 110 | xml: [ 111 | [''], 112 | ['"', '"'], 113 | ["'", "'"], 114 | [''] 115 | ] 116 | }; 117 | 118 | export const getMatchingTagPairs: ( 119 | languageId: string 120 | ) => [string, string][] = languageId => 121 | matchingTagPairs[languageId] || matchingTagPairs['html']; 122 | -------------------------------------------------------------------------------- /packages/service/src/htmlScanner/MultiLineStream.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For these chars code can be ambiguous, e.g. 3 | * `
      "` 4 | * Here, the button start tag can be interpreted as a class 5 | * or the closing div tag can be interpreted as a class. 6 | * In case of quotes we always skip. 7 | * 8 | * This is in contrast to chars that cannot be skipped, e.g. when 9 | * going forward and encountering `-->` we cannot go further 10 | * because we would go outside of the comment but when going 11 | * forward and encountering `"` we can go forward until 12 | * the next quote. 13 | * 14 | */ 15 | const quotes = new Set(['`', '"', "'"]); 16 | 17 | // const whitespaceSet = new Set([' ', '\n', '\t', '\f', '\r']); 18 | // const isWhitespace: (char: string) => boolean = char => whitespaceSet.has(char); 19 | 20 | export class MultiLineStream { 21 | public position: number; 22 | 23 | private source: string; 24 | 25 | private length: number; 26 | private matchingTagPairs: readonly [string, string][]; 27 | private nonQuoteMatchingTagPairs: readonly [string, string][]; 28 | 29 | constructor( 30 | source: string, 31 | position: number, 32 | matchingTagPairs: readonly [string, string][] 33 | ) { 34 | this.source = source; 35 | this.length = source.length; 36 | this.position = position; 37 | this.matchingTagPairs = matchingTagPairs; 38 | this.nonQuoteMatchingTagPairs = matchingTagPairs.filter( 39 | matchingTagPair => !quotes.has(matchingTagPair[0]) 40 | ); 41 | } 42 | 43 | public eos(): boolean { 44 | return this.length <= this.position; 45 | } 46 | 47 | public getSource(): string { 48 | return this.source; 49 | } 50 | 51 | public goTo(position: number): void { 52 | this.position = position; 53 | } 54 | 55 | public goBack(n: number): void { 56 | this.position -= n; 57 | } 58 | 59 | public advance(n: number): void { 60 | this.position += n; 61 | } 62 | 63 | private goToEnd(): void { 64 | this.position = this.source.length; 65 | } 66 | 67 | // TODO 68 | // public raceBackUntilChars(firstChar: string, secondChar: string): string { 69 | // this.position--; 70 | // while ( 71 | // this.position >= 0 && 72 | // this.source[this.position] !== firstChar && 73 | // this.source[this.position] !== secondChar 74 | // ) { 75 | // this.position--; 76 | // } 77 | // this.position++; 78 | // if (this.position === 0) { 79 | // return ''; 80 | // } 81 | // return this.source[this.position - 1]; 82 | // } 83 | 84 | private goBackToUntilChars(chars: string): void { 85 | const reversedChars = chars.split('').reverse().join(''); 86 | outer: while (this.position >= 0) { 87 | for (let i = 0; i < reversedChars.length; i++) { 88 | if (this.source[this.position - i] !== reversedChars[i]) { 89 | this.position--; 90 | continue outer; 91 | } 92 | } 93 | break; 94 | } 95 | this.position++; 96 | } 97 | 98 | public goBackUntilEitherChar( 99 | chars: string[], 100 | skipQuotes: boolean, 101 | isReact: boolean 102 | ): boolean { 103 | const specialCharSet = new Set([...(isReact ? ['{', '}'] : [])]); 104 | while (this.position >= 0) { 105 | if (isReact) { 106 | if (specialCharSet.has(this.source[this.position])) { 107 | if (this.source[this.position] === '{') { 108 | return false; 109 | } 110 | if (this.source[this.position] === '}') { 111 | let stackSize = 1; 112 | while (--this.position > 0) { 113 | if (this.source[this.position] === '}') { 114 | stackSize++; 115 | } else if (this.source[this.position] === '{') { 116 | stackSize--; 117 | if (stackSize === 0) { 118 | break; 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | // don't go outside of matching tag pairs, e.g. don't go before `` 126 | outerForLoop1: for (const matchingTagPair of this 127 | .nonQuoteMatchingTagPairs) { 128 | for (let j = 0; j < matchingTagPair[0].length; j++) { 129 | if ( 130 | matchingTagPair[0][matchingTagPair[0].length - 1 - j] !== 131 | this.source[this.position - j] 132 | ) { 133 | continue outerForLoop1; 134 | } 135 | } 136 | return false; 137 | } 138 | // skip matching tag pairs, e.g. skip '' in '' 139 | outerForLoop2: for (const matchingTagPair of this.matchingTagPairs) { 140 | for (let i = 0; i < matchingTagPair[1].length; i++) { 141 | if ( 142 | matchingTagPair[1][matchingTagPair[1].length - 1 - i] !== 143 | this.source[this.position - i] 144 | ) { 145 | continue outerForLoop2; 146 | } 147 | } 148 | if (quotes.has(matchingTagPair[0])) { 149 | if (!skipQuotes) { 150 | this.goBack(1); 151 | return this.goBackUntilEitherChar(chars, skipQuotes, isReact); 152 | } 153 | } 154 | this.goBack(matchingTagPair[1].length); // e.g. go before `-->` 155 | this.goBackToUntilChars(matchingTagPair[0]); // e.g. go back until `` in `` 202 | outerForLoop1: for (const matchingTagPair of this 203 | .nonQuoteMatchingTagPairs) { 204 | for (let j = 0; j < matchingTagPair[1].length; j++) { 205 | if (matchingTagPair[1][j] !== this.source[this.position + j]) { 206 | continue outerForLoop1; 207 | } 208 | } 209 | return false; 210 | } 211 | 212 | // skip matching tag pairs, e.g. skip '' in '' 213 | outerForLoop2: for (const matchingTagPair of this.matchingTagPairs) { 214 | for (let i = 0; i < matchingTagPair[0].length; i++) { 215 | if (matchingTagPair[0][i] !== this.source[this.position + i]) { 216 | continue outerForLoop2; 217 | } 218 | } 219 | if (quotes.has(matchingTagPair[0])) { 220 | if (!skipQuotes) { 221 | this.advance(1); 222 | return this.advanceUntilEitherChar(chars, skipQuotes, isReact); 223 | } 224 | } 225 | this.advance(matchingTagPair[0].length); // e.g. advance until after `` 227 | this.advance(matchingTagPair[1].length); // e.g. advance until after `-->` 228 | return this.advanceUntilEitherChar(chars, skipQuotes, isReact); 229 | } 230 | if (chars.includes(this.source[this.position])) { 231 | return true; 232 | } 233 | this.position++; 234 | } 235 | return false; 236 | } 237 | 238 | public peekLeft(n: number = 0): string { 239 | return this.source[this.position - n]; 240 | } 241 | 242 | public previousChars(n: number): string { 243 | return this.source.slice(this.position - n, this.position); 244 | } 245 | 246 | public peekRight(n: number = 0): string { 247 | return this.source[this.position + n] || ''; 248 | } 249 | 250 | public advanceIfRegExp(regex: RegExp): string | undefined { 251 | const str = this.source.substr(this.position); 252 | const match = str.match(regex); 253 | if (match) { 254 | this.position = this.position + match.index! + match[0].length; 255 | return match[0]; 256 | } 257 | return undefined; 258 | } 259 | 260 | private advanceUntilChars(ch: string): boolean { 261 | while (this.position + ch.length <= this.source.length) { 262 | let i = 0; 263 | while (i < ch.length && this.source[this.position + i] === ch[i]) { 264 | i++; 265 | } 266 | if (i === ch.length) { 267 | return true; 268 | } 269 | this.advance(1); 270 | } 271 | this.goToEnd(); 272 | return false; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /packages/service/src/htmlScanner/htmlScannerFast.ts: -------------------------------------------------------------------------------- 1 | import { MultiLineStream } from './MultiLineStream'; 2 | 3 | /** 4 | * HTML tag name (explaining the regex) 5 | * 6 | * This regex is for the name of the html tag 7 | * E.g. we want to match "div" inside "
      " 8 | * 9 | * ^ ### start 10 | * [:\w] ### ":" or character or digit 11 | * ((?![>\/])[\S]) ### everything except closing brackets 12 | */ 13 | const htmlTagNameRE = /^[!:\w\$]((?![>\/])[\S])*/; 14 | 15 | /** 16 | * Empty html tag, e.g. `< >` 17 | */ 18 | const htmlTagNameEmptyRE = /^\s+/; 19 | 20 | /** 21 | * Html attribute name (explaining the regex) 22 | * 23 | * This regex is for html attribute names, 24 | * E.g. we want to match "class" in "
      " 25 | * 26 | * ^ ### start 27 | * [^\s"'>/=]* ### any anything that isn't whitespace, ", ', >, / or = 28 | */ 29 | const htmlAttributeNameRE = /^[^\s"'>/=]*/; 30 | 31 | /** 32 | * Html attribute value (explaining the regex) 33 | * 34 | * ^ ### start 35 | * [^\s"'`=<>/]+ ### no whitespace, double quotes, single quotes, back quotes, "=", "<", ">" and "/" 36 | */ 37 | const htmlAttributeValueRE = /^[^\s"'`=<>/]+/; 38 | 39 | export const enum TokenTypeFast { 40 | StartCommentTag, // "<--" part of "" 41 | Comment, // " this is a comment " part of "" 42 | EndCommentTag, // "-->" part of "" 43 | StartTagOpen, // "<" part of "" 44 | StartTagClose, // ">" part of "" 45 | StartTagSelfClose, // "/>" part of "" 46 | StartTag, // "input" part of "" 47 | EndTagOpen, // "<" part of "" 48 | EndTagClose, // ">" part of "" 49 | EndTag, // "html" part of 50 | AttributeName, // "class" part of "
      " 51 | AttributeValue, // "center" part of "
      " 52 | Content, // "this is text" part of "

      this is text

      " 53 | EOS, // end of stream 54 | DelimiterAssign, // "=" part of "
      55 | Unknown, // anything that doesn't make sense, e.g. ";" in "i TokenTypeFast; 61 | readonly getTokenText: () => string; 62 | readonly stream: MultiLineStream; 63 | state: ScannerStateFast; 64 | } 65 | 66 | export const enum ScannerStateFast { 67 | WithinContent, 68 | AfterOpeningStartTag, 69 | AfterOpeningEndTag, 70 | WithinStartTag, 71 | WithinEndTag, 72 | WithinComment, 73 | AfterAttributeName, 74 | BeforeAttributeValue 75 | } 76 | 77 | export function createScannerFast({ 78 | input, 79 | initialOffset, 80 | initialState, 81 | matchingTagPairs 82 | }: { 83 | input: string; 84 | initialOffset: number; 85 | initialState: ScannerStateFast; 86 | matchingTagPairs: readonly [string, string][]; 87 | }): ScannerFast { 88 | const stream = new MultiLineStream(input, initialOffset, matchingTagPairs); 89 | let state: ScannerStateFast = initialState; 90 | let tokenOffset: number; 91 | /** 92 | * Whether or not a space is after the starting tag name. 93 | * E.g. "
      " but not "" and "
      " but not "
      "" 94 | * This is used to determine whether the following characters are attributes or just invalid 95 | */ 96 | 97 | function nextElementName(): string | undefined { 98 | let result = stream.advanceIfRegExp(htmlTagNameRE); 99 | if (result === undefined) { 100 | if (stream.advanceIfRegExp(htmlTagNameEmptyRE)) { 101 | result = ''; 102 | } 103 | } 104 | return result; 105 | } 106 | 107 | let lastTagName: string | undefined; 108 | // @ts-ignore 109 | function scan(): TokenTypeFast { 110 | tokenOffset = stream.position; 111 | if (stream.eos()) { 112 | return TokenTypeFast.EOS; 113 | } 114 | switch (state) { 115 | case ScannerStateFast.AfterOpeningEndTag: 116 | const tagName = nextElementName(); 117 | if (tagName) { 118 | state = ScannerStateFast.WithinEndTag; 119 | return TokenTypeFast.EndTag; 120 | } else if (stream.peekRight(0) === '>') { 121 | state = ScannerStateFast.WithinEndTag; 122 | return TokenTypeFast.EndTag; 123 | } 124 | return TokenTypeFast.Unknown; 125 | 126 | case ScannerStateFast.AfterOpeningStartTag: 127 | lastTagName = nextElementName(); 128 | if (lastTagName !== undefined) { 129 | if (lastTagName === '') { 130 | tokenOffset = stream.position; 131 | } 132 | state = ScannerStateFast.WithinStartTag; 133 | return TokenTypeFast.StartTag; 134 | } 135 | // this is a tag like "<>" 136 | if (stream.peekRight() === '>') { 137 | state = ScannerStateFast.WithinStartTag; 138 | return TokenTypeFast.StartTag; 139 | } 140 | // At this point there is no tag name sign after the opening tag "<" 141 | // E.g. "< div" 142 | // So we just assume that it is text 143 | state = ScannerStateFast.WithinContent; 144 | return scan(); 145 | default: 146 | break; 147 | } 148 | } 149 | 150 | return { 151 | scan, 152 | stream, 153 | getTokenText() { 154 | return stream.getSource().slice(tokenOffset, stream.position); 155 | }, 156 | set state(newState: any) { 157 | state = newState; 158 | } 159 | }; 160 | } 161 | -------------------------------------------------------------------------------- /packages/service/src/isSelfClosingTag.ts: -------------------------------------------------------------------------------- 1 | const tagsThatAreSelfClosingInHtml: Set = new Set([ 2 | 'area', 3 | 'base', 4 | 'br', 5 | 'col', 6 | 'command', 7 | 'embed', 8 | 'hr', 9 | 'img', 10 | 'input', 11 | 'keygen', 12 | 'link', 13 | 'menuitem', 14 | 'meta', 15 | 'param', 16 | 'source', 17 | 'track', 18 | 'wbr' 19 | ]); 20 | 21 | const EMPTY_SET: Set = new Set(); 22 | 23 | const tagsThatAreSelfClosing: { [languageId: string]: Set } = { 24 | css: tagsThatAreSelfClosingInHtml, 25 | ejs: tagsThatAreSelfClosingInHtml, 26 | ruby: tagsThatAreSelfClosingInHtml, 27 | html: tagsThatAreSelfClosingInHtml, 28 | markdown: tagsThatAreSelfClosingInHtml, 29 | marko: tagsThatAreSelfClosingInHtml, 30 | nunjucks: tagsThatAreSelfClosingInHtml, 31 | plaintext: tagsThatAreSelfClosingInHtml, 32 | php: tagsThatAreSelfClosingInHtml, 33 | javascript: tagsThatAreSelfClosingInHtml, 34 | javascriptreact: EMPTY_SET, 35 | mustache: tagsThatAreSelfClosingInHtml, 36 | razor: tagsThatAreSelfClosingInHtml, 37 | svelte: tagsThatAreSelfClosingInHtml, 38 | svg: EMPTY_SET, 39 | typescript: tagsThatAreSelfClosingInHtml, 40 | typescriptreact: EMPTY_SET, 41 | twig: tagsThatAreSelfClosingInHtml, 42 | volt: tagsThatAreSelfClosingInHtml, 43 | vue: EMPTY_SET, 44 | xml: EMPTY_SET 45 | }; 46 | 47 | export const isSelfClosingTagInLanguage: ( 48 | languageId: string 49 | ) => (tagName: string) => boolean = languageId => tagName => 50 | (tagsThatAreSelfClosing[languageId] || tagsThatAreSelfClosing['html']).has( 51 | tagName 52 | ); 53 | -------------------------------------------------------------------------------- /packages/service/src/serviceMain.ts: -------------------------------------------------------------------------------- 1 | export { doAutoRenameTag } from './doAutoRenameTag'; 2 | -------------------------------------------------------------------------------- /packages/service/src/util/getIndent.ts: -------------------------------------------------------------------------------- 1 | export const getIndent = (source: string, startOffset: number) => { 2 | let indent = 0; 3 | while (indent <= startOffset && source[startOffset - indent] !== '\n') { 4 | indent++; 5 | } 6 | return indent; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/service/src/util/getNextClosingTagName.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScannerFast, 3 | ScannerStateFast, 4 | TokenTypeFast, 5 | createScannerFast 6 | } from '../htmlScanner/htmlScannerFast'; 7 | import { getMatchingTagPairs } from '../getMatchingTagPairs'; 8 | import { getPreviousOpeningTagName } from './getPreviousOpenTagName'; 9 | import { getIndent } from './getIndent'; 10 | 11 | export const getNextClosingTagName: ( 12 | scanner: ScannerFast, 13 | initialOffset: number, 14 | isSelfClosingTag: (tagName: string) => boolean, 15 | isReact?: boolean 16 | ) => 17 | | { 18 | tagName: string; 19 | offset: number; 20 | seenRightAngleBracket: boolean; 21 | indent: number; 22 | } 23 | | undefined = (scanner, initialOffset, isSelfClosingTag, isReact = false) => { 24 | let offset = initialOffset; 25 | let nextClosingTagName: string | undefined; 26 | let stack: string[] = []; 27 | let seenRightAngleBracket = false; 28 | let i = 0; 29 | scanner.stream.goTo(offset); 30 | do { 31 | const hasFoundChar = scanner.stream.advanceUntilEitherChar( 32 | ['<', '>'], 33 | false, 34 | isReact 35 | ); 36 | if (!hasFoundChar) { 37 | return undefined; 38 | } 39 | const char = scanner.stream.peekRight(); 40 | if (!['<', '>'].includes(char)) { 41 | return undefined; 42 | } 43 | if (char === '<') { 44 | if (scanner.stream.peekRight(1) === '/') { 45 | scanner.stream.advance(2); 46 | offset = scanner.stream.position; 47 | scanner.state = ScannerStateFast.AfterOpeningEndTag; 48 | const token = scanner.scan(); 49 | if (token !== TokenTypeFast.EndTag) { 50 | return undefined; 51 | } 52 | const tokenText = scanner.getTokenText(); 53 | if (stack.length) { 54 | const top = stack.pop(); 55 | if (top !== tokenText) { 56 | // TODO 57 | // console.log(scanner.stream.position); 58 | // console.log(top); 59 | // console.log(tokenText); 60 | // console.error('no'); 61 | return undefined; 62 | } 63 | continue; 64 | } 65 | nextClosingTagName = tokenText; 66 | if (nextClosingTagName !== undefined) { 67 | break; 68 | } 69 | } 70 | 71 | scanner.stream.advance(1); 72 | scanner.state = ScannerStateFast.AfterOpeningStartTag; 73 | const token = scanner.scan(); 74 | if (token !== TokenTypeFast.StartTag) { 75 | return undefined; 76 | } 77 | const tokenText = scanner.getTokenText(); 78 | if (isSelfClosingTag(tokenText)) { 79 | scanner.stream.advanceUntilEitherChar(['>'], true, isReact); 80 | scanner.stream.advance(1); 81 | continue; 82 | } 83 | stack.push(tokenText); 84 | continue; 85 | } else { 86 | if (scanner.stream.peekRight(1) === '') { 87 | return undefined; 88 | } 89 | // don't go outside of comment when inside 90 | if (scanner.stream.previousChars(2) === '--') { 91 | return undefined; 92 | } 93 | if (scanner.stream.peekLeft(1) === '/') { 94 | const charBefore = scanner.stream.peekLeft(2); 95 | if (!/[\s"'\}]/.test(charBefore)) { 96 | const codeBefore = scanner.stream 97 | .getSource() 98 | .slice(0, scanner.stream.position); 99 | if (/href=[^\s]+$/.test(codeBefore)) { 100 | scanner.stream.advance(1); 101 | continue; 102 | } 103 | } 104 | if (stack.length === 0) { 105 | return undefined; 106 | } 107 | stack.pop(); 108 | scanner.stream.advance(1); 109 | continue; 110 | } 111 | scanner.stream.advance(1); 112 | } 113 | } while (true); 114 | 115 | const startOffset = offset; 116 | const indent = getIndent(scanner.stream.getSource(), startOffset - 3); 117 | 118 | return { 119 | tagName: nextClosingTagName, 120 | offset, 121 | seenRightAngleBracket, 122 | indent 123 | }; 124 | }; 125 | 126 | // getNextClosingTagName( 127 | // createScannerFast({ 128 | // input: `
      129 | //
      130 | //
      131 | //
      `, 132 | // initialOffset: 0, 133 | // initialState: ScannerStateFast.AfterOpeningEndTag, 134 | // matchingTagPairs: getMatchingTagPairs('javascriptreact') 135 | // }), 136 | // 12, 137 | // () => false, 138 | // true 139 | // ); //? 140 | -------------------------------------------------------------------------------- /packages/service/src/util/getPreviousOpenTagName.ts: -------------------------------------------------------------------------------- 1 | import { getMatchingTagPairs } from '../getMatchingTagPairs'; 2 | import { 3 | createScannerFast, 4 | ScannerFast, 5 | ScannerStateFast, 6 | TokenTypeFast 7 | } from '../htmlScanner/htmlScannerFast'; 8 | import { getIndent } from './getIndent'; 9 | 10 | export const getPreviousOpeningTagName: ( 11 | scanner: ScannerFast, 12 | initialOffset: number, 13 | isSelfClosingTag: (tagName: string) => boolean, 14 | isReact: boolean 15 | ) => 16 | | { 17 | tagName: string; 18 | offset: number; 19 | seenRightAngleBracket: boolean; 20 | indent: number; 21 | } 22 | | undefined = (scanner, initialOffset, isSelfClosingTag, isReact) => { 23 | let offset = initialOffset + 1; 24 | let parentTagName: string | undefined; 25 | let stack: string[] = []; 26 | let seenRightAngleBracket = false; 27 | let selfClosing = false; 28 | outer: do { 29 | scanner.stream.goTo(offset - 2); 30 | const hasFoundChar = scanner.stream.goBackUntilEitherChar( 31 | ['<', '>'], 32 | false, 33 | isReact 34 | ); 35 | if (!hasFoundChar) { 36 | return undefined; 37 | } 38 | const char = scanner.stream.peekLeft(1); 39 | if (!['<', '>'].includes(char)) { 40 | return undefined; 41 | } 42 | if (char === '>') { 43 | if (scanner.stream.peekLeft(2) === '/') { 44 | selfClosing = true; 45 | } 46 | seenRightAngleBracket = true; 47 | scanner.stream.goBack(1); 48 | scanner.stream.goBackUntilEitherChar(['<'], true, isReact); 49 | offset = scanner.stream.position; 50 | } 51 | // push closing tags onto the stack 52 | if (scanner.stream.peekRight() === '/') { 53 | offset = scanner.stream.position; 54 | scanner.stream.advance(1); 55 | scanner.state = ScannerStateFast.AfterOpeningEndTag; 56 | scanner.scan(); 57 | const token = scanner.getTokenText(); 58 | if (token === '') { 59 | offset = scanner.stream.position - 1; 60 | continue; 61 | } 62 | stack.push(scanner.getTokenText()); 63 | continue; 64 | } 65 | offset = scanner.stream.position; 66 | scanner.state = ScannerStateFast.AfterOpeningStartTag; 67 | const token = scanner.scan(); 68 | if (token !== TokenTypeFast.StartTag) { 69 | return undefined; 70 | } 71 | const tokenText = scanner.getTokenText(); 72 | if (selfClosing) { 73 | selfClosing = false; 74 | continue; 75 | } 76 | if (isSelfClosingTag(tokenText)) { 77 | continue; 78 | } 79 | // pop closing tags from the tags 80 | inner: while (stack.length) { 81 | let top = stack.pop(); 82 | if (top === tokenText) { 83 | continue outer; 84 | } 85 | if (isSelfClosingTag(top!)) { 86 | continue inner; 87 | } 88 | return undefined; 89 | } 90 | 91 | parentTagName = tokenText; 92 | if (parentTagName !== undefined) { 93 | break; 94 | } 95 | } while (true); 96 | 97 | const indent = getIndent(scanner.stream.getSource(), offset - 2); 98 | return { 99 | tagName: parentTagName, 100 | offset, 101 | seenRightAngleBracket, 102 | indent 103 | }; 104 | }; 105 | 106 | // getPreviousOpeningTagName( 107 | // createScannerFast({ 108 | // input: `if $variable < 0 { 109 | 110 | // } 111 | // $table .= '';`, 112 | // initialOffset: 35, 113 | // initialState: ScannerStateFast.WithinContent, 114 | // matchingTagPairs: getMatchingTagPairs('javascriptreact') 115 | // }), 116 | // 35, 117 | // () => false, 118 | // true 119 | // ); //? 120 | -------------------------------------------------------------------------------- /packages/service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "types": ["node"], 7 | "noUnusedParameters": false, 8 | "noUnusedLocals": false 9 | }, 10 | "include": ["src"] 11 | } 12 | -------------------------------------------------------------------------------- /scripts/package.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const root = path.join(__dirname, '..'); 5 | 6 | if (!fs.existsSync(path.join(root, 'dist'))) { 7 | fs.mkdirSync(path.join(root, 'dist')); 8 | } 9 | 10 | // @ts-ignore 11 | const pkg = require('../packages/extension/package.json'); 12 | 13 | pkg.main = './packages/extension/dist/extensionMain.js'; 14 | 15 | delete pkg.dependencies; 16 | delete pkg.devDependencies; 17 | delete pkg.scripts; 18 | delete pkg.enableProposedApi; 19 | 20 | fs.writeFileSync( 21 | path.join(root, 'dist/package.json'), 22 | `${JSON.stringify(pkg, null, 2)}\n` 23 | ); 24 | 25 | fs.copyFileSync( 26 | path.join(root, 'README.md'), 27 | path.join(root, 'dist/README.md') 28 | ); 29 | 30 | fs.copyFileSync( 31 | path.join(root, 'CHANGELOG.md'), 32 | path.join(root, 'dist/CHANGELOG.md') 33 | ); 34 | 35 | fs.copyFileSync(path.join(root, 'LICENSE'), path.join(root, 'dist/LICENSE')); 36 | 37 | fs.ensureDirSync(path.join(root, 'dist/images')); 38 | fs.copyFileSync( 39 | path.join(root, 'images/logo.png'), 40 | path.join(root, 'dist/images/logo.png') 41 | ); 42 | 43 | let extensionMain = fs 44 | .readFileSync( 45 | path.join(root, `dist/packages/extension/dist/extensionMain.js`) 46 | ) 47 | .toString(); 48 | 49 | extensionMain = extensionMain.replace( 50 | '../server/dist/serverMain.js', 51 | './packages/server/dist/serverMain.js' 52 | ); 53 | 54 | fs.writeFileSync( 55 | path.join(root, `dist/packages/extension/dist/extensionMain.js`), 56 | extensionMain 57 | ); 58 | -------------------------------------------------------------------------------- /tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "pretty": true, 4 | "lib": ["esnext"], 5 | "target": "es2019", 6 | "module": "commonjs", 7 | 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "composite": true, 12 | "noEmitOnError": true, 13 | 14 | "strictNullChecks": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "strictPropertyInitialization": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | 21 | "skipLibCheck": true, 22 | 23 | "alwaysStrict": true, 24 | 25 | "types": ["node"], 26 | "moduleResolution": "node", 27 | "resolveJsonModule": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig-noncomposite-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "declarationMap": false, 6 | "composite": false, 7 | "incremental": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "include": [], 4 | "references": [ 5 | { 6 | "path": "./packages/service" 7 | }, 8 | { 9 | "path": "./packages/extension" 10 | }, 11 | { 12 | "path": "./packages/server" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /webpack/client.webpack.config.js: -------------------------------------------------------------------------------- 1 | const withDefaults = require('./shared.webpack.config'); 2 | const path = require('path'); 3 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 4 | .BundleAnalyzerPlugin; 5 | 6 | module.exports = withDefaults({ 7 | context: path.join(__dirname, '../packages/extension'), 8 | entry: { 9 | extensionMain: './src/extensionMain.ts' 10 | }, 11 | optimization: { 12 | splitChunks: { 13 | minSize: 0, 14 | cacheGroups: { 15 | 'vscode-dependencies': { 16 | test: /node_modules\/(vscode|semver)/, 17 | chunks: 'all', 18 | name: 'vscode-dependencies' 19 | } 20 | } 21 | } 22 | }, 23 | externals: { 24 | vscode: 'commonjs vscode', 25 | bufferutil: 'commonjs bufferutil', 26 | 'utf-8-validate': 'commonjs utf-8-validate' 27 | }, 28 | output: { 29 | filename: '[name].js', 30 | path: path.join(__dirname, '../dist', 'packages/extension/dist') 31 | } 32 | // plugins: [new BundleAnalyzerPlugin()], 33 | }); 34 | -------------------------------------------------------------------------------- /webpack/server.webpack.config.js: -------------------------------------------------------------------------------- 1 | const withDefaults = require('./shared.webpack.config'); 2 | const path = require('path'); 3 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 4 | .BundleAnalyzerPlugin; 5 | 6 | module.exports = withDefaults({ 7 | context: path.join(__dirname, '../packages/server'), 8 | entry: { 9 | serverMain: './src/serverMain.ts' 10 | }, 11 | optimization: { 12 | splitChunks: { 13 | minSize: 0, 14 | cacheGroups: { 15 | 'vscode-dependencies': { 16 | test: /node_modules\/vscode/, 17 | chunks: 'all', 18 | name: 'vscode-dependencies' 19 | } 20 | } 21 | } 22 | }, 23 | output: { 24 | filename: '[name].js', 25 | path: path.join(__dirname, '../dist', 'packages/server/dist') 26 | } 27 | // plugins: [new BundleAnalyzerPlugin()], 28 | }); 29 | -------------------------------------------------------------------------------- /webpack/shared.webpack.config.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 2 | 3 | const path = require('path'); 4 | const merge = require('merge-options'); 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 6 | .BundleAnalyzerPlugin; 7 | 8 | module.exports = function withDefaults(/** @type WebpackConfig */ extConfig) { 9 | /** @type WebpackConfig */ 10 | let defaultConfig = { 11 | mode: 'production', 12 | target: 'node', 13 | node: { 14 | __dirname: false 15 | }, 16 | resolve: { 17 | mainFields: ['module', 'main'], 18 | extensions: ['.ts', '.js'] 19 | }, 20 | // optimization: { 21 | // minimize: false 22 | // }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.ts$/, 27 | exclude: /node_modules/, 28 | use: [ 29 | { 30 | loader: 'ts-loader', 31 | options: { 32 | compilerOptions: { 33 | sourceMap: true 34 | } 35 | } 36 | } 37 | ] 38 | } 39 | ] 40 | }, 41 | output: { 42 | filename: '[name].js', 43 | path: path.join(extConfig.context, 'dist'), 44 | libraryTarget: 'commonjs' 45 | }, 46 | devtool: 'source-map' 47 | // plugins: [new BundleAnalyzerPlugin()], 48 | }; 49 | 50 | return merge(defaultConfig, extConfig); 51 | }; 52 | --------------------------------------------------------------------------------