├── .eslintrc.json ├── .github └── workflows │ └── CI.yaml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images ├── auto-align-characters.gif └── icon.png ├── package-lock.json ├── package.json ├── src ├── extension.ts ├── formatter.ts ├── telemetry.ts └── test │ ├── data │ └── testcase.txt │ ├── runTest.ts │ └── suite │ ├── extension.test.ts │ ├── formatter.test.ts │ └── index.ts ├── tsconfig.json ├── webpack.config.js └── webpack.web.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": [ 11 | "error", 12 | { 13 | "selector": "memberLike", 14 | "modifiers": ["private"], 15 | "format": ["camelCase"], 16 | "leadingUnderscore": "require" 17 | }, 18 | { 19 | "selector": "enumMember", 20 | "format": ["PascalCase"] 21 | } 22 | ], 23 | "@typescript-eslint/semi": "error", 24 | "curly": "error", 25 | "eqeqeq": "error", 26 | "no-throw-literal": "error", 27 | "semi": "error" 28 | }, 29 | "ignorePatterns": ["out", "dist", "**/*.d.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | release: 9 | types: 10 | - published 11 | 12 | jobs: 13 | build: 14 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: 19 | - ubuntu-latest 20 | node: 21 | - 16 22 | outputs: 23 | vsixPath: ${{ steps.packageExtension.outputs.vsixPath }} 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Use Node.js ${{ matrix.node}} 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: ${{ matrix.node }} 30 | 31 | - name: Install Dependencies 32 | run: npm install 33 | 34 | - name: Compile Sources 35 | run: npm run compile 36 | 37 | - name: Run tests 38 | run: xvfb-run -a npm run test 39 | 40 | - name: Run Linter 41 | run: npm run lint 42 | 43 | - name: Package Extension 44 | id: packageExtension 45 | uses: HaaLeo/publish-vscode-extension@v1 46 | with: 47 | pat: stub 48 | dryRun: true 49 | - name: Upload Extension Package as Artifact 50 | uses: actions/upload-artifact@v2 51 | with: 52 | name: ${{ steps.packageExtension.outputs.vsixPath }} 53 | path: ${{ steps.packageExtension.outputs.vsixPath }} 54 | publish: 55 | name: Publish Github Action 56 | needs: 57 | - build 58 | if: github.event_name == 'release' 59 | runs-on: ubuntu-latest 60 | timeout-minutes: 10 61 | steps: 62 | - name: Checkout Source 63 | uses: actions/checkout@v2 64 | - name: Install Node v16 65 | uses: actions/setup-node@v1 66 | with: 67 | node-version: 16 68 | - name: Install Dependencies 69 | run: npm install 70 | - name: Download Build Artifact 71 | uses: actions/download-artifact@v3 72 | with: 73 | name: ${{ needs.build.outputs.vsixPath }} 74 | - name: Publish to Visual Studio Marketplace 75 | uses: HaaLeo/publish-vscode-extension@v1 76 | with: 77 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }} 78 | extensionFile: ${{ needs.build.outputs.vsixPath }} 79 | registryUrl: https://marketplace.visualstudio.com 80 | - name: Publish to Open VSX Registry 81 | uses: HaaLeo/publish-vscode-extension@v1 82 | with: 83 | pat: ${{ secrets.OPEN_VSX_TOKEN }} 84 | extensionFile: ${{ needs.build.outputs.vsixPath }} 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "endOfLine": "lf", 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "${workspaceFolder}/src/test/data", 26 | "${workspaceFolder}/src/test/data/testcase.txt", 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/**/*.js", 32 | "${workspaceFolder}/dist/**/*.js" 33 | ], 34 | "preLaunchTask": "tasks: watch-tests" 35 | }, 36 | { 37 | "name": "Run Web Extension in VS Code", 38 | "type": "extensionHost", 39 | "debugWebWorkerHost": true, 40 | "request": "launch", 41 | "args": [ 42 | "--extensionDevelopmentPath=${workspaceFolder}", 43 | "--extensionDevelopmentKind=web" 44 | ], 45 | "outFiles": ["${workspaceFolder}/dist/web/**/*.js"], 46 | "preLaunchTask": "${defaultBuildTask}" 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": [ 34 | "npm: watch", 35 | "npm: watch-tests" 36 | ], 37 | "problemMatcher": [] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .github/** 8 | .yarnrc 9 | webpack.config.js 10 | webpack.web.config.js 11 | vsc-extension-quickstart.md 12 | **/tsconfig.json 13 | **/.eslintrc.json 14 | **/*.map 15 | **/*.ts 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.4.2 [#](https://github.com/chouzz/vscode-better-align/releases/tag/v1.4.2) 2 | 3 | - Fix assignment like C style 4 | 5 | # v1.4.1 [#](https://github.com/chouzz/vscode-better-align/releases/tag/v1.4.1) 6 | 7 | - Fix tab indentation replaced by space indentation 8 | - Align command support `?:` operator 9 | 10 | # v1.4.0 [#](https://github.com/chouzz/vscode-better-align/releases/tag/v1.4.0) 11 | 12 | - Fix errors align with empty line 13 | - Fix format with double colon 14 | - Add support for three character operators 15 | - Fix incorrect indent during align with blocks 16 | - Add credit for origin author's contribution 17 | - Don't edit file if there is no any changes 18 | - Fix alignment with double slash comment 19 | - Fix add spaces if double align codes 20 | 21 | # v1.3.2 [#](https://github.com/chouzz/vscode-better-align/releases/tag/v1.3.2) 22 | 23 | - Add support autohotkey syntax `:=` 24 | - Update badges for vscode marketplace in readme 25 | 26 | # v1.3.1 [#](https://github.com/chouzz/vscode-better-align/releases/tag/v1.3.1) 27 | 28 | - Improve extension stability and quality 29 | - Update Dependencies 30 | 31 | # v1.3.0 [#](https://github.com/chouzz/vscode-better-align/releases/tag/v1.3.0) 32 | 33 | - Fix commands broken if cursor in empty line 34 | - Add web extension support 35 | 36 | # v1.2.0 [#](https://github.com/chouzz/vscode-better-align/releases/tag/v1.2.0) 37 | 38 | - Initial release 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Align for Visual Studio Code 2 | 3 | [![The MIT License](https://badgen.net/github/license/cerner/terra-framework)](https://badgen.net/github/license/cerner/terra-framework) 4 | [![GitHub](https://flat.badgen.net/github/release/chouzz/vscode-better-align)](https://github.com/chouzz/vscode-better-align/releases) 5 | [![Visual Studio Marketplace](https://vsmarketplacebadges.dev/installs-short/Chouzz.vscode-better-align.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=Chouzz.vscode-better-align) 6 | [![GithubActions](https://github.com/chouzz/vscode-better-align/actions/workflows/CI.yaml/badge.svg)](https://github.com/chouzz/vscode-better-align/actions/workflows/CI.yaml/badge.svg) 7 | 8 | Better vertical alignment with/without selection in any language for any characters or words. 9 | 10 | ## Features 11 | 12 | - Allow align code in any language 13 | - Align code by any characters 14 | - Smart align with or without selection 15 | - Auto align after you type enter 16 | 17 | ## Usage 18 | 19 | Place your cursor at where you want your code to be aligned, and use shortcut `alt + A` or invoke the `Align` command via Command Palette. 20 | 21 | ## Screenshots 22 | 23 | ![auto-align-characters.gif](https://github.com/chouzz/vscode-better-align/blob/main/images/auto-align-characters.gif) 24 | 25 | ## Extension Configuration 26 | 27 | ### `betterAlign.surroundSpace` 28 | 29 | Default value: 30 | 31 | ```json 32 | betterAlign.surroundSpace : { 33 | "colon" : [0, 1], // The first number specify how much space to add to the left, can be negative. 34 | // The second number is how much space to the right, can be negative. 35 | "assignment" : [1, 1], // The same as above. 36 | "arrow" : [1, 1], // The same as above. 37 | "comment" : 2 // Special how much space to add between the trailing comment and the code. 38 | // If this value is negative, it means don't align the trailing comment. 39 | } 40 | ``` 41 | 42 | ```javascript 43 | // Orignal code 44 | var abc = { 45 | hello: 1 46 | ,my :2//comment 47 | ,friend: 3 // comment 48 | } 49 | 50 | // "colon": [0, 1] 51 | // "comment": 2 52 | var abc = { 53 | hello : 1 54 | , my : 2 // comment 55 | , friend: 3 // comment 56 | } 57 | 58 | // "colon": [1, 2] 59 | // "comment": 4 60 | var abc = { 61 | hello : 1 62 | , my : 2 // comment 63 | , friend : 3 // comment 64 | } 65 | 66 | // "colon": [-1, 3] 67 | // "comment": 2 68 | var abc = { 69 | hello: 1 70 | , my: 2 // comment 71 | , friend: 3 // comment 72 | } 73 | 74 | // "colon": [-1, -1] 75 | // "comment": 2 76 | var abc = { 77 | hello:1 78 | , my:2 //comment 79 | , friend:3 // comment 80 | } 81 | 82 | 83 | // Orignal code 84 | $data = array( 85 | 'text' => 'something', 86 | 'here is another' => 'sample' 87 | ); 88 | 89 | // "arrow": [1, 3] 90 | $data = array( 91 | 'text' => 'something', 92 | 'here is another' => 'sample' 93 | ); 94 | 95 | ``` 96 | 97 | ## Issues/Contribution 98 | 99 | If you've found a bug, please file at . 100 | 101 | If you'd like to help out, fork the [repo](https://github.com/chouzz/vscode-better-align) and submit pull requests. 102 | 103 | ## License 104 | 105 | This work is licensed under [Apache License 2.0](https://opensource.org/licenses/Apache-2.0) 106 | 107 | ## Credit 108 | 109 | The codebase is based on [this repository](https://github.com/WarWithinMe/better-align). Thanks @WarWithinMe. 110 | -------------------------------------------------------------------------------- /images/auto-align-characters.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chouzz/vscode-better-align/581be43856b2019a9b7f8e29ed9c1b6478319f42/images/auto-align-characters.gif -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chouzz/vscode-better-align/581be43856b2019a9b7f8e29ed9c1b6478319f42/images/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-better-align", 3 | "displayName": "Better Align", 4 | "description": "Better vertical alignment with/without selection in any language for any characters or words.", 5 | "version": "1.4.2", 6 | "icon": "images/icon.png", 7 | "publisher": "chouzz", 8 | "license": "SEE LICENSE IN LICENSE", 9 | "author": { 10 | "name": "Chouzz", 11 | "url": "https://github.com/chouzz" 12 | }, 13 | "galleryBanner": { 14 | "color": "#272729", 15 | "theme": "dark" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/chouzz/vscode-better-align/issues", 19 | "email": "zhouhua25@qq.com" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/chouzz/vscode-better-align.git" 24 | }, 25 | "engines": { 26 | "vscode": "^1.47.0" 27 | }, 28 | "categories": [ 29 | "Formatters" 30 | ], 31 | "activationEvents": [ 32 | "onStartupFinished", 33 | "onCommand:vscode-better-align.align" 34 | ], 35 | "main": "./dist/extension.js", 36 | "browser": "./dist/web/extension.js", 37 | "contributes": { 38 | "commands": [ 39 | { 40 | "command": "vscode-better-align.align", 41 | "title": "Align" 42 | } 43 | ], 44 | "configuration": { 45 | "type": "object", 46 | "title": "Better Align", 47 | "properties": { 48 | "betterAlign.operatorPadding": { 49 | "type": "string", 50 | "enum": [ 51 | "left", 52 | "right" 53 | ], 54 | "default": "right", 55 | "description": "Control where to insert space to align different length operators (e.g. aligning = += *= ...)" 56 | }, 57 | "betterAlign.surroundSpace": { 58 | "type": "object", 59 | "default": { 60 | "colon": [ 61 | 0, 62 | 1 63 | ], 64 | "assignment": [ 65 | 1, 66 | 1 67 | ], 68 | "arrow": [ 69 | 1, 70 | 1 71 | ], 72 | "comment": 2 73 | }, 74 | "description": "Specify how many spaces to insert around the operator." 75 | }, 76 | "betterAlign.indentBase": { 77 | "type": "string", 78 | "enum": [ 79 | "firstline", 80 | "activeline", 81 | "dontchange" 82 | ], 83 | "default": "firstline", 84 | "description": "firstline: Change indent of all lines to the firstline.\n activeline: Change intent of all lines to the activeline.\n dontchange: Don't change line indent, only aligns those lines with same indentation." 85 | }, 86 | "betterAlign.alignAfterTypeEnter": { 87 | "type": "boolean", 88 | "default": false, 89 | "description": "Auto align current text near the cursor after type enter in editor" 90 | } 91 | } 92 | }, 93 | "configurationDefaults": { 94 | "[shellscript]": { 95 | "betterAlign.surroundSpace": { 96 | "colon": [ 97 | 0, 98 | 1 99 | ], 100 | "assignment": [ 101 | -1, 102 | -1 103 | ], 104 | "arrow": [ 105 | 1, 106 | 1 107 | ], 108 | "comment": 2 109 | } 110 | } 111 | }, 112 | "keybindings": [ 113 | { 114 | "command": "vscode-better-align.align", 115 | "key": "alt+a", 116 | "when": "editorTextFocus" 117 | } 118 | ] 119 | }, 120 | "scripts": { 121 | "vscode:prepublish": "npm run package", 122 | "compile": "webpack && npm run compile-web", 123 | "watch": "webpack --watch", 124 | "compile-web": "webpack --config webpack.web.config.js", 125 | "package": "webpack --mode production --devtool hidden-source-map && npm run compile-web", 126 | "compile-tests": "tsc -p . --outDir out", 127 | "watch-tests": "tsc -p . -w --outDir out", 128 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 129 | "lint": "eslint src --ext ts", 130 | "test": "node ./out/test/runTest.js" 131 | }, 132 | "devDependencies": { 133 | "@types/glob": "^8.0.0", 134 | "@types/mocha": "^9.1.1", 135 | "@types/node": "18.x", 136 | "@types/vscode": "^1.47.0", 137 | "@typescript-eslint/eslint-plugin": "^5.48.1", 138 | "@typescript-eslint/parser": "^5.45.0", 139 | "@vscode/test-electron": "^2.2.2", 140 | "esbuild-loader": "^2.20.0", 141 | "eslint": "^8.31.0", 142 | "glob": "^8.0.3", 143 | "mocha": "^10.0.0", 144 | "ts-loader": "^9.4.1", 145 | "typescript": "^4.9.4", 146 | "webpack": "^5.73.0", 147 | "webpack-cli": "^5.0.0" 148 | }, 149 | "dependencies": { 150 | "@vscode/extension-telemetry": "^0.7.7" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import * as telemetry from './telemetry'; 4 | import { Formatter } from './formatter'; 5 | 6 | export function activate(context: vscode.ExtensionContext) { 7 | telemetry.activate(context); 8 | const formatter = new Formatter(); 9 | let alignAfterEnter = vscode.workspace.getConfiguration('betterAlign').get('alignAfterTypeEnter'); 10 | 11 | context.subscriptions.push( 12 | vscode.commands.registerTextEditorCommand('vscode-better-align.align', (editor) => { 13 | telemetry.reporter.sendTelemetryEvent('align'); 14 | formatter.process(editor); 15 | }), 16 | vscode.workspace.onDidChangeTextDocument((e) => { 17 | if (alignAfterEnter && e.contentChanges.some((changes) => changes.text.includes('\n'))) { 18 | vscode.commands.executeCommand('vscode-better-align.align'); 19 | } 20 | }), 21 | vscode.workspace.onDidChangeConfiguration((e) => { 22 | if (e.affectsConfiguration('betterAlign')) { 23 | alignAfterEnter = vscode.workspace.getConfiguration('betterAlign').get('alignAfterTypeEnter'); 24 | } 25 | }), 26 | ); 27 | } 28 | 29 | // this method is called when your extension is deactivated 30 | export function deactivate() {} 31 | -------------------------------------------------------------------------------- /src/formatter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | enum TokenType { 4 | Invalid = 'Invalid', 5 | Word = 'Word', 6 | Assignment = 'Assignment', // = += -= *= /= %= ~= |= ^= .= := 7 | Arrow = 'Arrow', // => 8 | Block = 'Block', // {} [] () 9 | PartialBlock = 'PartialBlock', // { [ ( 10 | EndOfBlock = 'EndOfBlock', // } ] ) 11 | String = 'String', 12 | PartialString = 'PartialString', 13 | Comment = 'Comment', 14 | Whitespace = 'Whitespace', 15 | Colon = 'Colon', 16 | Comma = 'Comma', 17 | CommaAsWord = 'CommaAsWord', 18 | Insertion = 'Insertion', 19 | } 20 | 21 | interface Token { 22 | type: TokenType; 23 | text: string; 24 | } 25 | 26 | export interface LineInfo { 27 | line: vscode.TextLine; 28 | sgfntTokenType: TokenType; 29 | sgfntTokens: TokenType[]; 30 | tokens: Token[]; 31 | } 32 | 33 | export interface LineRange { 34 | anchor: number; 35 | infos: LineInfo[]; 36 | } 37 | 38 | const REG_WS = /\s/; 39 | const BRACKET_PAIR: any = { 40 | '{': '}', 41 | '[': ']', 42 | '(': ')', 43 | }; 44 | 45 | function whitespace(count: number) { 46 | return new Array(count + 1).join(' '); 47 | } 48 | 49 | export class Formatter { 50 | /* Align: 51 | * operators = += -= *= /= : 52 | * trailling comment 53 | * preceding comma 54 | * Ignore anything inside a quote, comment, or block 55 | */ 56 | public process(editor: vscode.TextEditor): void { 57 | this.editor = editor; 58 | 59 | // Get line ranges 60 | const ranges = this.getLineRanges(editor); 61 | 62 | // Format 63 | let formatted: string[][] = []; 64 | for (let range of ranges) { 65 | formatted.push(this.format(range)); 66 | } 67 | 68 | // Apply 69 | editor.edit((editBuilder) => { 70 | for (let i = 0; i < ranges.length; ++i) { 71 | var infos = ranges[i].infos; 72 | var lastline = infos[infos.length - 1].line; 73 | var location = new vscode.Range(infos[0].line.lineNumber, 0, lastline.lineNumber, lastline.text.length); 74 | const eol = editor.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n'; 75 | const replaced = formatted[i].join(eol); 76 | if (editor.document.getText(location) === replaced) { 77 | continue; 78 | } 79 | editBuilder.replace(location, replaced); 80 | } 81 | }); 82 | } 83 | 84 | protected editor: vscode.TextEditor; 85 | 86 | protected getLineRanges(editor: vscode.TextEditor): LineRange[] { 87 | var ranges: LineRange[] = []; 88 | editor.selections.forEach((sel) => { 89 | const indentBase = this.getConfig().get('indentBase', 'firstline') as string; 90 | const importantIndent: boolean = indentBase === 'dontchange'; 91 | 92 | let res: LineRange; 93 | if (sel.isSingleLine) { 94 | // If this selection is single line. Look up and down to search for the similar neighbour 95 | ranges.push(this.narrow(0, editor.document.lineCount - 1, sel.active.line, importantIndent)); 96 | } else { 97 | // Otherwise, narrow down the range where to align 98 | let start = sel.start.line; 99 | let end = sel.end.line; 100 | 101 | while (true) { 102 | res = this.narrow(start, end, start, importantIndent); 103 | let lastLine = res.infos[res.infos.length - 1]; 104 | 105 | if (lastLine.line.lineNumber > end) { 106 | break; 107 | } 108 | 109 | if (res.infos[0] && res.infos[0].sgfntTokenType !== TokenType.Invalid) { 110 | ranges.push(res); 111 | } 112 | 113 | if (lastLine.line.lineNumber === end) { 114 | break; 115 | } 116 | 117 | start = lastLine.line.lineNumber + 1; 118 | } 119 | } 120 | }); 121 | return ranges; 122 | } 123 | 124 | protected getConfig() { 125 | let defaultConfig = vscode.workspace.getConfiguration('betterAlign'); 126 | let langConfig: any = null; 127 | 128 | try { 129 | langConfig = vscode.workspace.getConfiguration().get(`[${this.editor.document.languageId}]`) as any; 130 | } catch (e) {} 131 | 132 | return { 133 | get: function (key: any, defaultValue?: any): any { 134 | if (langConfig) { 135 | var key1 = 'betterAlign.' + key; 136 | if (langConfig.hasOwnProperty(key1)) { 137 | return langConfig[key1]; 138 | } 139 | } 140 | 141 | return defaultConfig.get(key, defaultValue); 142 | }, 143 | }; 144 | } 145 | 146 | protected tokenize(line: number): LineInfo { 147 | let textline = this.editor.document.lineAt(line); 148 | let text = textline.text; 149 | let pos = 0; 150 | let lt: LineInfo = { 151 | line: textline, 152 | sgfntTokenType: TokenType.Invalid, 153 | sgfntTokens: [], 154 | tokens: [], 155 | }; 156 | 157 | let lastTokenType = TokenType.Invalid; 158 | let tokenStartPos = -1; 159 | 160 | while (pos < text.length) { 161 | let char = text.charAt(pos); 162 | let next = text.charAt(pos + 1); 163 | let third = text.charAt(pos + 2); 164 | 165 | let currTokenType: TokenType; 166 | 167 | let nextSeek = 1; 168 | 169 | // Tokens order are important 170 | if (char.match(REG_WS)) { 171 | currTokenType = TokenType.Whitespace; 172 | } else if (char === '"' || char === "'" || char === '`') { 173 | currTokenType = TokenType.String; 174 | } else if (char === '{' || char === '(' || char === '[') { 175 | currTokenType = TokenType.Block; 176 | } else if (char === '}' || char === ')' || char === ']') { 177 | currTokenType = TokenType.EndOfBlock; 178 | } else if ( 179 | char === '/' && 180 | ((next === '/' && (pos > 0 ? text.charAt(pos - 1) : '') !== ':') || // only `//` but not `://` 181 | next === '*') 182 | ) { 183 | currTokenType = TokenType.Comment; 184 | } else if (char === ',') { 185 | if (lt.tokens.length === 0 || (lt.tokens.length === 1 && lt.tokens[0].type === TokenType.Whitespace)) { 186 | currTokenType = TokenType.CommaAsWord; // Comma-first style 187 | } else { 188 | currTokenType = TokenType.Comma; 189 | } 190 | } else if (char === '=' && next === '>') { 191 | currTokenType = TokenType.Arrow; 192 | nextSeek = 2; 193 | } else if ( 194 | // Currently we support only known operators, 195 | // formatters will not work for unknown operators, we should find a way to support all operators. 196 | // Math operators 197 | (char === '+' || 198 | char === '-' || 199 | char === '*' || 200 | char === '/' || 201 | char === '%' || // FIXME: Find a way to work with the `**` operator 202 | // Bitwise operators 203 | char === '~' || 204 | char === '|' || 205 | char === '^' || // FIXME: Find a way to work with the `<<` and `>>` bitwise operators 206 | // Other operators 207 | char === '.' || 208 | char === ':' || 209 | char === '!' || 210 | char === '&' || 211 | char === '=') && 212 | next === '=' 213 | ) { 214 | currTokenType = TokenType.Assignment; 215 | nextSeek = third === '=' ? 3 : 2; 216 | } else if (char === '=' && next !== '=') { 217 | currTokenType = TokenType.Assignment; 218 | } else if (char === ':' && next === ':') { 219 | currTokenType = TokenType.Word; 220 | nextSeek = 2; 221 | } else if (char === ':' && next !== ':' || (char === '?' && next === ':')) { 222 | currTokenType = TokenType.Colon; 223 | } else { 224 | currTokenType = TokenType.Word; 225 | } 226 | 227 | if (currTokenType !== lastTokenType) { 228 | if (tokenStartPos !== -1) { 229 | lt.tokens.push({ 230 | type: lastTokenType, 231 | text: textline.text.substr(tokenStartPos, pos - tokenStartPos), 232 | }); 233 | } 234 | 235 | lastTokenType = currTokenType; 236 | tokenStartPos = pos; 237 | 238 | if ( 239 | lastTokenType === TokenType.Assignment || 240 | lastTokenType === TokenType.Colon || 241 | lastTokenType === TokenType.Arrow || 242 | lastTokenType === TokenType.Comment 243 | ) { 244 | if (lt.sgfntTokens.indexOf(lastTokenType) === -1) { 245 | lt.sgfntTokens.push(lastTokenType); 246 | } 247 | } 248 | } 249 | 250 | // Skip to end of string 251 | if (currTokenType === TokenType.String) { 252 | ++pos; 253 | while (pos < text.length) { 254 | let quote = text.charAt(pos); 255 | if (quote === char && text.charAt(pos - 1) !== '\\') { 256 | break; 257 | } 258 | ++pos; 259 | } 260 | if (pos >= text.length) { 261 | lastTokenType = TokenType.PartialString; 262 | } 263 | } 264 | 265 | // Skip to end of block 266 | if (currTokenType === TokenType.Block) { 267 | ++pos; 268 | let bracketCount = 1; 269 | while (pos < text.length) { 270 | let bracket = text.charAt(pos); 271 | if (bracket === char) { 272 | ++bracketCount; 273 | } else if (bracket === BRACKET_PAIR[char] && text.charAt(pos - 1) !== '\\') { 274 | if (bracketCount === 1) { 275 | break; 276 | } else { 277 | --bracketCount; 278 | } 279 | } 280 | ++pos; 281 | } 282 | if (pos >= text.length) { 283 | lastTokenType = TokenType.PartialBlock; 284 | } 285 | // -1 then + nextSeek so keep pos not change in next loop 286 | // or we will lost symbols like "] } )" 287 | } 288 | 289 | if (char === '/') { 290 | // Skip to end if we encounter single line comment 291 | if (next === '/') { 292 | pos = text.length; 293 | } else if (next === '*') { 294 | ++pos; 295 | while (pos < text.length) { 296 | if (text.charAt(pos) === '*' && text.charAt(pos + 1) === '/') { 297 | ++pos; 298 | currTokenType = TokenType.Word; 299 | break; 300 | } 301 | ++pos; 302 | } 303 | } 304 | } 305 | 306 | pos += nextSeek; 307 | } 308 | 309 | if (tokenStartPos !== -1) { 310 | lt.tokens.push({ 311 | type: lastTokenType, 312 | text: textline.text.substr(tokenStartPos, pos - tokenStartPos), 313 | }); 314 | } 315 | 316 | return lt; 317 | } 318 | 319 | protected hasPartialToken(info: LineInfo): boolean { 320 | for (let j = info.tokens.length - 1; j >= 0; --j) { 321 | let lastT = info.tokens[j]; 322 | if ( 323 | lastT.type === TokenType.PartialBlock || 324 | lastT.type === TokenType.EndOfBlock || 325 | lastT.type === TokenType.PartialString 326 | ) { 327 | return true; 328 | } 329 | } 330 | return false; 331 | } 332 | 333 | protected hasSameIndent(info1: LineInfo, info2: LineInfo): boolean { 334 | var t1 = info1.tokens[0]; 335 | var t2 = info2.tokens[0]; 336 | 337 | if (t1.type === TokenType.Whitespace) { 338 | if (t1.text === t2.text) { 339 | return true; 340 | } 341 | } else if (t2.type !== TokenType.Whitespace) { 342 | return true; 343 | } 344 | 345 | return false; 346 | } 347 | 348 | protected arrayAnd(array1: TokenType[], array2: TokenType[]): TokenType[] { 349 | var res: TokenType[] = []; 350 | var map: any = {}; 351 | for (var i = 0; i < array1.length; ++i) { 352 | map[array1[i]] = true; 353 | } 354 | for (var i = 0; i < array2.length; ++i) { 355 | if (map[array2[i]]) { 356 | res.push(array2[i]); 357 | } 358 | } 359 | return res; 360 | } 361 | 362 | /* 363 | * Determine which blocks of code needs to be align. 364 | * 1. Empty lines is the boundary of a block. 365 | * 2. If user selects something, blocks are always within selection, 366 | * but not necessarily is the selection. 367 | * 3. Bracket / Brace usually means boundary. 368 | * 4. Unsimilar line is boundary. 369 | */ 370 | protected narrow(start: number, end: number, anchor: number, importantIndent: boolean): LineRange { 371 | let anchorToken = this.tokenize(anchor); 372 | let range = { anchor, infos: [anchorToken] }; 373 | 374 | let tokenTypes = anchorToken.sgfntTokens; 375 | 376 | if (anchorToken.sgfntTokens.length === 0) { 377 | return range; 378 | } 379 | 380 | if (this.hasPartialToken(anchorToken)) { 381 | return range; 382 | } 383 | 384 | let i = anchor - 1; 385 | while (i >= start) { 386 | let token = this.tokenize(i); 387 | 388 | if (this.hasPartialToken(token)) { 389 | break; 390 | } 391 | 392 | let tt = this.arrayAnd(tokenTypes, token.sgfntTokens); 393 | if (tt.length === 0) { 394 | break; 395 | } 396 | tokenTypes = tt; 397 | 398 | if (importantIndent && !this.hasSameIndent(anchorToken, token)) { 399 | break; 400 | } 401 | 402 | range.infos.unshift(token); 403 | --i; 404 | } 405 | 406 | i = anchor + 1; 407 | while (i <= end) { 408 | let token = this.tokenize(i); 409 | 410 | let tt = this.arrayAnd(tokenTypes, token.sgfntTokens); 411 | if (tt.length === 0) { 412 | break; 413 | } 414 | tokenTypes = tt; 415 | 416 | if (importantIndent && !this.hasSameIndent(anchorToken, token)) { 417 | break; 418 | } 419 | 420 | if (this.hasPartialToken(token)) { 421 | range.infos.push(token); 422 | break; 423 | } 424 | 425 | range.infos.push(token); 426 | ++i; 427 | } 428 | 429 | let sgt; 430 | if (tokenTypes.indexOf(TokenType.Assignment) >= 0) { 431 | sgt = TokenType.Assignment; 432 | } else { 433 | sgt = tokenTypes[0]; 434 | } 435 | for (let info of range.infos) { 436 | info.sgfntTokenType = sgt; 437 | } 438 | 439 | return range; 440 | } 441 | 442 | protected format(range: LineRange): string[] { 443 | // 0. Remove indentatioin, and trailing whitespace 444 | let indentation = ''; 445 | let anchorLine = range.infos[0]; 446 | const config = this.getConfig(); 447 | 448 | if ((config.get('indentBase', 'firstline') as string) === 'activeline') { 449 | for (let info of range.infos) { 450 | if (info.line.lineNumber === range.anchor) { 451 | anchorLine = info; 452 | break; 453 | } 454 | } 455 | } 456 | if (!anchorLine.tokens.length) { 457 | return []; 458 | } 459 | 460 | // Get indentation from multiple lines 461 | /* 462 | fasdf !== 1231321; => indentation = 0 463 | var abc === 123; 464 | 465 | test := 1 => indentation = 4 466 | teastas := 2 467 | 468 | */ 469 | let firstNonSpaceCharIndex = 0; 470 | let min = Infinity; 471 | let whiteSpaceType = ' '; 472 | for (let info of range.infos) { 473 | firstNonSpaceCharIndex = info.line.text.search(/\S/); 474 | min = Math.min(min, firstNonSpaceCharIndex); 475 | if (info.tokens[0].type === TokenType.Whitespace) { 476 | whiteSpaceType = info.tokens[0].text[0] ?? ' '; 477 | info.tokens.shift(); 478 | } 479 | if (info.tokens.length > 1 && info.tokens[info.tokens.length - 1].type === TokenType.Whitespace) { 480 | info.tokens.pop(); 481 | } 482 | } 483 | indentation = whiteSpaceType.repeat(min); 484 | /* 1. Special treatment for Word-Word-Operator ( e.g. var abc = ) 485 | For example, without: 486 | 487 | var abc === 123; var abc === 123; 488 | var fsdafsf === 32423, => var fsdafsf === 32423, 489 | fasdf !== 1231321; fasdf !== 1231321; 490 | 491 | with this : 492 | 493 | var abc === 123; var abc === 123; 494 | var fsdafsf === 32423, => var fsdafsf === 32423, 495 | fasdf !== 1231321; fasdf !== 1231321; 496 | */ 497 | 498 | // Calculate first word's length 499 | let firstWordLength = 0; 500 | for (let info of range.infos) { 501 | let count = 0; 502 | for (let token of info.tokens) { 503 | if (token.type === info.sgfntTokenType) { 504 | count = -count; 505 | break; 506 | } 507 | // Skip calculate word length before block, See https://github.com/chouzz/vscode-better-align/issues/57 508 | if (token.type === TokenType.Block) { 509 | continue; 510 | } 511 | if (token.type !== TokenType.Whitespace) { 512 | ++count; 513 | } 514 | } 515 | 516 | if (count < -1) { 517 | firstWordLength = Math.max(firstWordLength, info.tokens[0].text.length); 518 | } 519 | } 520 | 521 | // Add white space after the first word 522 | if (firstWordLength > 0) { 523 | let wordSpace: Token = { 524 | type: TokenType.Insertion, 525 | text: whitespace(firstWordLength + 1), 526 | }; 527 | let oneSpace: Token = { type: TokenType.Insertion, text: ' ' }; 528 | 529 | for (let info of range.infos) { 530 | let count = 0; 531 | for (let token of info.tokens) { 532 | if (token.type === info.sgfntTokenType) { 533 | count = -count; 534 | break; 535 | } 536 | if (token.type !== TokenType.Whitespace) { 537 | ++count; 538 | } 539 | } 540 | 541 | if (count === -1) { 542 | info.tokens.unshift(wordSpace); 543 | } else if (count < -1) { 544 | if (info.tokens[1].type === TokenType.Whitespace) { 545 | info.tokens[1] = oneSpace; 546 | } else if (info.tokens[0].type === TokenType.CommaAsWord) { 547 | info.tokens.splice(1, 0, oneSpace); 548 | } 549 | if (info.tokens[0].text.length !== firstWordLength) { 550 | let ws = { 551 | type: TokenType.Insertion, 552 | text: whitespace(firstWordLength - info.tokens[0].text.length), 553 | }; 554 | if (info.tokens[0].type === TokenType.CommaAsWord) { 555 | info.tokens.unshift(ws); 556 | } else { 557 | info.tokens.splice(1, 0, ws); 558 | } 559 | } 560 | } 561 | } 562 | } 563 | 564 | // 2. Remove whitespace surrounding operator ( comma in the middle of the line is also consider an operator ). 565 | for (let info of range.infos) { 566 | let i = 1; 567 | while (i < info.tokens.length) { 568 | if (info.tokens[i].type === info.sgfntTokenType || info.tokens[i].type === TokenType.Comma) { 569 | if (info.tokens[i - 1].type === TokenType.Whitespace) { 570 | info.tokens.splice(i - 1, 1); 571 | --i; 572 | } 573 | if (info.tokens[i + 1] && info.tokens[i + 1].type === TokenType.Whitespace) { 574 | info.tokens.splice(i + 1, 1); 575 | } 576 | } 577 | ++i; 578 | } 579 | } 580 | 581 | // 3. Align 582 | const configOP = config.get('operatorPadding') as string; 583 | const configWS = config.get('surroundSpace'); 584 | const stt = TokenType[range.infos[0].sgfntTokenType].toLowerCase(); 585 | const configDef: any = { 586 | colon: [0, 1], 587 | assignment: [1, 1], 588 | comment: 2, 589 | arrow: [1, 1], 590 | }; 591 | const configSTT = configWS[stt] || configDef[stt]; 592 | const configComment = configWS['comment'] || configDef['comment']; 593 | 594 | const rangeSize = range.infos.length; 595 | 596 | let length = new Array(rangeSize); 597 | length.fill(0); 598 | let column = new Array(rangeSize); 599 | column.fill(0); 600 | let result = new Array(rangeSize); 601 | result.fill(indentation); 602 | 603 | let exceed = 0; // Tracks how many line have reached to the end. 604 | let hasTrallingComment = false; 605 | let resultSize = 0; 606 | 607 | while (exceed < rangeSize) { 608 | let operatorSize = 0; 609 | 610 | // First pass: for each line, scan until we reach to the next operator 611 | for (let l = 0; l < rangeSize; ++l) { 612 | let i = column[l]; 613 | let info = range.infos[l]; 614 | let tokenSize = info.tokens.length; 615 | 616 | if (i === -1) { 617 | continue; 618 | } 619 | 620 | let end = tokenSize; 621 | let res = result[l]; 622 | 623 | // Bail out if we reach to the trailing comment 624 | if (tokenSize > 1 && info.tokens[tokenSize - 1].type === TokenType.Comment) { 625 | hasTrallingComment = true; 626 | if (tokenSize > 2 && info.tokens[tokenSize - 2].type === TokenType.Whitespace) { 627 | end = tokenSize - 2; 628 | } else { 629 | end = tokenSize - 1; 630 | } 631 | } 632 | 633 | for (; i < end; ++i) { 634 | let token = info.tokens[i]; 635 | // Vertical align will occur at significant operator or subsequent comma 636 | if (token.type === info.sgfntTokenType || (token.type === TokenType.Comma && i !== 0)) { 637 | operatorSize = Math.max(operatorSize, token.text.length); 638 | break; 639 | } else { 640 | res += token.text; 641 | } 642 | } 643 | 644 | result[l] = res; 645 | if (i < end) { 646 | resultSize = Math.max(resultSize, res.length); 647 | } 648 | 649 | if (i === end) { 650 | ++exceed; 651 | column[l] = -1; 652 | info.tokens.splice(0, end); 653 | } else { 654 | column[l] = i; 655 | } 656 | } 657 | 658 | // Second pass: align 659 | for (let l = 0; l < rangeSize; ++l) { 660 | let i = column[l]; 661 | if (i === -1) { 662 | continue; 663 | } 664 | 665 | let info = range.infos[l]; 666 | let res = result[l]; 667 | 668 | let op = info.tokens[i].text; 669 | if (op.length < operatorSize) { 670 | if (configOP === 'right') { 671 | op = whitespace(operatorSize - op.length) + op; 672 | } else { 673 | op = op + whitespace(operatorSize - op.length); 674 | } 675 | } 676 | 677 | let padding = ''; 678 | if (resultSize > res.length) { 679 | padding = whitespace(resultSize - res.length); 680 | } 681 | 682 | if (info.tokens[i].type === TokenType.Comma) { 683 | res += op; 684 | if (i < info.tokens.length - 1) { 685 | res += padding + ' '; // Ensure there's one space after comma. 686 | } 687 | // Skip if there is only comment type without any operators. 688 | } else if (info.tokens.length === 1 && info.tokens[0].type === TokenType.Comment) { 689 | exceed++; 690 | break; 691 | } else { 692 | if (configSTT[0] < 0) { 693 | // operator will stick with the leftside word 694 | if (configSTT[1] < 0) { 695 | // operator will be aligned, and the sibling token will be connected with the operator 696 | let z = res.length - 1; 697 | while (z >= 0) { 698 | let ch = res.charAt(z); 699 | if (ch.match(REG_WS)) { 700 | break; 701 | } 702 | --z; 703 | } 704 | res = res.substring(0, z + 1) + padding + res.substring(z + 1) + op; 705 | } else { 706 | res = res + op; 707 | if (i < info.tokens.length - 1) { 708 | res += padding; 709 | } 710 | } 711 | } else { 712 | res = res + padding + whitespace(configSTT[0]) + op; 713 | } 714 | if (configSTT[1] > 0) { 715 | res += whitespace(configSTT[1]); 716 | } 717 | } 718 | 719 | result[l] = res; 720 | column[l] = i + 1; 721 | } 722 | } 723 | 724 | // 4. Align trailing comment 725 | if (configComment < 0) { 726 | // It means user don't want to align trailing comment. 727 | for (let l = 0; l < rangeSize; ++l) { 728 | let info = range.infos[l]; 729 | for (let token of info.tokens) { 730 | result[l] += token.text; 731 | } 732 | } 733 | } else { 734 | resultSize = 0; 735 | for (let res of result) { 736 | resultSize = Math.max(res.length, resultSize); 737 | } 738 | for (let l = 0; l < rangeSize; ++l) { 739 | let info = range.infos[l]; 740 | if (info.tokens.length) { 741 | let res = result[l]; 742 | result[l] = res + whitespace(resultSize - res.length + configComment) + info.tokens.pop()?.text; 743 | } 744 | } 745 | } 746 | 747 | return result; 748 | } 749 | } 750 | -------------------------------------------------------------------------------- /src/telemetry.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import TelemetryReporter from '@vscode/extension-telemetry'; 3 | 4 | // the application insights key (also known as instrumentation key) 5 | const key = '713fdedf-aaef-442b-bc8f-11e7f0eabd12'; 6 | 7 | // telemetry reporter 8 | export let reporter: TelemetryReporter; 9 | 10 | export function activate(context: vscode.ExtensionContext) { 11 | reporter = new TelemetryReporter(key); 12 | context.subscriptions.push(reporter); 13 | } -------------------------------------------------------------------------------- /src/test/data/testcase.txt: -------------------------------------------------------------------------------- 1 | // Only some comments 2 | // Only some comments 3 | // Only some comments 4 | // Only some comments 5 | // Only some comments 6 | 7 | var abc = 123; 8 | var fsdafsf = 32423, 9 | fasdf = 1231321; 10 | 11 | let fdsafa:fadsf = fdsaf; 12 | let lt:LineInfo = { 13 | line:textline 14 | , sgfntTokenType:TokenType.Invalid 15 | , tokens:[] 16 | }; 17 | 18 | // AutoHotKey Syntax 19 | test := 1 20 | teastas := 2 21 | 22 | // Php Syntax 23 | $data = [ 24 | self::LAUNCHAAA => 'test', 25 | self::WAIT => 'testtas', 26 | ]; 27 | 28 | var abc == 123; 29 | var fsdafsf == 32423, 30 | fasdf != 1231321; 31 | 32 | var abc === 123; 33 | var fsdafsf === 32423, 34 | fasdf !== 1231321; 35 | 36 | public function index() 37 | { 38 | $x = 'test'; 39 | if ( 40 | $x === 'nott test' 41 | || $x === 0 42 | || $x !== 'test' 43 | ) { 44 | return false; 45 | } else { 46 | return true; 47 | } 48 | } 49 | 50 | $item["venue_id"] = $venue->id; 51 | $item["account_id"] = $venue->parent_id; 52 | $item["expire_date"] = Carbon::now()->{$carbon_function}(); 53 | $acc_license_data[] = $item; 54 | 55 | class MyClass { 56 | public: 57 | int myNum; // Attribute (int variable) 58 | string myString; // Attribute (string variable) 59 | }; 60 | 61 | test := 1 // Only some comments 62 | teastas := 2 // Only some comments 63 | 64 | test123 := 123 65 | global test1 := 13 66 | test2332 = 1234 67 | test4124 += 124 68 | 69 | export interface TA_P{ 70 | click_type : string|number, // a 71 | page_type ?: string, 72 | card_type ?: string, //c 73 | } 74 | 75 | $test = 123; 76 | $test123 = 456; 77 | 78 | int a &= b; 79 | int c |= d; -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | const workspace = path.resolve(__dirname, '../../src/test/data'); 15 | const openedFile = path.resolve(__dirname, '../../src/test/data/testcase.txt'); 16 | // Download VS Code, unzip it and run the integration test 17 | await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs: [workspace, openedFile] }); 18 | } catch (err) { 19 | console.error('Failed to run tests'); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | main(); 25 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/formatter.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | import { Formatter, LineRange } from '../../formatter'; 4 | 5 | class FakeFormatter extends Formatter { 6 | public format(range: LineRange): string[] { 7 | return super.format(range); 8 | } 9 | 10 | public getLineRanges(editor: vscode.TextEditor) { 11 | super.editor = editor; 12 | return super.getLineRanges(editor); 13 | } 14 | } 15 | 16 | suite('Formatter Test Suite', () => { 17 | const editor = vscode.window.activeTextEditor; 18 | if (!editor) { 19 | return; 20 | } 21 | 22 | test('Formatter::should format comment', () => { 23 | editor.selection = new vscode.Selection(0, 0, 0, 0); 24 | const formatter = new FakeFormatter(); 25 | const ranges = formatter.getLineRanges(editor); 26 | const actual = formatter.format(ranges[0]); 27 | const expect = [ 28 | ' // Only some comments', 29 | ' // Only some comments', 30 | ' // Only some comments', 31 | ' // Only some comments', 32 | ' // Only some comments', 33 | ]; 34 | assert.deepEqual(actual, expect); 35 | }); 36 | 37 | test('Formatter::should format assignment like =', () => { 38 | editor.selection = new vscode.Selection(6, 0, 6, 0); 39 | const formatter = new FakeFormatter(); 40 | const ranges = formatter.getLineRanges(editor); 41 | const actual = formatter.format(ranges[0]); 42 | const expect = [ 43 | 'var abc = 123;', 44 | 'var fsdafsf = 32423,', 45 | ' fasdf = 1231321;' 46 | ]; 47 | assert.deepEqual(actual, expect); 48 | }); 49 | 50 | test('Formatter::should format colon like :', () => { 51 | editor.selection = new vscode.Selection(12, 0, 12, 0); 52 | const formatter = new FakeFormatter(); 53 | const ranges = formatter.getLineRanges(editor); 54 | const actual = formatter.format(ranges[0]); 55 | const expect = [ 56 | ' line : textline', 57 | ' , sgfntTokenType: TokenType.Invalid', 58 | ' , tokens : []', 59 | ]; 60 | assert.deepEqual(actual, expect); 61 | }); 62 | 63 | test('Formatter::should format assignment like :=', () => { 64 | editor.selection = new vscode.Selection(18, 0, 18, 0); 65 | const formatter = new FakeFormatter(); 66 | const ranges = formatter.getLineRanges(editor); 67 | const actual = formatter.format(ranges[0]); 68 | const expect = [ 69 | 'test := 1', 70 | 'teastas := 2', 71 | ]; 72 | assert.deepEqual(actual, expect); 73 | }); 74 | 75 | test('Formatter::should format assignment like == and !=', () => { 76 | editor.selection = new vscode.Selection(29, 0, 29, 0); 77 | const formatter = new FakeFormatter(); 78 | const ranges = formatter.getLineRanges(editor); 79 | const actual = formatter.format(ranges[0]); 80 | const expect = [ 81 | 'var abc == 123;', 82 | 'var fsdafsf == 32423,', 83 | ' fasdf != 1231321;' 84 | ]; 85 | assert.deepEqual(actual, expect); 86 | }); 87 | 88 | test('Formatter::should format assignment like === and !==', () => { 89 | editor.selection = new vscode.Selection(33, 0, 33, 0); 90 | const formatter = new FakeFormatter(); 91 | const ranges = formatter.getLineRanges(editor); 92 | const actual = formatter.format(ranges[0]); 93 | const expect = [ 94 | 'var abc === 123;', 95 | 'var fsdafsf === 32423,', 96 | ' fasdf !== 1231321;' 97 | ]; 98 | assert.deepEqual(actual, expect); 99 | }); 100 | 101 | test('Formatter::should format assignment like === and !== with special character', () => { 102 | editor.selection = new vscode.Selection(41, 0, 41, 0); 103 | const formatter = new FakeFormatter(); 104 | const ranges = formatter.getLineRanges(editor); 105 | const actual = formatter.format(ranges[0]); 106 | const expect = [ 107 | ` $x === 'nott test'`, 108 | ' || $x === 0', 109 | ` || $x !== 'test'` 110 | ]; 111 | assert.deepEqual(actual, expect); 112 | }); 113 | 114 | test('Formatter::should not format double Colon like ::', () => { 115 | editor.selection = new vscode.Selection(23, 0, 23, 0); 116 | const formatter = new FakeFormatter(); 117 | const ranges = formatter.getLineRanges(editor); 118 | const actual = formatter.format(ranges[0]); 119 | const expect = [ 120 | ` self::LAUNCHAAA => 'test',`, 121 | ` self::WAIT => 'testtas',`, 122 | ]; 123 | assert.deepEqual(actual, expect); 124 | }); 125 | 126 | test('Formatter::should format code with block', () => { 127 | editor.selection = new vscode.Selection(50, 0, 50, 0); 128 | const formatter = new FakeFormatter(); 129 | const ranges = formatter.getLineRanges(editor); 130 | const actual = formatter.format(ranges[0]); 131 | const expect = [ 132 | '$item["venue_id"] = $venue->id;', 133 | '$item["account_id"] = $venue->parent_id;', 134 | '$item["expire_date"] = Carbon::now()->{$carbon_function}();', 135 | '$acc_license_data[] = $item;', 136 | ]; 137 | assert.deepEqual(actual, expect); 138 | }); 139 | 140 | test('Formatter::should format comment with words', () => { 141 | editor.selection = new vscode.Selection(57, 0, 57, 0); 142 | const formatter = new FakeFormatter(); 143 | const ranges = formatter.getLineRanges(editor); 144 | const actual = formatter.format(ranges[0]); 145 | const expect = [ 146 | ' int myNum; // Attribute (int variable)', 147 | ' string myString; // Attribute (string variable)' 148 | ]; 149 | assert.deepEqual(actual, expect); 150 | }); 151 | 152 | test('Formatter::should format comment with operators', () => { 153 | editor.selection = new vscode.Selection(61, 0, 61, 0); 154 | const formatter = new FakeFormatter(); 155 | const ranges = formatter.getLineRanges(editor); 156 | const actual = formatter.format(ranges[0]); 157 | const expect = [ 158 | 'test := 1 // Only some comments', 159 | 'teastas := 2 // Only some comments' 160 | ]; 161 | assert.deepEqual(actual, expect); 162 | }); 163 | 164 | test('Formatter::should not format if first line contain space', () => { 165 | editor.selection = new vscode.Selection(64, 0, 64, 0); 166 | const formatter = new FakeFormatter(); 167 | const ranges = formatter.getLineRanges(editor); 168 | const actual = formatter.format(ranges[0]); 169 | const expect = [ 170 | ' test123 := 123', 171 | 'global test1 := 13', 172 | ' test2332 = 1234', 173 | ' test4124 += 124', 174 | ]; 175 | assert.deepEqual(actual, expect); 176 | }); 177 | 178 | test('Formatter::should format operator like ?:', () => { 179 | editor.selection = new vscode.Selection(70, 0, 70, 0); 180 | const formatter = new FakeFormatter(); 181 | const ranges = formatter.getLineRanges(editor); 182 | const actual = formatter.format(ranges[0]); 183 | const expect = [ 184 | ' click_type : string|number, // a', 185 | ' page_type ?: string,', 186 | ' card_type ?: string, //c', 187 | ]; 188 | assert.deepEqual(actual, expect); 189 | }); 190 | 191 | test('Formatter::should not break tab indent', () => { 192 | editor.selection = new vscode.Selection(75, 0, 75, 0); 193 | const formatter = new FakeFormatter(); 194 | const ranges = formatter.getLineRanges(editor); 195 | const actual = formatter.format(ranges[0]); 196 | const expect = [ 197 | ' $test = 123;', 198 | ' $test123 = 456;' 199 | ]; 200 | assert.deepEqual(actual, expect); 201 | }); 202 | 203 | test('Formatter::should get correct result for c like assignment', () => { 204 | editor.selection = new vscode.Selection(78, 0, 78, 0); 205 | const formatter = new FakeFormatter(); 206 | const ranges = formatter.getLineRanges(editor); 207 | const actual = formatter.format(ranges[0]); 208 | const expect = [ 209 | 'int a &= b;', 210 | 'int c |= d;' 211 | ]; 212 | assert.deepEqual(actual, expect); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "strictPropertyInitialization": false, 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module 26 | '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module 27 | '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module 28 | '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module 29 | '@azure/functions-core': 'commonjs @azure/functions-core' 30 | }, 31 | resolve: { 32 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 33 | extensions: ['.ts', '.js'] 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.ts$/, 39 | exclude: /node_modules/, 40 | loader: 'esbuild-loader', 41 | options: { 42 | loader: 'ts', 43 | target: 'es2015' 44 | } 45 | } 46 | ] 47 | }, 48 | devtool: 'nosources-source-map', 49 | infrastructureLogging: { 50 | level: "log", // enables logging required for problem matchers 51 | }, 52 | }; 53 | module.exports = [ extensionConfig ]; 54 | -------------------------------------------------------------------------------- /webpack.web.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 5 | /** @type WebpackConfig */ 6 | const webExtensionConfig = { 7 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 8 | target: 'webworker', // extensions run in a webworker context 9 | entry: { 10 | extension: './src/extension.ts', // source of the web extension main file 11 | //'test/suite/index': './src/web/test/suite/index.ts' // source of the web extension test runner 12 | }, 13 | output: { 14 | filename: '[name].js', 15 | path: path.join(__dirname, './dist/web'), 16 | libraryTarget: 'commonjs', 17 | devtoolModuleFilenameTemplate: '../../[resource-path]' 18 | }, 19 | resolve: { 20 | mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules 21 | extensions: ['.ts', '.js'], // support ts-files and js-files 22 | alias: { 23 | // provides alternate implementation for node module and source files 24 | }, 25 | fallback: { 26 | // Webpack 5 no longer polyfills Node.js core modules automatically. 27 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 28 | // for the list of Node.js core module polyfills. 29 | assert: require.resolve('assert') 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.ts$/, 36 | exclude: /node_modules/, 37 | loader: 'esbuild-loader', 38 | options: { 39 | loader: 'ts', 40 | target: 'es2015' 41 | } 42 | } 43 | ] 44 | }, 45 | plugins: [ 46 | new webpack.ProvidePlugin({ 47 | process: 'process/browser' // provide a shim for the global `process` variable 48 | }) 49 | ], 50 | externals: { 51 | vscode: 'commonjs vscode', // ignored because it doesn't exist 52 | 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // ignored because we don't ship native module 53 | '@opentelemetry/tracing': 'commonjs @opentelemetry/tracing', // ignored because we don't ship this module 54 | '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship this module 55 | '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship this module 56 | '@azure/functions-core': 'commonjs @azure/functions-core' 57 | }, 58 | performance: { 59 | hints: false 60 | }, 61 | devtool: 'nosources-source-map' // create a source map that points to the original source file 62 | }; 63 | module.exports = [webExtensionConfig]; 64 | --------------------------------------------------------------------------------