├── .c8rc.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.mjs ├── images ├── capitalize.gif ├── demo-multiline.gif ├── demo-not.gif ├── demo.gif ├── infer-names.png └── logo.png ├── package-lock.json ├── package.json ├── src ├── completionItemBuilder.ts ├── extension.ts ├── htmlLikeSupport.ts ├── notCommand.ts ├── postfixCompletionProvider.ts ├── template.d.ts ├── templates │ ├── awaitTemplate.ts │ ├── baseTemplates.ts │ ├── callTemplate.ts │ ├── castTemplates.ts │ ├── consoleTemplates.ts │ ├── customTemplate.ts │ ├── equalityTemplates.ts │ ├── forTemplates.ts │ ├── ifTemplates.ts │ ├── newTemplate.ts │ ├── notTemplate.ts │ ├── promisifyTemplate.ts │ ├── returnTemplate.ts │ └── varTemplates.ts ├── utils.ts └── utils │ ├── infer-names.ts │ ├── invert-expression.ts │ ├── multiline-expressions.ts │ ├── templates.ts │ └── typescript.ts ├── tasks.mjs ├── test ├── dsl.ts ├── extension.multiline.test.ts ├── extension.singleline.test.ts ├── extension.svelte-vue-html.test.ts ├── index.ts ├── runTests.ts ├── runner.ts ├── template.usage.test.ts ├── utils.test.ts └── utils.ts └── tsconfig.json /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "lcov", 4 | "text-summary" 5 | ], 6 | "exclude": [ 7 | "tasks.mjs", 8 | "**/test/**" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = unset 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | /coverage 4 | /.vscode-test 5 | /test/index.ts 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | root: true, 4 | parser: '@typescript-eslint/parser', 5 | plugins: [ 6 | '@typescript-eslint', 7 | ], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | rules: { 13 | "@typescript-eslint/explicit-module-boundary-types": "off", 14 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 15 | "prefer-const": ["error", { "destructuring": "all" }], 16 | "no-constant-condition": ["error", { "checkLoops": false }], 17 | "curly": ["error"], 18 | "brace-style": "error", 19 | "indent": ["error", 2, { 20 | SwitchCase: 1 21 | }] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | tags: 9 | - v* 10 | 11 | pull_request: 12 | branches: 13 | - master 14 | - develop 15 | 16 | jobs: 17 | build-and-publish: 18 | runs-on: ubuntu-20.04 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | - name: Install Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '16.15' 26 | - run: npm ci 27 | - name: Run Linter 28 | run: npm run lint 29 | - name: Run tests 30 | run: xvfb-run -a npm run test-with-coverage 31 | - name: Code coverage 32 | uses: codecov/codecov-action@v3 33 | - name: Publish 34 | if: success() && startsWith( github.ref, 'refs/tags/v') 35 | run: npm run deploy 36 | env: 37 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | /*.vsix 4 | /.vscode/symbols.json 5 | /coverage 6 | /.vscode-test -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "connor4312.esbuild-problem-matchers", 4 | "dbaeumer.vscode-eslint", 5 | "editorconfig.editorconfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.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=${workspaceRoot}" 12 | ], 13 | "sourceMaps": true, 14 | "outFiles": [ 15 | "${workspaceRoot}/out/**/*.js" 16 | ], 17 | "preLaunchTask": "npm: watch", 18 | }, 19 | { 20 | "name": "Launch Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "runtimeExecutable": "${execPath}", 24 | "args": [ 25 | "--disable-extensions", 26 | "--extensionDevelopmentPath=${workspaceRoot}/out", 27 | "--extensionTestsPath=${workspaceRoot}/out/test" 28 | ], 29 | "env": { 30 | "NODE_ENV": "test" 31 | }, 32 | "sourceMaps": true, 33 | "outFiles": [ 34 | "${workspaceRoot}/out/test/**/*.js" 35 | ], 36 | "preLaunchTask": "npm: pretest" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.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 | "node_modules": true 6 | }, 7 | "search.exclude": { 8 | "out": true // set this to false to include "out" folder in search results 9 | }, 10 | "tslint.autoFixOnSave": true, 11 | "files.insertFinalNewline": true, 12 | "files.trimFinalNewlines": true, 13 | "files.trimTrailingWhitespace": true, 14 | "[typescript][javascript]": { 15 | "editor.defaultFormatter": "vscode.typescript-language-features", 16 | }, 17 | "typescript.format.enable": true, 18 | "typescript.format.semicolons": "remove", 19 | "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": true, 20 | "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": true, 21 | "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, 22 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 23 | "typescript.format.insertSpaceAfterCommaDelimiter": true, 24 | "typescript.format.insertSpaceAfterConstructor": false, 25 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 26 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, 27 | "typescript.format.insertSpaceBeforeFunctionParenthesis": false 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | // A task runner that calls a custom npm script that compiles the extension. 9 | { 10 | "version": "2.0.0", 11 | "tasks": [ 12 | { 13 | "type": "shell", 14 | "label": "npm: watch", 15 | "command": "npm", 16 | "args": [ 17 | "run", 18 | "compile", 19 | "--", 20 | "--watch" 21 | ], 22 | "group": "build", 23 | "presentation": { 24 | "panel": "dedicated", 25 | "reveal": "never" 26 | }, 27 | "problemMatcher": "$esbuild-watch", 28 | "isBackground": true 29 | }, 30 | { 31 | "type": "shell", 32 | "label": "npm: pretest", 33 | "command": "npm", 34 | "args": [ 35 | "run", 36 | "pretest" 37 | ], 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/** 2 | */* 3 | !out/extension.js 4 | !images/logo.png 5 | 6 | !package.json 7 | !README.md 8 | !CHANGELOG.md 9 | !LICENSE 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.13.2] - 2024-05-05 4 | ### Added: 5 | - option to enable extension based on .enablepostfix file [#116](https://github.com/ipatalas/vscode-postfix-ts/issues/116) 6 | 7 | ## [1.13.1] - 2023-09-08 8 | ### Added: 9 | - option to define custom template as array of strings [#115](https://github.com/ipatalas/vscode-postfix-ts/issues/115) 10 | 11 | ## [1.13.0] - 2023-04-23 12 | ### Added: 13 | - `forin` template 14 | - `call` template 15 | ### Changed: 16 | - Updated dependencies to support TypeScript 5.0 17 | 18 | ## [1.12.1] - 2023-03-02 19 | ### Fixed: 20 | - Merged PR [#110](https://github.com/ipatalas/vscode-postfix-ts/pull/110) - fix for #108 and #109 - thx @zardoy for reporting and fixing! 21 | 22 | ## [1.12.0] - 2023-02-01 23 | ### Added: 24 | - [.null & .undefined postfixes in if](https://github.com/ipatalas/vscode-postfix-ts/issues/89) 25 | - [Svelte/Vue/Html support](https://github.com/ipatalas/vscode-postfix-ts/issues/97) - thx @zardoy for great work! 26 | - long awaited `await` template 27 | - option to disable particular built-in templates (see `postfix.disabledBuiltinTemplates`) 28 | ### Fixed: 29 | - [Incorrect '\\\\' escaping](https://github.com/ipatalas/vscode-postfix-ts/issues/94) 30 | - [Doesn't work correctly in complex binary expressions](https://github.com/ipatalas/vscode-postfix-ts/issues/88) 31 | - Merged PR [#100](https://github.com/ipatalas/vscode-postfix-ts/pull/100) - fix for #99 32 | 33 | ## [1.11.3] - 2022-10-10 34 | ### Fixed: 35 | - Fixed binary exrpression regression [#80](https://github.com/ipatalas/vscode-postfix-ts/issues/80) 36 | - Merged PR [#86](https://github.com/ipatalas/vscode-postfix-ts/pull/86) 37 | - [Incorrect JSX behavior](https://github.com/ipatalas/vscode-postfix-ts/issues/82) 38 | - [Incorrect multiline indentation](https://github.com/ipatalas/vscode-postfix-ts/pull/83) 39 | 40 | ## [1.11.2] - 2022-09-21 41 | ### Fixed: 42 | - Oooops, last release was incorrectly published, fixing manually for now - no changes here, only republish. 43 | 44 | ## [1.11.1] - 2022-09-20 45 | ### Fixed: 46 | - [Don't display choice when only one variant is available](https://github.com/ipatalas/vscode-postfix-ts/issues/76) 47 | - [Binary expression with equals regression](https://github.com/ipatalas/vscode-postfix-ts/issues/77) 48 | - Merged PR [#78](https://github.com/ipatalas/vscode-postfix-ts/pull/70) - improvement for fix for #77 49 | 50 | ## [1.11.0] - 2022-09-18 51 | ### Added: 52 | - Infer variable names for some templates [#63](https://github.com/ipatalas/vscode-postfix-ts/issues/63) - many thanks to @zardoy for great ideas and part of the implementation 53 | - Merged PR [#73](https://github.com/ipatalas/vscode-postfix-ts/pull/73) - minor improvement for custom templates management 54 | ### Fixed: 55 | - [Doesn't always work in binary expression](https://github.com/ipatalas/vscode-postfix-ts/issues/67) 56 | - [Templates did not work inside nested method declaration](https://github.com/ipatalas/vscode-postfix-ts/issues/66) 57 | - Merged PR [#70](https://github.com/ipatalas/vscode-postfix-ts/pull/70) - Fix for [#69](https://github.com/ipatalas/vscode-postfix-ts/issues/69) 58 | - [Editor jumping when inserting a snippet](https://github.com/ipatalas/vscode-postfix-ts/pull/73) - credits to @zaradoy again 59 | 60 | ## [1.10.1] - 2022-06-26 61 | ### Changed: 62 | - Merged PR [#61](https://github.com/ipatalas/vscode-postfix-ts/pull/61) - Print render text instead of raw body 63 | ### Fixed: 64 | - [Incorrect insertion when in mid of expression](https://github.com/ipatalas/vscode-postfix-ts/issues/60) 65 | - [Postfixes should be suggested only after .](https://github.com/ipatalas/vscode-postfix-ts/issues/64) 66 | 67 | ## [1.10.0] - 2022-05-22 68 | ### Added: 69 | - Merged PR [#52](https://github.com/ipatalas/vscode-postfix-ts/pull/52) - Bundle extension to reduce size and improve startup time (thanks @jasonwilliams!) 70 | - Merged PR [#54](https://github.com/ipatalas/vscode-postfix-ts/pull/54) - Fancy suggestions with syntax highlighting (thanks @zardoy!) 71 | - Sensible default for custom template description if left empty 72 | ### Fixed: 73 | - [Does not work with strings](https://github.com/ipatalas/vscode-postfix-ts/issues/48) 74 | - [Incorrect expanding of $a.$a with double {{expr}} in snippet](https://github.com/ipatalas/vscode-postfix-ts/issues/55) 75 | 76 | ## [1.9.4] - 2021-05-01 77 | ### Added: 78 | - New option to determine how to merge custom templates if they have the same name (fixes [#40](https://github.com/ipatalas/vscode-postfix-ts/issues/40)) 79 | ### Fixed: 80 | - [TM_CURRENT_LINE can not get correct value](https://github.com/ipatalas/vscode-postfix-ts/issues/45) 81 | 82 | ## [1.9.3] - 2021-02-28 83 | ### Added: 84 | - New `.promisify` template (Type.promisify -> Promise) 85 | ### Fixed: 86 | - ["when" on TypeScript types](https://github.com/ipatalas/vscode-postfix-ts/issues/41) 87 | 88 | ## [1.9.2] - 2020-08-09 89 | ### Fixed: 90 | - [Using .log for object](https://github.com/ipatalas/vscode-postfix-ts/issues/37) 91 | 92 | ## [1.9.1] - 2019-11-17 93 | ### Added: 94 | - Support for two different modes for `.undefined` and `.notundefined` templates. See [#27](https://github.com/ipatalas/vscode-postfix-ts/issues/27) 95 | ### Fixed: 96 | - [Postfix does not work in function expression with variable declaration](https://github.com/ipatalas/vscode-postfix-ts/issues/26) 97 | - [await'ed expression - wrong replacement](https://github.com/ipatalas/vscode-postfix-ts/issues/28) 98 | - [return template does not work in some cases](https://github.com/ipatalas/vscode-postfix-ts/issues/29) 99 | 100 | ## [1.9.0] - 2019-09-25 101 | ### Added: 102 | - Support for multiline expressions! 103 | - New `.new` template for expressions (Type.new -> new Type()) 104 | - Support for `new SomeType().` scope 105 | ### Changed: 106 | - [Show more templates in return statement](https://github.com/ipatalas/vscode-postfix-ts/commit/ba3f09c90a6a7dcffb93fdfbf748c7a1b2b9aa3c#diff-8c49ec2779bc5b36c7347b60d5d79f08) 107 | - [Do not show all templates when expression is a function argument](https://github.com/ipatalas/vscode-postfix-ts/commit/3518a7a75dd75d6dc0320313f11e8b897d86e268#diff-8c49ec2779bc5b36c7347b60d5d79f08) 108 | - [Do not show all templates when expression is inside variable declaration or assignment](https://github.com/ipatalas/vscode-postfix-ts/commit/d1c69a3de69e11c40f89d091c8d438b1e42f5279#diff-8c49ec2779bc5b36c7347b60d5d79f08) 109 | - Description in autocomplete now show the actual replacement instead of abstract one 110 | ### Fixed: 111 | - [Binary expressions did not work when surrounded by brackets](https://github.com/ipatalas/vscode-postfix-ts/commit/52111da175ec3058184e199a5e65ee19fb90a296#diff-579bc502e2c0744db6d55afe38b9f3d9) 112 | - Reload extension only when it's own configuration has been changed ([my bad!](https://github.com/ipatalas/vscode-postfix-ts/commit/8515485bfec38af2723be9b939066b1197725e46)) 113 | 114 | ## [1.8.2] - 2019-09-01 115 | ### Changed: 116 | - Merged PR [#25](https://github.com/ipatalas/vscode-postfix-ts/pull/25) - Enable extension in JSX/TSX by default 117 | 118 | ## [1.8.1] - 2018-10-21 119 | ### Fixed: 120 | - Fixed issue [#17](https://github.com/ipatalas/vscode-postfix-ts/issues/17) - suggestions should not be shown inside comments 121 | 122 | ## [1.8.0] - 2018-07-01 123 | ### Added: 124 | - Merged [#16](https://github.com/ipatalas/vscode-postfix-ts/pull/16) - cast templates (thanks @Coffee0127!) 125 | 126 | ## [1.7.0] - 2018-05-30 127 | ### Added: 128 | - Enable usage of multiple '{{here}}' placeholder in a custom template (PR from @AdrieanKhisbe, thanks!) 129 | 130 | ## [1.6.0] - 2017-06-10 131 | ### Added: 132 | - Support for array access expressions so that `expr[i]` will display suggestions as well 133 | - Support for simple custom templates 134 | 135 | ## [1.5.1] - 2017-05-27 136 | ### Added: 137 | - Fixed issue #9 - snippets always on top of suggestions 138 | 139 | ## [1.5.0] - 2017-05-13 140 | ### Added: 141 | - Ability to activate extension in files other than JS/TS 142 | 143 | ## [1.4.0] - 2017-05-03 144 | ### Improved: 145 | - `not` templates now really invert expressions (issue #7) 146 | 147 | ## [1.3.0] - 2017-04-11 148 | ### Added: 149 | - New `foreach` template (issue #6) and general improvements in `for*` templates 150 | 151 | ## [1.2.0] - 2017-04-09 152 | ### Added: 153 | - `not` template can now negate different parts of the expression (selected from Quick Pick) 154 | 155 | ### Fixed: 156 | - Fixed issue #4 - Console templates are no longer suggested on console expression itself 157 | - Fixed issue #5 - Already negated expressions can now be "un-negated" by using `not` template on them again 158 | 159 | ## [1.1.1] - 2017-04-05 160 | ### Added: 161 | - Support for postfix templates on unary expressions (ie. i++) 162 | 163 | ### Fixed: 164 | - Some fixes after adding basic tests 165 | 166 | ## [1.1.0] - 2017-04-03 167 | ### Added: 168 | - Console templates (PR from @jrieken, thanks!) 169 | 170 | ## [1.0.0] - 2017-04-02 171 | 172 | - Initial release 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Ireneusz Patalas 2 | 3 | All rights reserved. 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-postfix-ts 2 | 3 | [![MarketPlace Tag](https://img.shields.io/visual-studio-marketplace/v/ipatalas.vscode-postfix-ts)](https://marketplace.visualstudio.com/items?itemName=ipatalas.vscode-postfix-ts) 4 | [![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/ipatalas.vscode-postfix-ts)](https://marketplace.visualstudio.com/items?itemName=ipatalas.vscode-postfix-ts) 5 | [![codecov](https://codecov.io/gh/ipatalas/vscode-postfix-ts/branch/develop/graph/badge.svg)](https://codecov.io/gh/ipatalas/vscode-postfix-ts) 6 | 7 | [![Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-$4-orange?logo=buy-me-a-coffee&style=flat)](https://www.buymeacoffee.com/0t1sqOM) 8 | 9 | > Postfix templates for TypeScript/JavaScript 10 | 11 | ## Features 12 | 13 | This extension features postfix templates that can be used to improve productivity. 14 | It's been inspired on former, great [R# extension](https://github.com/controlflow/resharper-postfix) 15 | 16 | I find it annoying to jump the cursor back and forth whenever I want to perform some simple operations. This extension makes it easier. I use this feature on daily basis in C# but was missing it in JS/TS until now. 17 | 18 | A simple animation is worth more than words: 19 | 20 | ![feature X](images/demo.gif) 21 | 22 | It also works pretty well with multiline expressions (v1.9.0+): 23 | 24 | ![feature X](images/demo-multiline.gif) 25 | 26 | There is also a special handling for `.not` template which allows you to select specific expression to invert when having more options: 27 | 28 | ![feature X](images/demo-not.gif) 29 | 30 | All available templates (`expr` means the expression on which the template is applied): 31 | 32 | | Template | Outcome | 33 | | -------: | ------- | 34 | | **.if** | `if (expr)` | 35 | | **.else** | `if (!expr)` | 36 | | **.null** | `if (expr === null)` | 37 | | **.notnull** | `if (expr !== null)` | 38 | | **.undefined** | `if (expr === undefined)` or `if (typeof expr === "undefined")` (see [settings](#Configuration)) | 39 | | **.notundefined** | `if (expr !== undefined)` or `if (typeof expr !== "undefined")` (see [settings](#Configuration))| 40 | | **.for** | `for (let i = 0; i < expr.Length; i++)` | 41 | | **.forof** | `for (const item of expr)` | 42 | | **.forin** | `for (const item in expr)` | 43 | | **.foreach** | `expr.forEach(item => )` | 44 | | **.not** | `!expr` | 45 | | **.return** | `return expr` | 46 | | **.var** | `var name = expr` | 47 | | **.let** | `let name = expr` | 48 | | **.const** | `const name = expr` | 49 | | **.log** | `console.log(expr)` | 50 | | **.error** | `console.error(expr)` | 51 | | **.warn** | `console.warn(expr)` | 52 | | **.cast** | `(expr)` | 53 | | **.castas** | `(expr as SomeType)` | 54 | | **.call** | `{cursor}(expr)` | 55 | | **.new** | `new expr()` | 56 | | **.promisify** | `Promise` | 57 | | **.await** | `await expr` | 58 | 59 | If for any reason you don't like either of those templates you can disable them one by one using `postfix.disabledBuiltinTemplates` setting. 60 | 61 | ## Custom templates (1.6.0 and above) 62 | 63 | You can now add your own templates if the defaults are not enough. This will only work for simple ones as some templates require additional tricky handling. 64 | To configure a template you need to set `postfix.customTemplates` setting. It's an array of the following objects: 65 | 66 | ```JSON 67 | { 68 | "name": "...", 69 | "description": "...", 70 | "body": "...", 71 | "when": ["..."] 72 | } 73 | ``` 74 | 75 | `name` defines what will be the name of the suggestion 76 | `description` will show additional optional description when suggestion panel is opened 77 | `body` defines how the template will work (see below) 78 | `when` defines conditions when the template should be suggested 79 | 80 | ### Template body 81 | 82 | Template body defines how will the expression before the cursor be replaced. Body can be defined either as single string or array of strings. If it's an array then strings will be joined with a newline character. 83 | It supports standard Visual Studio Code [Snippet syntax](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax). 84 | There is also one special placeholder that can be used: 85 | 86 | - `{{expr}}`: this will be replaced by the expression on which the template is applied so for example `!{{expr}}` will simply negate the expression 87 | - this placeholder can have modifiers (`upper`, `lower`, `capitalize`) which can be used in the following way: 88 | ```JSON 89 | { 90 | "name": "useState", 91 | "body": "const [{{expr}}, set{{expr:capitalize}}] = React.useState();", 92 | "description": "const [{{expr}}, set{{expr:capitalize}}] = React.useState();", 93 | "when": [] 94 | } 95 | ``` 96 | 97 | This snippet will have the following outcome (name of the original identifier has been capitalized): 98 | ![capitalize example](images/capitalize.gif) 99 | 100 | ### Template conditions 101 | 102 | `when` condition can be zero or more of the following options: 103 | 104 | - `identifier`: simple identifier, ie. `variableName` (inside an if statement or function call arguments) 105 | - `expression`: can be either a simple expression like `object.property.value` or `array[index]` or a combination of them 106 | - `binary-expression`: a binary expression, ie. `x > 3`, `x * 100`, `x && y` 107 | - `unary-expression`: an unary expression, ie. `!x`, `x++` or `++x` 108 | - `new-expression`: a new expression, ie. `new Type(arg1, arg2)` 109 | - `function-call`: a function call expression, ie. `func()`, `object.method()` and so on 110 | - `type`: type in function/variable definition, ie. `const x: string` 111 | - `string-literal`: string literal, ie. `'a string'` or `"string in double quotes"` 112 | 113 | If no conditions are specified then given template will be available under all possible situations 114 | 115 | ## Infer variable names (1.11.0 and above) 116 | 117 | For `var`/`let`/`const` and `forof`/`foreach` templates the extension will try to infer a better name for the variable based on the subject expression. 118 | For instance `fs.readFile()` expression will result in variable named `file` instead of default `name`. Same applies to `forof`/`foreach` templates, but in this case the extension is trying to figure out a singular form of the subject. Of course this can still be easily changed, it's only a suggestion. 119 | Few examples on the image below: 120 | 121 | ![infer-names](images/infer-names.png) 122 | 123 | If you have ideas for more "patterns" that could be easily handled please create an issue. 124 | 125 | ## Configuration 126 | 127 | This plugin contributes the following [settings](https://code.visualstudio.com/docs/customization/userandworkspace): 128 | 129 | - `postfix.languages`: array of [language identifiers](https://code.visualstudio.com/docs/languages/identifiers) in which the extension will be available. Default value is **['javascript', 'typescript', 'javascriptreact', 'typescriptreact']** 130 | - `postfix.customTemplates`: array of custom template definitions - see [Custom templates (1.6.0 and above)](#custom-templates-160-and-above) 131 | - `postfix.customTemplates.mergeMode`: determines how custom templates are shown if they share the same name with built-in template: 132 | - `append` - both built-in and custom template will be shown 133 | - `override` - only custom template will be shown (it overrides built-in one) 134 | - `postfix.undefinedMode`: determines the behavior of `.undefined` and `.notundefined` templates, either equality comparison or typeof 135 | - `postfix.inferVariableName`: enables variable name inferring 136 | - `postfix.disabledBuiltinTemplates`: allows to disable particular built-in templates (for instance discouraged `var`) 137 | 138 | The `postfix.languages` setting can be used to make the extension available for inline JS/TS which is in other files like **.html**, **.vue** or others. You must still include `javascript` and `typescript` if you want the extension to be available there among the others. 139 | 140 | ## Known issues 141 | 142 | Feel free to open issues for whatever you think may improve the extension's value. New ideas for more templates are also welcome. Most of them are pretty easy to implement. 143 | -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | import * as process from "node:process"; 3 | import * as console from "node:console"; 4 | 5 | const production = process.argv.includes("--production") 6 | const watch = process.argv.includes("--watch") 7 | 8 | /** @type {esbuild.BuildOptions} */ 9 | const options = { 10 | entryPoints: ["./src/extension.ts"], 11 | bundle: true, 12 | outdir: "out", 13 | external: ["vscode"], 14 | format: "cjs", 15 | sourcemap: !production, 16 | minify: production, 17 | platform: "node", 18 | logLevel: 'info', 19 | // needed for debugger 20 | keepNames: true, 21 | // needed for vscode-* deps 22 | mainFields: ['module', 'main'] 23 | }; 24 | 25 | (async function () { 26 | try { 27 | if (watch) { 28 | const ctx = await esbuild.context(options) 29 | await ctx.watch() 30 | } else { 31 | await esbuild.build(options); 32 | } 33 | } catch (error) { 34 | console.error(error) 35 | process.exit(1) 36 | } 37 | })() 38 | -------------------------------------------------------------------------------- /images/capitalize.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipatalas/vscode-postfix-ts/340e758b0ce57b02d279b6023fa391a9e491f675/images/capitalize.gif -------------------------------------------------------------------------------- /images/demo-multiline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipatalas/vscode-postfix-ts/340e758b0ce57b02d279b6023fa391a9e491f675/images/demo-multiline.gif -------------------------------------------------------------------------------- /images/demo-not.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipatalas/vscode-postfix-ts/340e758b0ce57b02d279b6023fa391a9e491f675/images/demo-not.gif -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipatalas/vscode-postfix-ts/340e758b0ce57b02d279b6023fa391a9e491f675/images/demo.gif -------------------------------------------------------------------------------- /images/infer-names.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipatalas/vscode-postfix-ts/340e758b0ce57b02d279b6023fa391a9e491f675/images/infer-names.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipatalas/vscode-postfix-ts/340e758b0ce57b02d279b6023fa391a9e491f675/images/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-postfix-ts", 3 | "displayName": "TS/JS postfix completion", 4 | "description": "Postfix templates for TypeScript/Javascript", 5 | "version": "1.13.2", 6 | "license": "MIT", 7 | "publisher": "ipatalas", 8 | "engines": { 9 | "vscode": "^1.60.0" 10 | }, 11 | "icon": "images/logo.png", 12 | "categories": [ 13 | "Snippets", 14 | "Other" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/ipatalas/vscode-postfix-ts" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/ipatalas/vscode-postfix-ts/issues" 22 | }, 23 | "activationEvents": [ 24 | "onLanguage:javascript", 25 | "onLanguage:typescript", 26 | "onLanguage:javascriptreact", 27 | "onLanguage:typescriptreact", 28 | "onLanguage:vue", 29 | "onLanguage:html", 30 | "onLanguage:svelte", 31 | "workspaceContains:.enable-postfix" 32 | ], 33 | "main": "./out/extension", 34 | "contributes": { 35 | "configuration": { 36 | "title": "Postfix completion", 37 | "properties": { 38 | "postfix.languages": { 39 | "type": "array", 40 | "description": "A list of languages in which the completion will be available", 41 | "default": [ 42 | "javascript", 43 | "typescript", 44 | "javascriptreact", 45 | "typescriptreact", 46 | "vue", 47 | "svelte", 48 | "html" 49 | ] 50 | }, 51 | "postfix.undefinedMode": { 52 | "type": "string", 53 | "markdownDescription": "Determines how the `.undefined` and `.notundefined` templates work", 54 | "default": "Equal", 55 | "enum": [ 56 | "Equal", 57 | "Typeof" 58 | ], 59 | "enumDescriptions": [ 60 | "if (expr === undefined)", 61 | "if (typeof expr === \"undefined\")" 62 | ] 63 | }, 64 | "postfix.snippetPreviewMode": { 65 | "type": "string", 66 | "enum": [ 67 | "raw", 68 | "inserted" 69 | ], 70 | "default": "inserted", 71 | "markdownEnumDescriptions": [ 72 | "Raw snippet as you defined in settings.json", 73 | "The inserted text variant" 74 | ] 75 | }, 76 | "postfix.customTemplates": { 77 | "type": "array", 78 | "items": { 79 | "type": "object", 80 | "required": [ 81 | "name", 82 | "body" 83 | ], 84 | "defaultSnippets": [ 85 | { 86 | "label": "New postfix", 87 | "body": { 88 | "name": "$1", 89 | "body": "$2", 90 | "when": [ 91 | "$3" 92 | ] 93 | } 94 | } 95 | ], 96 | "properties": { 97 | "name": { 98 | "type": "string", 99 | "suggestSortText": "0", 100 | "description": "Name of the template. It will be used in auto-complete suggestions" 101 | }, 102 | "description": { 103 | "type": "string", 104 | "description": "Description of the template. It will be used in auto-complete suggestions" 105 | }, 106 | "body": { 107 | "anyOf": [ 108 | { 109 | "type": "string" 110 | }, 111 | { 112 | "type": "array", 113 | "items": { 114 | "type": "string" 115 | } 116 | } 117 | ], 118 | "markdownDescription": "Body of the template. `{{expr}}` will be replaced with the expression before the cursor" 119 | }, 120 | "when": { 121 | "type": "array", 122 | "description": "Context in which the template should be suggested", 123 | "uniqueItems": true, 124 | "items": { 125 | "type": "string", 126 | "enum": [ 127 | "identifier", 128 | "expression", 129 | "binary-expression", 130 | "unary-expression", 131 | "function-call", 132 | "new-expression", 133 | "string-literal", 134 | "type" 135 | ] 136 | } 137 | } 138 | } 139 | } 140 | }, 141 | "postfix.customTemplate.mergeMode": { 142 | "type": "string", 143 | "markdownDescription": "Determines how custom templates are shown if they share a name with built-in template:\n`append` - both built-in and custom template will be shown\n`override` - only custom template will be shown (it overrides built-in one)", 144 | "default": "append", 145 | "enum": [ 146 | "append", 147 | "override" 148 | ] 149 | }, 150 | "postfix.inferVariableName": { 151 | "type": "boolean", 152 | "markdownDescription": "Try to guess variable names for `var`, `let`, `const`, `forEach` and `forof` templates.", 153 | "default": true 154 | }, 155 | "postfix.disabledBuiltinTemplates": { 156 | "type": "array", 157 | "markdownDescription": "Name all built-in templates that you want to disable, eg. `forof`", 158 | "items": { 159 | "type": "string" 160 | }, 161 | "uniqueItems": true, 162 | "default": [] 163 | } 164 | } 165 | } 166 | }, 167 | "scripts": { 168 | "vscode:prepublish": "node build.mjs --production", 169 | "compile": "node build.mjs", 170 | "pretest": "node tasks.mjs pretest && tsc -p ./", 171 | "test": "cross-env NODE_ENV=test node ./out/test/runTests.js", 172 | "test-with-coverage": "c8 npm test", 173 | "lint": "eslint .", 174 | "package": "vsce package --no-dependencies", 175 | "deploy": "vsce publish" 176 | }, 177 | "devDependencies": { 178 | "@types/lodash": "^4.14.192", 179 | "@types/mocha": "^10.0.1", 180 | "@types/node": "^16.11.7", 181 | "@types/pluralize": "^0.0.29", 182 | "@types/vscode": "^1.60.0", 183 | "@typescript-eslint/eslint-plugin": "^5.57.1", 184 | "@typescript-eslint/parser": "^5.57.1", 185 | "@vscode/test-electron": "^2.3.0", 186 | "@vscode/vsce": "^2.18.0", 187 | "c8": "^7.13.0", 188 | "cross-env": "^7.0.3", 189 | "esbuild": "^0.17.4", 190 | "eslint": "^8.37.0", 191 | "glob": "^8.1.0", 192 | "mocha": "^10.2.0", 193 | "source-map-support": "0.5.21" 194 | }, 195 | "dependencies": { 196 | "lodash": "^4.17.21", 197 | "pluralize": "github:plurals/pluralize#36f03cd2d573fa6d23e12e1529fa4627e2af74b4", 198 | "typescript": "^5.0.4", 199 | "vscode-html-languageservice": "^5.0.4", 200 | "vscode-snippet-parser": "^0.0.5" 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/completionItemBuilder.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import ts = require('typescript') 3 | import { adjustLeadingWhitespace, adjustMultilineIndentation } from './utils/multiline-expressions' 4 | import { SnippetParser } from 'vscode-snippet-parser' 5 | import { getConfigValue } from './utils' 6 | import { IndentInfo } from './template' 7 | 8 | const RegexExpression = '{{expr(?::(upper|lower|capitalize))?}}' 9 | 10 | export class CompletionItemBuilder { 11 | private item: vsc.CompletionItem 12 | private code: string 13 | private node: ts.Node 14 | 15 | private constructor(keyword: string, node: ts.Node, private indentInfo: IndentInfo) { 16 | if (ts.isAwaitExpression(node.parent)) { 17 | node = node.parent 18 | } 19 | 20 | this.node = node 21 | this.item = new vsc.CompletionItem({ label: keyword, description: 'POSTFIX' }, vsc.CompletionItemKind.Snippet) 22 | this.code = adjustMultilineIndentation(node.getText(), indentInfo?.indentSize) 23 | } 24 | 25 | public static create = (keyword: string, node: ts.Node, indentInfo: IndentInfo) => new CompletionItemBuilder(keyword, node, indentInfo) 26 | 27 | public command = (command: vsc.Command) => { 28 | this.item.command = command 29 | return this 30 | } 31 | 32 | public insertText = (insertText?: string) => { 33 | this.item.insertText = insertText 34 | return this 35 | } 36 | 37 | public replace = (replacement: string): CompletionItemBuilder => { 38 | this.addCodeBlockDescription(replacement, this.code.replace(/\\/g, '\\\\')) 39 | 40 | const src = this.node.getSourceFile() 41 | const nodeStart = ts.getLineAndCharacterOfPosition(src, this.node.getStart(src)) 42 | const nodeEnd = ts.getLineAndCharacterOfPosition(src, this.node.getEnd()) 43 | 44 | const rangeToDelete = new vsc.Range( 45 | new vsc.Position(nodeStart.line, nodeStart.character), 46 | new vsc.Position(nodeEnd.line, nodeEnd.character + 1) // accomodate 1 character for the dot 47 | ) 48 | 49 | const useSnippets = /(? { 79 | if (!description) { 80 | return this 81 | } 82 | 83 | this.item.documentation = new vsc.MarkdownString(description) 84 | 85 | return this 86 | } 87 | 88 | private addCodeBlockDescription = (replacement: string, inputCode: string) => { 89 | const addCodeBlock = (md: vsc.MarkdownString) => { 90 | const code = this.replaceExpression(replacement, inputCode) 91 | const snippetPreviewMode = getConfigValue<'raw' | 'inserted'>('snippetPreviewMode') 92 | return md.appendCodeblock(snippetPreviewMode === 'inserted' ? new SnippetParser().text(code) : code, 'ts') 93 | } 94 | 95 | if (!this.item.documentation) { 96 | const md = new vsc.MarkdownString() 97 | addCodeBlock(md) 98 | this.item.documentation = md 99 | } else { 100 | addCodeBlock(this.item.documentation as vsc.MarkdownString) 101 | } 102 | } 103 | 104 | public build = () => this.item 105 | 106 | private replaceExpression = (replacement: string, code: string, customRegex?: string) => { 107 | const re = new RegExp(customRegex || RegexExpression, 'g') 108 | 109 | return replacement.replace(re, (_match, p1) => { 110 | if (p1 && this.filters[p1]) { 111 | return this.filters[p1](code) 112 | } 113 | return code 114 | }) 115 | } 116 | 117 | private filters: { [key: string]: (x: string) => string } = { 118 | 'upper': (x: string) => x.toUpperCase(), 119 | 'lower': (x: string) => x.toLowerCase(), 120 | 'capitalize': (x: string) => x.substring(0, 1).toUpperCase() + x.substring(1), 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import * as vsc from 'vscode' 3 | import * as ts from 'typescript' 4 | import { PostfixCompletionProvider } from './postfixCompletionProvider' 5 | import { notCommand, NOT_COMMAND } from './notCommand' 6 | 7 | let completionProvider: vsc.Disposable 8 | 9 | export function activate(context: vsc.ExtensionContext): void { 10 | registerCompletionProvider(context) 11 | 12 | context.subscriptions.push(vsc.commands.registerTextEditorCommand(NOT_COMMAND, async (editor: vsc.TextEditor, _: vsc.TextEditorEdit, ...args: ts.BinaryExpression[]) => { 13 | const [...expressions] = args 14 | 15 | await notCommand(editor, expressions) 16 | })) 17 | 18 | context.subscriptions.push(vsc.workspace.onDidChangeConfiguration(e => { 19 | if (!e.affectsConfiguration('postfix')) { 20 | return 21 | } 22 | 23 | if (completionProvider) { 24 | const idx = context.subscriptions.indexOf(completionProvider) 25 | context.subscriptions.splice(idx, 1) 26 | completionProvider.dispose() 27 | } 28 | 29 | registerCompletionProvider(context) 30 | })) 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-empty-function 34 | export function deactivate(): void { 35 | } 36 | 37 | function registerCompletionProvider(context: vsc.ExtensionContext) { 38 | const provider = new PostfixCompletionProvider() 39 | 40 | const TESTS_SELECTOR: vsc.DocumentSelector = ['postfix', 'html'] 41 | const DOCUMENT_SELECTOR: vsc.DocumentSelector = 42 | process.env.NODE_ENV === 'test' ? TESTS_SELECTOR : vsc.workspace.getConfiguration('postfix').get('languages') 43 | 44 | completionProvider = vsc.languages.registerCompletionItemProvider(DOCUMENT_SELECTOR, provider, '.') 45 | context.subscriptions.push(completionProvider) 46 | } 47 | -------------------------------------------------------------------------------- /src/htmlLikeSupport.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import { getLanguageService, TokenType } from 'vscode-html-languageservice' 3 | 4 | const languageService = getLanguageService() 5 | 6 | const getHtmlLikeEmbedRange = (document: vsc.TextDocument, cursorOffset: number): undefined | null | Record<'start' | 'end', number> => { 7 | const html = languageService.parseHTMLDocument({ ...document, uri: undefined }) 8 | const node = html.findNodeAt(cursorOffset) 9 | 10 | let mostTopNode = node 11 | while (mostTopNode?.tag) { 12 | if (mostTopNode.tag === 'style') { 13 | return null 14 | } 15 | if (mostTopNode.tag === 'script') { 16 | const { startTagEnd, endTagStart } = mostTopNode 17 | return { 18 | start: startTagEnd, 19 | end: endTagStart 20 | } 21 | } 22 | mostTopNode = mostTopNode.parent 23 | } 24 | // vue: not sure of custom blocks, probably should also be ignored 25 | 26 | const validAttributeRegexps = { 27 | html: /^on/, 28 | vue: /^(?::|@|v-)/ 29 | } 30 | 31 | if (!Object.keys(validAttributeRegexps).includes(document.languageId)) { 32 | return undefined 33 | } 34 | 35 | const scanner = languageService.createScanner(document.getText()) 36 | 37 | let scannedTokens = 0 38 | let attrName: string | undefined 39 | let attrValue: string | undefined 40 | while (scanner.scan() !== TokenType.EOS) { 41 | const tokenEnd = scanner.getTokenEnd() 42 | const tokenType = scanner.getTokenType() 43 | if ([TokenType.DelimiterAssign, TokenType.Whitespace].includes(tokenType)) { 44 | continue 45 | } 46 | if (tokenType === TokenType.AttributeValue && cursorOffset > scanner.getTokenOffset() && cursorOffset < tokenEnd) { 47 | attrValue = scanner.getTokenText() 48 | break 49 | } else { 50 | attrName = tokenType === TokenType.AttributeName ? scanner.getTokenText() : undefined 51 | } 52 | 53 | if (tokenEnd > cursorOffset) { 54 | break 55 | } 56 | if (scannedTokens++ === 3000) { 57 | return null 58 | } 59 | } 60 | 61 | if (attrName !== undefined && attrValue !== undefined) { 62 | // return range without quotes 63 | if (validAttributeRegexps[document.languageId].test(attrName)) { 64 | return { start: scanner.getTokenOffset() + 1, end: scanner.getTokenEnd() - 2 } 65 | } else { 66 | return null 67 | } 68 | } 69 | 70 | return undefined 71 | } 72 | 73 | export const getHtmlLikeEmbedText = (document: vsc.TextDocument, cursorOffset: number): string | null => { 74 | const handleRange = getHtmlLikeEmbedRange(document, cursorOffset) 75 | if (!handleRange) { 76 | return null 77 | } 78 | const { start, end } = handleRange 79 | const fullText = document.getText() 80 | return fullText.slice(0, start).replaceAll(/[^\n]/g, ' ') + fullText.slice(start, end) + fullText.slice(end).replaceAll(/[^\n]/g, ' ') 81 | } 82 | -------------------------------------------------------------------------------- /src/notCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import * as ts from 'typescript' 3 | import { invertExpression } from './utils/invert-expression' 4 | 5 | export const NOT_COMMAND = 'complete.notTemplate' 6 | 7 | export function notCommand(editor: vsc.TextEditor, expressions: ts.BinaryExpression[]) { 8 | return vsc.window.showQuickPick(expressions.map(node => ({ 9 | label: node.getText().replace(/\s+/g, ' '), 10 | description: '', 11 | detail: 'Invert this expression', 12 | node 13 | }))) 14 | .then(value => { 15 | if (!value) { 16 | return undefined 17 | } 18 | 19 | editor.edit(e => { 20 | const node = value.node 21 | 22 | const src = node.getSourceFile() 23 | const nodeStart = ts.getLineAndCharacterOfPosition(src, node.getStart(src)) 24 | const nodeEnd = ts.getLineAndCharacterOfPosition(src, node.getEnd()) 25 | 26 | const range = new vsc.Range( 27 | new vsc.Position(nodeStart.line, nodeStart.character), 28 | new vsc.Position(nodeEnd.line, nodeEnd.character + 1) // accomodate 1 character for the dot 29 | ) 30 | 31 | e.delete(range) 32 | e.insert(range.start, invertExpression(value.node)) 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/postfixCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import * as ts from 'typescript' 3 | 4 | import { IndentInfo, IPostfixTemplate } from './template' 5 | import { AllTabs, AllSpaces } from './utils/multiline-expressions' 6 | import { loadBuiltinTemplates, loadCustomTemplates } from './utils/templates' 7 | import { findNodeAtPosition } from './utils/typescript' 8 | import { CustomTemplate } from './templates/customTemplate' 9 | import { getHtmlLikeEmbedText } from './htmlLikeSupport' 10 | 11 | let currentSuggestion = undefined 12 | 13 | export const overrideTsxEnabled = { value: false } 14 | 15 | export class PostfixCompletionProvider implements vsc.CompletionItemProvider { 16 | private templates: IPostfixTemplate[] = [] 17 | private customTemplateNames: string[] = [] 18 | private mergeMode: 'append' | 'override' 19 | 20 | constructor() { 21 | this.mergeMode = vsc.workspace.getConfiguration('postfix.customTemplate').get('mergeMode', 'append') 22 | 23 | const customTemplates = loadCustomTemplates() 24 | this.customTemplateNames = customTemplates.map(t => t.templateName) 25 | 26 | this.templates = [ 27 | ...loadBuiltinTemplates(), 28 | ...customTemplates 29 | ] 30 | } 31 | 32 | provideCompletionItems(document: vsc.TextDocument, position: vsc.Position, _token: vsc.CancellationToken): vsc.CompletionItem[] | vsc.CompletionList | Thenable { 33 | const line = document.lineAt(position.line) 34 | const dotIdx = line.text.lastIndexOf('.', position.character - 1) 35 | const wordRange = document.getWordRangeAtPosition(position) 36 | const isCursorOnWordAfterDot = (wordRange?.start ?? position).character === dotIdx + 1 37 | 38 | if (dotIdx === -1 || !isCursorOnWordAfterDot) { 39 | return [] 40 | } 41 | 42 | const { currentNode, fullSource, fullCurrentNode } = this.getNodeBeforeTheDot(document, position, dotIdx) 43 | 44 | if (!currentNode || this.shouldBeIgnored(fullSource, position)) { 45 | return [] 46 | } 47 | 48 | const indentInfo = this.getIndentInfo(document, currentNode) 49 | const node = this.isTypeReference(fullCurrentNode) ? fullCurrentNode : currentNode 50 | const replacementNode = this.getNodeForReplacement(node) 51 | 52 | try { 53 | return this.templates 54 | .filter(t => { 55 | let canUseTemplate = t.canUse(ts.isNonNullExpression(node) ? node.expression : node) 56 | 57 | if (this.mergeMode === 'override') { 58 | canUseTemplate &&= (t instanceof CustomTemplate || !this.customTemplateNames.includes(t.templateName)) 59 | } 60 | 61 | return canUseTemplate 62 | }) 63 | .flatMap(t => t.buildCompletionItem(replacementNode, indentInfo)) 64 | } catch (err) { 65 | console.error('Error while building postfix autocomplete items:') 66 | console.error(err) 67 | 68 | return [] 69 | } 70 | } 71 | 72 | resolveCompletionItem(item: vsc.CompletionItem, _token: vsc.CancellationToken): vsc.ProviderResult { 73 | currentSuggestion = (item.label as vsc.CompletionItemLabel)?.label || item.label 74 | return item 75 | } 76 | 77 | private isTypeReference = (node: ts.Node) => { 78 | const typeRef = ts.findAncestor(node, ts.isTypeReferenceNode) 79 | return !!typeRef 80 | } 81 | 82 | private getNodeForReplacement = (node: ts.Node) => { 83 | if (ts.isTemplateSpan(node)) { 84 | return node.parent 85 | } 86 | 87 | if (ts.isPrefixUnaryExpression(node.parent) || ts.isPropertyAccessExpression(node.parent)) { 88 | return node.parent 89 | } 90 | 91 | if (ts.isQualifiedName(node.parent)) { 92 | const typeRef = ts.findAncestor(node, ts.isTypeReferenceNode) 93 | 94 | if (ts.isQualifiedName(typeRef.typeName)) { 95 | return typeRef.typeName.left 96 | } 97 | 98 | return typeRef 99 | } 100 | 101 | return node 102 | } 103 | 104 | private getHtmlLikeEmbeddedText(document: vsc.TextDocument, position: vsc.Position) { 105 | const knownHtmlLikeLangs = [ 106 | 'html', 107 | 'vue', 108 | 'svelte' 109 | ] 110 | 111 | if (knownHtmlLikeLangs.includes(document.languageId)) { 112 | return getHtmlLikeEmbedText(document, document.offsetAt(position)) 113 | } 114 | 115 | return undefined 116 | } 117 | 118 | private getNodeBeforeTheDot(document: vsc.TextDocument, position: vsc.Position, dotIdx: number) { 119 | const dotOffset = document.offsetAt(position.with({ character: dotIdx })) 120 | const speciallyHandledText = this.getHtmlLikeEmbeddedText(document, position) 121 | 122 | if (speciallyHandledText === null) { 123 | return {} 124 | } 125 | 126 | const fullText = speciallyHandledText ?? document.getText() 127 | const codeBeforeTheDot = fullText.slice(0, dotOffset) 128 | 129 | const scriptKind = this.convertToScriptKind(document) 130 | const source = ts.createSourceFile('test.ts', codeBeforeTheDot, ts.ScriptTarget.ESNext, true, scriptKind) 131 | const fullSource = ts.createSourceFile('test.ts', fullText, ts.ScriptTarget.ESNext, true, scriptKind) 132 | 133 | const typedTemplate = document.getText(document.getWordRangeAtPosition(position)) 134 | 135 | const findNormalizedNode = (source: ts.SourceFile) => { 136 | const beforeTheDotPosition = ts.getPositionOfLineAndCharacter(source, position.line, dotIdx - 1) 137 | let node = findNodeAtPosition(source, beforeTheDotPosition) 138 | if (node && ts.isIdentifier(node) && ts.isPropertyAccessExpression(node.parent) 139 | && (node.parent.name.text != typedTemplate || ts.isPrefixUnaryExpression(node.parent.parent))) { 140 | node = node.parent 141 | } 142 | return node 143 | } 144 | 145 | return { currentNode: findNormalizedNode(source), fullSource, fullCurrentNode: findNormalizedNode(fullSource) } 146 | } 147 | 148 | private convertToScriptKind(document: vsc.TextDocument) { 149 | if (overrideTsxEnabled.value) { 150 | return ts.ScriptKind.TSX 151 | } 152 | switch (document.languageId) { 153 | case 'javascript': 154 | return ts.ScriptKind.JS 155 | case 'typescript': 156 | return ts.ScriptKind.TS 157 | case 'javascriptreact': 158 | return ts.ScriptKind.JSX 159 | case 'typescriptreact': 160 | return ts.ScriptKind.TSX 161 | default: 162 | return ts.ScriptKind.Unknown 163 | } 164 | } 165 | 166 | private getIndentInfo(document: vsc.TextDocument, node: ts.Node): IndentInfo { 167 | const source = node.getSourceFile() 168 | const position = ts.getLineAndCharacterOfPosition(source, node.getStart(source)) 169 | 170 | const line = document.lineAt(position.line) 171 | const whitespaces = line.text.substring(0, line.firstNonWhitespaceCharacterIndex) 172 | let indentSize = 0 173 | 174 | if (AllTabs.test(whitespaces)) { 175 | indentSize = whitespaces.length 176 | } else if (AllSpaces.test(whitespaces)) { 177 | indentSize = whitespaces.length / (vsc.window.activeTextEditor.options.tabSize as number) 178 | } 179 | 180 | return { 181 | indentSize, 182 | leadingWhitespace: whitespaces 183 | } 184 | } 185 | 186 | private shouldBeIgnored(fullSource: ts.SourceFile, position: vsc.Position) { 187 | const pos = fullSource.getPositionOfLineAndCharacter(position.line, position.character) 188 | const node = findNodeAtPosition(fullSource, pos) 189 | 190 | return node && (isComment(node) || isJsx(node)) 191 | 192 | function isComment(node: ts.Node) { 193 | return [ 194 | ts.SyntaxKind.JSDocComment, 195 | ts.SyntaxKind.JSDoc, 196 | ts.SyntaxKind.MultiLineCommentTrivia, 197 | ts.SyntaxKind.SingleLineCommentTrivia 198 | ].includes(node.kind) 199 | } 200 | 201 | function isJsx(node: ts.Node) { 202 | const jsx = ts.findAncestor(node, ts.isJsxElement) 203 | const jsxFragment = ts.findAncestor(node, ts.isJsxFragment) 204 | const jsxExpression = ts.findAncestor(node, ts.isJsxExpression) 205 | 206 | return (!!jsx || !!jsxFragment) && !jsxExpression 207 | } 208 | } 209 | } 210 | 211 | export const getCurrentSuggestion = () => currentSuggestion 212 | export const resetCurrentSuggestion = () => currentSuggestion = undefined 213 | -------------------------------------------------------------------------------- /src/template.d.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import * as ts from 'typescript' 3 | 4 | export interface IPostfixTemplate { 5 | readonly templateName: string 6 | 7 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo): vsc.CompletionItem 8 | 9 | canUse(node: ts.Node): boolean 10 | } 11 | 12 | export interface IndentInfo { 13 | indentSize?: number 14 | /** Leading whitespace characters of the first line of replacing node */ 15 | leadingWhitespace?: string 16 | } 17 | -------------------------------------------------------------------------------- /src/templates/awaitTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { IndentInfo } from '../template' 4 | import { BaseExpressionTemplate } from './baseTemplates' 5 | 6 | export class AwaitTemplate extends BaseExpressionTemplate { 7 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 8 | return CompletionItemBuilder 9 | .create('await', node, indentInfo) 10 | .replace('await {{expr}}$0') 11 | .build() 12 | } 13 | 14 | override canUse(node: ts.Node) { 15 | return !this.isTypeNode(node) && !this.inAssignmentStatement(node) 16 | && !this.isBinaryExpression(node) && !this.inAwaitedExpression(node) && 17 | (this.isIdentifier(node) || 18 | this.isExpression(node) || 19 | this.isCallExpression(node)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/templates/baseTemplates.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import * as vsc from 'vscode' 3 | import { IndentInfo, IPostfixTemplate } from '../template' 4 | import { isAssignmentBinaryExpression, isStringLiteral } from '../utils/typescript' 5 | 6 | export abstract class BaseTemplate implements IPostfixTemplate { 7 | constructor(public readonly templateName: string) {} 8 | 9 | abstract buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo): vsc.CompletionItem 10 | abstract canUse(node: ts.Node): boolean 11 | 12 | protected isSimpleExpression = (node: ts.Node) => ts.isExpressionStatement(node) && !isStringLiteral(node) 13 | protected isPropertyAccessExpression = (node: ts.Node) => ts.isPropertyAccessExpression(node) 14 | protected isElementAccessExpression = (node: ts.Node) => ts.isElementAccessExpression(node) 15 | protected isExpression = (node: ts.Node) => this.isSimpleExpression(node) || this.isPropertyAccessExpression(node) || this.isElementAccessExpression(node) 16 | protected isIdentifier = (node: ts.Node) => ts.isIdentifier(node) && !this.inTypeReference(node.parent) 17 | 18 | protected isUnaryExpression = (node: ts.Node) => ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) 19 | protected isCallExpression = (node: ts.Node) => ts.isCallExpression(node) 20 | protected isNewExpression = (node: ts.Node) => ts.isNewExpression(node) 21 | protected inFunctionArgument = (node: ts.Node) => ts.isCallExpression(node.parent) && node.parent.arguments.includes(node as ts.Expression) 22 | 23 | protected isObjectLiteral = (node: ts.Node) => { 24 | return ts.isBlock(node) && (node.statements.length === 0 || node.statements.some(x => ts.isLabeledStatement(x))) 25 | } 26 | 27 | protected isTypeNode = (node: ts.Node) => { 28 | if (ts.isTypeNode(node)) { // built-in types 29 | return true 30 | } 31 | 32 | // Custom types (including namespaces) are encapsulated in TypeReferenceNode 33 | return node.parent && this.inTypeReference(node.parent) 34 | } 35 | 36 | protected inAwaitedExpression = (node: ts.Node) => { 37 | if (this.isAnyFunction(node)) { 38 | return false 39 | } 40 | return node.kind === ts.SyntaxKind.AwaitExpression || (node.parent && this.inAwaitedExpression(node.parent)) 41 | } 42 | 43 | protected inReturnStatement = (node: ts.Node) => { 44 | if (this.isAnyFunction(node)) { 45 | return false 46 | } 47 | return node.kind === ts.SyntaxKind.ReturnStatement || (node.parent && this.inReturnStatement(node.parent)) 48 | } 49 | 50 | protected inVariableDeclaration = (node: ts.Node) => { 51 | if (this.isAnyFunction(node)) { 52 | return false 53 | } 54 | 55 | return node.kind === ts.SyntaxKind.VariableDeclaration || node.parent && this.inVariableDeclaration(node.parent) 56 | } 57 | 58 | protected isBinaryExpression = (node: ts.Node) => { 59 | if (ts.isBinaryExpression(node) && !isAssignmentBinaryExpression(node)) { 60 | return true 61 | } 62 | 63 | return ts.isParenthesizedExpression(node) && ts.isBinaryExpression(node.expression) 64 | || node.parent && this.isBinaryExpression(node.parent) 65 | } 66 | 67 | protected unwindBinaryExpression = (node: ts.Node, removeParens = true) => { 68 | let binaryExpression = removeParens && ts.isParenthesizedExpression(node) && ts.isBinaryExpression(node.expression) 69 | ? node.expression 70 | : ts.findAncestor(node, ts.isBinaryExpression) 71 | 72 | while (binaryExpression && ts.isBinaryExpression(binaryExpression.parent)) { 73 | binaryExpression = binaryExpression.parent 74 | } 75 | 76 | if (binaryExpression && !isAssignmentBinaryExpression(binaryExpression)) { 77 | return binaryExpression 78 | } 79 | 80 | return node 81 | } 82 | 83 | protected isAnyFunction = (node: ts.Node) => { 84 | return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) 85 | } 86 | 87 | protected inAssignmentStatement = (node: ts.Node) => { 88 | if (this.isAnyFunction(node)) { 89 | return false 90 | } 91 | 92 | if (ts.isBinaryExpression(node)) { 93 | return isAssignmentBinaryExpression(node) 94 | } 95 | 96 | return node.parent && this.inAssignmentStatement(node.parent) 97 | } 98 | 99 | protected inIfStatement = (node: ts.Node, expressionNode?: ts.Node) => { 100 | if (ts.isIfStatement(node)) { 101 | return !expressionNode || node.expression === expressionNode 102 | } 103 | 104 | return node.parent && this.inIfStatement(node.parent, node) 105 | } 106 | 107 | protected inTypeReference = (node: ts.Node) => { 108 | if (ts.isTypeReferenceNode(node)) { 109 | return true 110 | } 111 | 112 | return node.parent && this.inTypeReference(node.parent) 113 | } 114 | } 115 | 116 | export abstract class BaseExpressionTemplate extends BaseTemplate { 117 | abstract override buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) 118 | 119 | canUse(node: ts.Node) { 120 | return !this.inIfStatement(node) && !this.isTypeNode(node) && !this.inAssignmentStatement(node) && 121 | (this.isIdentifier(node) || 122 | this.isExpression(node) || 123 | this.isUnaryExpression(node) || 124 | this.isBinaryExpression(node) || 125 | this.isCallExpression(node)) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/templates/callTemplate.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "typescript" 2 | import { IndentInfo } from "../template" 3 | import { BaseTemplate } from "./baseTemplates" 4 | import { CompletionItemBuilder } from "../completionItemBuilder" 5 | 6 | export class CallTemplate extends BaseTemplate { 7 | constructor(private keyword: 'call') { 8 | super(keyword) 9 | } 10 | 11 | override buildCompletionItem(node: Node, indentInfo?: IndentInfo) { 12 | return CompletionItemBuilder.create(this.keyword, node, indentInfo) 13 | .replace('$0({{expr}})') 14 | .build() 15 | } 16 | 17 | override canUse(node: Node) { 18 | return !this.inIfStatement(node) && !this.isTypeNode(node) && 19 | (this.isIdentifier(node) || 20 | this.isExpression(node) || 21 | this.isNewExpression(node) || 22 | this.isUnaryExpression(node) || 23 | this.isBinaryExpression(node) || 24 | this.isCallExpression(node)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/templates/castTemplates.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { IndentInfo } from '../template' 4 | import { BaseExpressionTemplate } from './baseTemplates' 5 | 6 | export class CastTemplate extends BaseExpressionTemplate { 7 | constructor(private keyword: 'cast' | 'castas') { 8 | super(keyword) 9 | } 10 | 11 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 12 | const completionitembuilder = CompletionItemBuilder.create(this.keyword, node, indentInfo) 13 | 14 | if (this.keyword === 'castas') { 15 | return completionitembuilder 16 | .replace('({{expr}} as $1)$0') 17 | .build() 18 | } 19 | 20 | return completionitembuilder 21 | .replace('(<$1>{{expr}})$0') 22 | .build() 23 | } 24 | 25 | override canUse(node: ts.Node) { 26 | return !this.inIfStatement(node) && !this.isTypeNode(node) && 27 | (this.isIdentifier(node) || 28 | this.isExpression(node) || 29 | this.isNewExpression(node) || 30 | this.isUnaryExpression(node) || 31 | this.isBinaryExpression(node) || 32 | this.isCallExpression(node)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/templates/consoleTemplates.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { IndentInfo } from '../template' 4 | import { isStringLiteral } from '../utils/typescript' 5 | import { BaseExpressionTemplate } from './baseTemplates' 6 | 7 | export class ConsoleTemplate extends BaseExpressionTemplate { 8 | 9 | constructor(private level: 'log' | 'warn' | 'error') { 10 | super(level) 11 | } 12 | 13 | buildCompletionItem(node: ts.Node, indentInfo: IndentInfo) { 14 | node = this.unwindBinaryExpression(node) 15 | 16 | return CompletionItemBuilder 17 | .create(this.level, node, indentInfo) 18 | .replace(`console.${this.level}({{expr}})`) 19 | .build() 20 | } 21 | 22 | isConsoleExpression = (node: ts.Node) => ts.isIdentifier(node) && node.text === 'console' 23 | 24 | override canUse(node: ts.Node) { 25 | return (super.canUse(node) || this.isNewExpression(node) || this.isObjectLiteral(node) || isStringLiteral(node)) 26 | && !this.inReturnStatement(node) 27 | && !this.isConsoleExpression(node) 28 | && !this.inFunctionArgument(node) 29 | && !this.inVariableDeclaration(node) 30 | && !this.inAssignmentStatement(node) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/customTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { BaseTemplate } from './baseTemplates' 3 | import { CompletionItemBuilder } from '../completionItemBuilder' 4 | import { IndentInfo } from '../template' 5 | import { isStringLiteral } from '../utils/typescript' 6 | import { CustomTemplateBodyType } from '../utils/templates' 7 | 8 | export class CustomTemplate extends BaseTemplate { 9 | private conditionsMap = new Map boolean>([ 10 | ['type', node => this.isTypeNode(node)], 11 | ['identifier', node => this.isIdentifier(node)], 12 | ['string-literal', node => isStringLiteral(node)], 13 | ['expression', node => this.isExpression(node)], 14 | ['binary-expression', node => this.isBinaryExpression(node)], 15 | ['unary-expression', node => this.isUnaryExpression(node.parent)], 16 | ['new-expression', node => this.isNewExpression(node)], 17 | ['function-call', node => this.isCallExpression(node)] 18 | ]) 19 | 20 | constructor(name: string, private description: string, private body: CustomTemplateBodyType, private when: string[]) { 21 | super(name) 22 | } 23 | 24 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 25 | if (this.when.includes('binary-expression')) { 26 | node = this.unwindBinaryExpression(node) 27 | } 28 | 29 | const body = Array.isArray(this.body) ? this.body.join('\n') : this.body 30 | 31 | return CompletionItemBuilder 32 | .create(this.templateName, node, indentInfo) 33 | .description(this.description) 34 | .replace(body) 35 | .build() 36 | } 37 | 38 | canUse(node: ts.Node): boolean { 39 | return node.parent && (this.when.length === 0 || this.when.some(w => this.condition(node, w))) 40 | } 41 | 42 | condition = (node: ts.Node, when: string) => { 43 | const callback = this.conditionsMap.get(when) 44 | return callback && callback(node) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/templates/equalityTemplates.ts: -------------------------------------------------------------------------------- 1 | import ts = require("typescript") 2 | import { CompletionItemBuilder } from "../completionItemBuilder" 3 | import { IndentInfo } from "../template" 4 | import { getConfigValue } from "../utils" 5 | import { BaseTemplate } from "./baseTemplates" 6 | 7 | export class EqualityTemplate extends BaseTemplate { 8 | constructor(private keyword: string, private operator: string, private operand: string, private isUndefinedTemplate?: boolean) { 9 | super(keyword) 10 | } 11 | 12 | override canUse(node: ts.Node) { 13 | return (this.isIdentifier(node) || 14 | this.isExpression(node) || 15 | this.isCallExpression(node)) 16 | && 17 | (this.inReturnStatement(node) || 18 | this.inIfStatement(node) || 19 | this.inVariableDeclaration(node) || 20 | this.inAssignmentStatement(node)) 21 | } 22 | 23 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 24 | const typeOfMode = this.isUndefinedTemplate && getConfigValue('undefinedMode') == 'Typeof' 25 | 26 | return CompletionItemBuilder 27 | .create(this.keyword, node, indentInfo) 28 | .replace(typeOfMode 29 | ? `typeof {{expr}} ${this.operator} "${this.operand}"` 30 | : `{{expr}} ${this.operator} ${this.operand}`) 31 | .build() 32 | } 33 | } -------------------------------------------------------------------------------- /src/templates/forTemplates.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { BaseTemplate } from './baseTemplates' 4 | import { getConfigValue, getIndentCharacters, getPlaceholderWithOptions } from '../utils' 5 | import { inferForVarTemplate } from '../utils/infer-names' 6 | import { IndentInfo } from '../template' 7 | 8 | abstract class BaseForTemplate extends BaseTemplate { 9 | canUse(node: ts.Node): boolean { 10 | return !this.inReturnStatement(node) && 11 | !this.inIfStatement(node) && 12 | !this.inFunctionArgument(node) && 13 | !this.inVariableDeclaration(node) && 14 | !this.inAssignmentStatement(node) && 15 | !this.isTypeNode(node) && 16 | !this.isBinaryExpression(node) && 17 | (this.isIdentifier(node) || 18 | this.isPropertyAccessExpression(node) || 19 | this.isElementAccessExpression(node) || 20 | this.isCallExpression(node) || 21 | this.isArrayLiteral(node)) 22 | } 23 | 24 | protected isArrayLiteral = (node: ts.Node) => node.kind === ts.SyntaxKind.ArrayLiteralExpression 25 | } 26 | 27 | export class ForTemplate extends BaseForTemplate { 28 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 29 | const isAwaited = node.parent && ts.isAwaitExpression(node.parent) 30 | const prefix = isAwaited ? '(' : '' 31 | const suffix = isAwaited ? ')' : '' 32 | 33 | return CompletionItemBuilder 34 | .create('for', node, indentInfo) 35 | .replace(`for (let \${1:i} = 0; \${1} < \${2:${prefix}{{expr}}${suffix}}.length; \${1}++) {\n${getIndentCharacters()}\${0}\n}`) 36 | .build() 37 | } 38 | 39 | override canUse(node: ts.Node) { 40 | return super.canUse(node) 41 | && !this.isArrayLiteral(node) 42 | && !this.isCallExpression(node) 43 | } 44 | } 45 | 46 | export class ForInTemplate extends BaseForTemplate { 47 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 48 | return CompletionItemBuilder 49 | .create('forin', node, indentInfo) 50 | .replace(`for (const \${1:key} in \${2:{{expr}}}) {\n${getIndentCharacters()}\${0}\n}`) 51 | .build() 52 | } 53 | 54 | override canUse(node: ts.Node) { 55 | const isAwaited = node.parent && ts.isAwaitExpression(node.parent) 56 | 57 | return super.canUse(node) && !isAwaited 58 | } 59 | } 60 | 61 | const getArrayItemNames = (node: ts.Node): string[] => { 62 | const inferVarNameEnabled = getConfigValue('inferVariableName') 63 | const suggestedNames = inferVarNameEnabled ? inferForVarTemplate(node) : undefined 64 | return suggestedNames?.length > 0 ? suggestedNames : ['item'] 65 | } 66 | 67 | export class ForOfTemplate extends BaseForTemplate { 68 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 69 | const itemNames = getArrayItemNames(node) 70 | 71 | return CompletionItemBuilder 72 | .create('forof', node, indentInfo) 73 | .replace(`for (const ${getPlaceholderWithOptions(itemNames)} of \${2:{{expr}}}) {\n${getIndentCharacters()}\${0}\n}`) 74 | .build() 75 | } 76 | } 77 | 78 | export class ForEachTemplate extends BaseForTemplate { 79 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 80 | const isAwaited = node.parent && ts.isAwaitExpression(node.parent) 81 | const prefix = isAwaited ? '(' : '' 82 | const suffix = isAwaited ? ')' : '' 83 | const itemNames = getArrayItemNames(node) 84 | 85 | return CompletionItemBuilder 86 | .create('foreach', node, indentInfo) 87 | .replace(`${prefix}{{expr}}${suffix}.forEach(${getPlaceholderWithOptions(itemNames)} => \${2})`) 88 | .build() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/templates/ifTemplates.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { BaseExpressionTemplate } from './baseTemplates' 4 | import { getConfigValue, getIndentCharacters } from '../utils' 5 | import { invertExpression } from '../utils/invert-expression' 6 | import { IndentInfo } from '../template' 7 | 8 | abstract class BaseIfElseTemplate extends BaseExpressionTemplate { 9 | override canUse(node: ts.Node) { 10 | return super.canUse(node) 11 | && !this.inReturnStatement(node) 12 | && !this.inFunctionArgument(node) 13 | && !this.inVariableDeclaration(node) 14 | && !this.inAssignmentStatement(node) 15 | } 16 | } 17 | 18 | export class IfTemplate extends BaseIfElseTemplate { 19 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 20 | node = this.unwindBinaryExpression(node, false) 21 | const replacement = this.unwindBinaryExpression(node, true).getText() 22 | 23 | return CompletionItemBuilder 24 | .create('if', node, indentInfo) 25 | .replace(`if (${replacement}) {\n${getIndentCharacters()}\${0}\n}`) 26 | .build() 27 | } 28 | } 29 | 30 | export class ElseTemplate extends BaseIfElseTemplate { 31 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 32 | node = this.unwindBinaryExpression(node, false) 33 | const replacement = invertExpression(this.unwindBinaryExpression(node, true)) 34 | 35 | return CompletionItemBuilder 36 | .create('else', node, indentInfo) 37 | .replace(`if (${replacement}) {\n${getIndentCharacters()}\${0}\n}`) 38 | .build() 39 | } 40 | } 41 | 42 | export class IfEqualityTemplate extends BaseIfElseTemplate { 43 | constructor(private keyword: string, private operator: string, private operand: string, private isUndefinedTemplate?: boolean) { 44 | super(keyword) 45 | } 46 | 47 | override canUse(node: ts.Node) { 48 | return super.canUse(node) && !this.isBinaryExpression(node) 49 | } 50 | 51 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 52 | const typeOfMode = this.isUndefinedTemplate && getConfigValue('undefinedMode') == 'Typeof' 53 | 54 | return CompletionItemBuilder 55 | .create(this.keyword, node, indentInfo) 56 | .replace(typeOfMode 57 | ? `if (typeof {{expr}} ${this.operator} "${this.operand}") {\n${getIndentCharacters()}\${0}\n}` 58 | : `if ({{expr}} ${this.operator} ${this.operand}) {\n${getIndentCharacters()}\${0}\n}`) 59 | .build() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/templates/newTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { IndentInfo } from '../template' 4 | import { BaseTemplate } from './baseTemplates' 5 | 6 | export class NewTemplate extends BaseTemplate { 7 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 8 | return CompletionItemBuilder 9 | .create('new', node, indentInfo) 10 | .replace('new {{expr}}($0)') 11 | .build() 12 | } 13 | 14 | canUse(node: ts.Node) { 15 | return (this.isIdentifier(node) || this.isPropertyAccessExpression(node)) 16 | && !this.inAwaitedExpression(node.parent) 17 | && !this.isTypeNode(node) 18 | && !this.isBinaryExpression(node) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/templates/notTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { BaseTemplate } from './baseTemplates' 4 | import { NOT_COMMAND } from '../notCommand' 5 | import { invertExpression } from '../utils/invert-expression' 6 | import { IndentInfo } from '../template' 7 | 8 | export class NotTemplate extends BaseTemplate { 9 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 10 | node = this.normalizeBinaryExpression(node) 11 | 12 | const completionBuilder = CompletionItemBuilder 13 | .create('not', node, indentInfo) 14 | 15 | if (this.isBinaryExpression(node)) { 16 | const expressions = this.getBinaryExpressions(node) 17 | if (expressions.length > 1) { 18 | return completionBuilder 19 | .insertText('') 20 | .command({ 21 | title: '', 22 | command: NOT_COMMAND, 23 | arguments: expressions 24 | }) 25 | .description('`!expr` - *[multiple options]*') 26 | .build() 27 | } 28 | } 29 | 30 | const replacement = invertExpression(node, undefined) 31 | return completionBuilder 32 | .replace(replacement) 33 | .build() 34 | } 35 | 36 | canUse(node: ts.Node) { 37 | return !this.isTypeNode(node) && 38 | (this.isExpression(node) 39 | || this.isUnaryExpression(node) 40 | || this.isUnaryExpression(node.parent) 41 | || this.isBinaryExpression(node) 42 | || this.isCallExpression(node) 43 | || this.isIdentifier(node)) 44 | } 45 | 46 | private isStrictEqualityOrInstanceofBinaryExpression = (node: ts.Node) => { 47 | return ts.isBinaryExpression(node) && [ 48 | ts.SyntaxKind.EqualsEqualsEqualsToken, 49 | ts.SyntaxKind.ExclamationEqualsEqualsToken, 50 | ts.SyntaxKind.InstanceOfKeyword 51 | ].includes(node.operatorToken.kind) 52 | } 53 | 54 | private getBinaryExpressions = (node: ts.Node) => { 55 | const possibleExpressions = [node] 56 | 57 | do { 58 | this.isBinaryExpression(node.parent) && possibleExpressions.push(node.parent) 59 | 60 | node = node.parent 61 | } while (node.parent) 62 | 63 | return possibleExpressions 64 | } 65 | 66 | private normalizeBinaryExpression = (node: ts.Node) => { 67 | if (ts.isParenthesizedExpression(node.parent) && ts.isBinaryExpression(node.parent.expression)) { 68 | return node.parent 69 | } 70 | 71 | if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) { 72 | return node 73 | } 74 | 75 | if (this.isStrictEqualityOrInstanceofBinaryExpression(node.parent)) { 76 | return node.parent 77 | } 78 | 79 | return node 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/templates/promisifyTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { IndentInfo } from '../template' 4 | import { BaseTemplate } from './baseTemplates' 5 | 6 | export class PromisifyTemplate extends BaseTemplate { 7 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 8 | return CompletionItemBuilder 9 | .create('promisify', node, indentInfo) 10 | .replace('Promise<{{expr}}>') 11 | .build() 12 | } 13 | 14 | canUse(node: ts.Node) { 15 | return node.parent && this.isTypeNode(node) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/templates/returnTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { IndentInfo } from '../template' 4 | import { isStringLiteral } from '../utils/typescript' 5 | import { BaseExpressionTemplate } from './baseTemplates' 6 | 7 | export class ReturnTemplate extends BaseExpressionTemplate { 8 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 9 | node = this.unwindBinaryExpression(node) 10 | 11 | return CompletionItemBuilder 12 | .create('return', node, indentInfo) 13 | .replace('return {{expr}}') 14 | .build() 15 | } 16 | 17 | override canUse(node: ts.Node) { 18 | return (super.canUse(node) || this.isNewExpression(node) || this.isObjectLiteral(node) || isStringLiteral(node)) 19 | && !this.inReturnStatement(node) 20 | && !this.inFunctionArgument(node) 21 | && !this.inVariableDeclaration(node) 22 | && !this.inAssignmentStatement(node) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/templates/varTemplates.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import { CompletionItemBuilder } from '../completionItemBuilder' 3 | import { BaseExpressionTemplate } from './baseTemplates' 4 | import { getConfigValue, getPlaceholderWithOptions } from '../utils' 5 | import { inferVarTemplateName } from '../utils/infer-names' 6 | import { IndentInfo } from '../template' 7 | import { isStringLiteral } from '../utils/typescript' 8 | 9 | export class VarTemplate extends BaseExpressionTemplate { 10 | constructor(private keyword: 'var' | 'let' | 'const') { 11 | super(keyword) 12 | } 13 | 14 | buildCompletionItem(node: ts.Node, indentInfo?: IndentInfo) { 15 | node = this.unwindBinaryExpression(node) 16 | 17 | const inferVarNameEnabled = getConfigValue('inferVariableName') 18 | const suggestedVarNames = (inferVarNameEnabled ? inferVarTemplateName(node) : undefined) ?? ['name'] 19 | 20 | return CompletionItemBuilder 21 | .create(this.keyword, node, indentInfo) 22 | .replace(`${this.keyword} ${getPlaceholderWithOptions(suggestedVarNames)} = {{expr}}$0`) 23 | .build() 24 | } 25 | 26 | override canUse(node: ts.Node) { 27 | return (super.canUse(node) || this.isNewExpression(node) || this.isObjectLiteral(node) || isStringLiteral(node)) 28 | && !this.inReturnStatement(node) 29 | && !this.inFunctionArgument(node) 30 | && !this.inVariableDeclaration(node) 31 | && !this.inAssignmentStatement(node) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | 3 | export const getIndentCharacters = () => { 4 | if (vsc.window.activeTextEditor.options.insertSpaces) { 5 | return ' '.repeat(vsc.window.activeTextEditor.options.tabSize as number) 6 | } else { 7 | return '\t' 8 | } 9 | } 10 | 11 | export const getConfigValue = (name: string): Type | undefined => { 12 | return vsc.workspace.getConfiguration('postfix', null).get(name) 13 | } 14 | 15 | export const getPlaceholderWithOptions = (options: string[], placeholderNumber = 1) => { 16 | if (options.length > 1) { 17 | return `\${${placeholderNumber}|${options.join(',')}|}` 18 | } 19 | 20 | return `\${${placeholderNumber}:${options[0]}}` 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/infer-names.ts: -------------------------------------------------------------------------------- 1 | import _ = require("lodash") 2 | import pluralize = require("pluralize") 3 | import ts = require("typescript") 4 | 5 | const MethodCallRegex = /^(get|read|create|retrieve|select|modify|update|use|find)(?[A-Z].+?)?$/ 6 | const CleanNameRegex = /((By|With|From).*$)|(Sync$)|.*(?=Items|Lines$)/ 7 | 8 | const lowerFirst = (name: string) => name && _.lowerFirst(name) 9 | 10 | export const inferVarTemplateName = (node: ts.Node): string[] => { 11 | if (ts.isNewExpression(node)) { 12 | return [lowerFirst(inferNewExpressionVar(node))] 13 | } else if (ts.isCallExpression(node)) { 14 | const methodName = getMethodName(node) 15 | const name = beautifyMethodName(methodName) 16 | if (!name) { 17 | return 18 | } 19 | 20 | return getUniqueVariants(name).map(lowerFirst) 21 | } 22 | } 23 | 24 | export const inferForVarTemplate = (node: ts.Node): string[] => { 25 | const subjectName = getForExpressionName(node) 26 | if (!subjectName) { 27 | return 28 | } 29 | 30 | const clean = ts.isCallExpression(node) 31 | ? beautifyMethodName(subjectName) 32 | : subjectName.replace(/^(?:all)?(.+?)(?:List)?$/, "$1") 33 | 34 | return getUniqueVariants(clean) 35 | .map(pluralize.singular) 36 | .filter(x => x !== clean) 37 | .map(lowerFirst) 38 | } 39 | 40 | function getUniqueVariants(name?: string) { 41 | const cleanerVariant = name?.replace(CleanNameRegex, '') 42 | const uniqueValues = [...new Set([cleanerVariant, name])] 43 | return uniqueValues.filter(x => x) 44 | } 45 | 46 | function beautifyMethodName(name: string) { 47 | return MethodCallRegex.exec(name)?.groups?.name 48 | } 49 | 50 | function getForExpressionName(node: ts.Node) { 51 | if (ts.isIdentifier(node)) { 52 | return node.text 53 | } else if (ts.isPropertyAccessExpression(node)) { 54 | return node.name.text 55 | } else if (ts.isCallExpression(node)) { 56 | return getMethodName(node) 57 | } 58 | } 59 | 60 | function getMethodName(node: ts.CallExpression) { 61 | if (ts.isIdentifier(node.expression)) { 62 | return node.expression.text 63 | } else if (ts.isPropertyAccessExpression(node.expression)) { 64 | return node.expression.name.text 65 | } 66 | } 67 | 68 | function inferNewExpressionVar(node: ts.NewExpression) { 69 | if (ts.isIdentifier(node.expression)) { 70 | return node.expression.text 71 | } else if (ts.isPropertyAccessExpression(node.expression)) { 72 | return node.expression.name.text 73 | } 74 | } -------------------------------------------------------------------------------- /src/utils/invert-expression.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | 3 | const operatorMapping = new Map([ 4 | [ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsToken], 5 | [ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken], 6 | [ts.SyntaxKind.GreaterThanEqualsToken, ts.SyntaxKind.LessThanToken], 7 | [ts.SyntaxKind.GreaterThanToken, ts.SyntaxKind.LessThanEqualsToken] 8 | ]) 9 | 10 | const reverseMapping = new Map() 11 | operatorMapping.forEach((v, k) => reverseMapping.set(v, k)) 12 | 13 | const logicalOperatorMapping = new Map([ 14 | [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken], 15 | [ts.SyntaxKind.BarBarToken, ts.SyntaxKind.AmpersandAmpersandToken] 16 | ]) 17 | 18 | export const invertBinaryExpression = (expr: ts.BinaryExpression, addOrBrackets = false): string => { 19 | let op = operatorMapping.get(expr.operatorToken.kind) || reverseMapping.get(expr.operatorToken.kind) 20 | if (op) { 21 | return `${expr.left.getText()} ${ts.tokenToString(op)} ${expr.right.getText()}` 22 | } 23 | 24 | op = logicalOperatorMapping.get(expr.operatorToken.kind) 25 | if (op) { 26 | const left = invertExpression(expr.left, op !== ts.SyntaxKind.BarBarToken) 27 | const right = invertExpression(expr.right, op !== ts.SyntaxKind.BarBarToken) 28 | const match = /^\s+/.exec(expr.right.getFullText()) 29 | const leadingWhitespaces = match ? match[0] : ' ' 30 | 31 | const result = `${left} ${ts.tokenToString(op)}${leadingWhitespaces + right}` 32 | 33 | return addOrBrackets && op === ts.SyntaxKind.BarBarToken ? `(${result})` : result 34 | } 35 | } 36 | 37 | export const invertExpression = (expr: ts.Node, addOrBrackets = false) => { 38 | // !(expr) => expr 39 | if (ts.isPrefixUnaryExpression(expr) && expr.operator === ts.SyntaxKind.ExclamationToken) { 40 | if (ts.isParenthesizedExpression(expr.operand) && ts.isBinaryExpression(expr.operand.expression)) { 41 | return expr.operand.expression.getText() 42 | } 43 | } 44 | 45 | // (x > y) => (x <= y) 46 | if (ts.isParenthesizedExpression(expr) && ts.isBinaryExpression(expr.expression)) { 47 | const result = invertBinaryExpression(expr.expression, addOrBrackets) 48 | if (result) { 49 | return `(${result})` 50 | } 51 | } 52 | 53 | const text = expr.getText() 54 | 55 | if (ts.isBinaryExpression(expr)) { 56 | // x > y => x <= y 57 | const result = invertBinaryExpression(expr, addOrBrackets) 58 | if (result) { 59 | return result 60 | } 61 | 62 | return text.startsWith('!') ? text.substring(1) : `!(${text})` 63 | } 64 | 65 | return text.startsWith('!') ? text.substring(1) : `!${text}` 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/multiline-expressions.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | 3 | export const AllTabs = /^\t+$/ 4 | export const AllSpaces = /^ +$/ 5 | 6 | export function adjustMultilineIndentation(code: string, indentSize?: number) { 7 | if (!indentSize) { 8 | return code 9 | } 10 | 11 | const reNewLine = /\r?\n/ 12 | const lines = code.split(reNewLine) 13 | 14 | if (lines.length === 1) { 15 | return code 16 | } 17 | 18 | const newLine = reNewLine.exec(code)[0] 19 | 20 | return lines.map((line, i) => i > 0 ? stripLineIndent(line, indentSize) : line) 21 | .join(newLine) 22 | } 23 | 24 | function stripLineIndent(line: string, indentSize: number) { 25 | const whitespacesMatch = /^[\t ]+/.exec(line) 26 | if (!whitespacesMatch) { 27 | return line 28 | } 29 | 30 | const whitespaces = whitespacesMatch[0] 31 | 32 | if (AllTabs.test(whitespaces) && indentSize <= whitespaces.length) { 33 | return line.substring(indentSize) 34 | } 35 | 36 | const tabSize = vsc.window.activeTextEditor.options.tabSize as number 37 | 38 | if (AllSpaces.test(whitespaces) && indentSize <= (whitespaces.length / tabSize)) { 39 | return line.substring(indentSize * tabSize) 40 | } 41 | 42 | return line 43 | } 44 | 45 | export const adjustLeadingWhitespace = (content: string, leadingWhitespace = '') => { 46 | return content.split(/\r?\n/).map((line, i) => !i ? line : leadingWhitespace + line).join('\n') 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/templates.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import { IPostfixTemplate } from '../template' 3 | import { AwaitTemplate } from '../templates/awaitTemplate' 4 | import { CastTemplate } from '../templates/castTemplates' 5 | import { ConsoleTemplate } from '../templates/consoleTemplates' 6 | import { CustomTemplate } from '../templates/customTemplate' 7 | import { EqualityTemplate } from '../templates/equalityTemplates' 8 | import { ForTemplate, ForOfTemplate, ForEachTemplate, ForInTemplate } from '../templates/forTemplates' 9 | import { IfTemplate, ElseTemplate, IfEqualityTemplate } from '../templates/ifTemplates' 10 | import { NewTemplate } from '../templates/newTemplate' 11 | import { NotTemplate } from '../templates/notTemplate' 12 | import { PromisifyTemplate } from '../templates/promisifyTemplate' 13 | import { ReturnTemplate } from '../templates/returnTemplate' 14 | import { VarTemplate } from '../templates/varTemplates' 15 | import { CallTemplate } from '../templates/callTemplate' 16 | 17 | export const loadCustomTemplates = () => { 18 | const config = vsc.workspace.getConfiguration('postfix') 19 | const templates = config.get('customTemplates') 20 | if (templates) { 21 | return templates.map(t => new CustomTemplate(t.name, t.description, t.body, t.when)) 22 | } 23 | 24 | return [] 25 | } 26 | 27 | export const loadBuiltinTemplates = () => { 28 | const config = vsc.workspace.getConfiguration('postfix') 29 | const disabledTemplates = config.get('disabledBuiltinTemplates', []) 30 | 31 | const templates: IPostfixTemplate[] = [ 32 | new CastTemplate('cast'), 33 | new CastTemplate('castas'), 34 | new CallTemplate('call'), 35 | new ConsoleTemplate('log'), 36 | new ConsoleTemplate('warn'), 37 | new ConsoleTemplate('error'), 38 | new ForTemplate('for'), 39 | new ForOfTemplate('forof'), 40 | new ForInTemplate('forin'), 41 | new ForEachTemplate('foreach'), 42 | new IfTemplate('if'), 43 | new ElseTemplate('else'), 44 | new IfEqualityTemplate('null', '===', 'null'), 45 | new IfEqualityTemplate('notnull', '!==', 'null'), 46 | new IfEqualityTemplate('undefined', '===', 'undefined', true), 47 | new IfEqualityTemplate('notundefined', '!==', 'undefined', true), 48 | new EqualityTemplate('null', '===', 'null'), 49 | new EqualityTemplate('notnull', '!==', 'null'), 50 | new EqualityTemplate('undefined', '===', 'undefined', true), 51 | new EqualityTemplate('notundefined', '!==', 'undefined', true), 52 | new NewTemplate('new'), 53 | new NotTemplate('not'), 54 | new PromisifyTemplate('promisify'), 55 | new ReturnTemplate('return'), 56 | new VarTemplate('var'), 57 | new VarTemplate('let'), 58 | new VarTemplate('const'), 59 | new AwaitTemplate('await') 60 | ] 61 | 62 | return templates.filter(t => !disabledTemplates.includes(t.templateName)) 63 | } 64 | 65 | export type CustomTemplateBodyType = string | string[] 66 | 67 | interface ICustomTemplateDefinition { 68 | name: string 69 | description: string 70 | body: CustomTemplateBodyType, 71 | when: string[] 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/typescript.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import * as _ from 'lodash' 3 | 4 | export const findNodeAtPosition = (source: ts.SourceFile, character: number): ts.Node | undefined => { 5 | const matchingNodes: INode[] = [] 6 | source.statements.forEach(visitNode) 7 | const sortedNodes = _.orderBy(matchingNodes, [m => m.width, m => m.depth], ['asc', 'desc']) 8 | 9 | if (sortedNodes.length > 0) { 10 | return sortedNodes[0].node 11 | } 12 | 13 | function visitNode(node: ts.Node, depth = 0) { 14 | const start = node.getStart(source) 15 | const end = node.getEnd() 16 | const isToken = ts.isToken(node) && !ts.isIdentifier(node) && !ts.isTypeNode(node) && !isStringLiteral(node) 17 | 18 | if (!isToken && start <= character && character < end) { 19 | matchingNodes.push({ 20 | depth, 21 | node, 22 | width: end - start 23 | }) 24 | } 25 | 26 | node.getChildren(source).forEach(n => visitNode(n, depth + 1)) 27 | } 28 | } 29 | 30 | export const isAssignmentBinaryExpression = (node: ts.BinaryExpression) => { 31 | return [ 32 | ts.SyntaxKind.EqualsToken, 33 | ts.SyntaxKind.PlusEqualsToken, 34 | ts.SyntaxKind.MinusEqualsToken, 35 | ts.SyntaxKind.SlashEqualsToken, 36 | ts.SyntaxKind.AsteriskEqualsToken, 37 | ts.SyntaxKind.AsteriskAsteriskEqualsToken, 38 | ts.SyntaxKind.AmpersandEqualsToken, 39 | // Bitwise assignments 40 | ts.SyntaxKind.BarEqualsToken, 41 | ts.SyntaxKind.BarBarEqualsToken, 42 | ts.SyntaxKind.CaretEqualsToken, 43 | ts.SyntaxKind.LessThanLessThanToken, 44 | ts.SyntaxKind.LessThanLessThanEqualsToken, 45 | ts.SyntaxKind.GreaterThanEqualsToken, 46 | ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, 47 | ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, 48 | // relatively new 49 | ts.SyntaxKind.AmpersandAmpersandEqualsToken, 50 | ts.SyntaxKind.QuestionQuestionEqualsToken, 51 | ts.SyntaxKind.BarBarEqualsToken, 52 | ].includes(node.operatorToken.kind) 53 | } 54 | 55 | export const isStringLiteral = (node: ts.Node) => { 56 | return ts.isTemplateSpan(node) || ts.isStringLiteralLike(node) 57 | || (ts.isExpressionStatement(node) && ts.isStringLiteralLike(node.expression)) 58 | } 59 | 60 | interface INode { 61 | width: number 62 | depth: number 63 | node: ts.Node 64 | } 65 | -------------------------------------------------------------------------------- /tasks.mjs: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import * as process from "node:process"; 3 | import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' 4 | 5 | const LANGUAGE = 'postfix' 6 | 7 | function pretest() { 8 | const pkg = readPackageJson() 9 | pkg.contributes.languages = [{ id: LANGUAGE }] 10 | // Activate the extension right after start to avoid delay and failure in first test 11 | pkg.activationEvents = ['*'] 12 | // Don't use bundler for tests as it breaks template usage tests 13 | pkg.main = './src/extension' 14 | writePackageJson(pkg) 15 | } 16 | 17 | const writePackageJson = (content) => { 18 | mkdirSync('./out', { recursive: true, }) 19 | writeFileSync('./out/package.json', JSON.stringify(content, undefined, '\t')) 20 | } 21 | const readPackageJson = () => JSON.parse(readFileSync('package.json', 'utf8')) 22 | 23 | const taskToExecute = { pretest }[process.argv[2] ?? ''] 24 | taskToExecute?.() 25 | -------------------------------------------------------------------------------- /test/dsl.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from 'os' 2 | 3 | export function parseDSL(input: string) { 4 | const inputLines: string[] = [] 5 | const expectedLines: string[] = [] 6 | let template = '' 7 | let cursorLine = 0 8 | let cursorCharacter = 0 9 | 10 | const lines = input.split(/\r?\n/).filter(l => l.length > 0) 11 | 12 | for (let i = 0; i < lines.length; i++) { 13 | let [input, expected] = lines[i].split('>> ') 14 | input = input.trimEnd() 15 | 16 | const leadingMark = /^\s*\| /.exec(input) 17 | if (leadingMark !== null) { 18 | input = input.replace(leadingMark[0], '') 19 | } 20 | 21 | const match = /(? ' '.repeat(size * TabSize) 6 | 7 | describe('Multiline template tests', () => { 8 | Test(`let template - method call 9 | | object.call() >> let name = object.call() 10 | | \t.anotherCall() >> \t.anotherCall() 11 | | \t.lastOne(){let} >> \t.lastOne()`) 12 | 13 | Test(`let template - method call (equal indentation) 14 | | \tobject.call() >> \tlet name = object.call() 15 | | .anotherCall() >> \t.anotherCall() 16 | | .lastOne(){let} >> \t.lastOne()`) 17 | 18 | Test(`let template - method call (indentation - tabs) 19 | | \t\tobject.call() >> \t\tlet name = object.call() 20 | | \t\t\t.anotherCall() >> \t\t\t.anotherCall() 21 | | \t\t\t.lastOne(){let} >> \t\t\t.lastOne()`) 22 | 23 | // first line gets to keep original indentation in VSCode 24 | Test(`let template - method call (indentation - spaces) 25 | | ${indent(2)}object.call() >> ${indent(2)}let name = object.call() 26 | | ${indent(3)}.anotherCall() >> \t\t\t.anotherCall() 27 | | ${indent(3)}.lastOne(){let} >> \t\t\t.lastOne()`) 28 | 29 | Test(`let template - method call (indentation - mixed) 30 | | \t\tobject.call() >> \t\tlet name = object.call() 31 | | ${indent(3)}.anotherCall() >> \t\t\t.anotherCall() 32 | | \t\t\t.lastOne(){let} >> \t\t\t.lastOne()`) 33 | 34 | Test(`let template - method call (indentation - completely mixed) 35 | | \tobject.call() >> \tlet name = object.call() 36 | | \t .anotherCall() >> \t\t .anotherCall() 37 | | \t .lastOne(){let} >> \t\t .lastOne()`) 38 | 39 | Test(`return template - method call (indentation - tabs) 40 | | \t\tobject.call() >> \t\treturn object.call() 41 | | \t\t\t.anotherCall() >> \t\t\t.anotherCall() 42 | | \t\t\t.lastOne(){return} >> \t\t\t.lastOne()`) 43 | 44 | // first line gets to keep original indentation in VSCode 45 | Test(`return template - method call (indentation - spaces) 46 | | ${indent(2)}object.call() >> ${indent(2)}return object.call() 47 | | ${indent(3)}.anotherCall() >> \t\t\t.anotherCall() 48 | | ${indent(3)}.lastOne(){return} >> \t\t\t.lastOne()`) 49 | 50 | Test(`return template - method call (indentation - mixed) 51 | | \t\tobject.call() >> \t\treturn object.call() 52 | | ${indent(3)}.anotherCall() >> \t\t\t.anotherCall() 53 | | \t\t\t.lastOne(){return} >> \t\t\t.lastOne()`) 54 | 55 | Test(`return template - method call (indentation - completely mixed) 56 | | \tobject.call() >> \treturn object.call() 57 | | \t .anotherCall() >> \t\t .anotherCall() 58 | | \t .lastOne(){return} >> \t\t .lastOne()`) 59 | 60 | Test(`let template - property access expression 61 | | object. >> let name = object. 62 | | \t.a >> \t.a 63 | | \t.b >> \t.b 64 | | \t.c{let} >> \t.c`) 65 | 66 | Test(`let template - unary expression 67 | | object. >> let name = object. 68 | | \t.a >> \t.a 69 | | \t.b >> \t.b 70 | | \t.c++{let} >> \t.c++`) 71 | 72 | Test(`return template - method call (equal indentation) 73 | | \tobject.call() >> \treturn object.call() 74 | | .anotherCall() >> \t.anotherCall() 75 | | .lastOne(){return} >> \t.lastOne()`) 76 | 77 | Test(`return template - new expression 78 | | new Type( >> return new Type( 79 | | \t1, >> \t1, 80 | | \t2, >> \t2, 81 | | \t3){return} >> \t3)`) 82 | 83 | describe('Without {{expr}}', () => { 84 | const run = runWithCustomTemplate('1\n\t1\n1') 85 | 86 | run('expression', `indentation - completely mixed 87 | | \tobject.call() >> \t1 88 | | \t .anotherCall() >> \t\t1 89 | | \t .lastOne{custom} >> \t1`) 90 | }) 91 | 92 | Test(`let template - method call in return object method 93 | | function hoge() { >> function hoge() { 94 | | return { >> return { 95 | | method(){ >> method(){ 96 | | caller(){let} >> let name = caller() 97 | | }, >> }, 98 | | } >> } 99 | | } >> }`) 100 | 101 | Test(`let template - no postfixes incorrect jsx parsing 102 | | const func1 = () => {} >> const func1 = () => {} 103 | | const func2 = () => { >> const func2 = () => { 104 | | let b >> let b 105 | | a = () => { >> a = () => { 106 | | b = c >> b = c 107 | | b{let} >> let name = b 108 | | } >> } 109 | | } >> }`) 110 | 111 | Test(`log template - in arrow function 112 | | test = () => { >> test = () => { 113 | | wrapMe{log} >> console.log(wrapMe) 114 | | } >> }`) 115 | 116 | QuickPick(`not template - whitespaces - first expression 117 | | if ( >> if ( 118 | | a && (b && >> a && (!b || 119 | | a >> !a 120 | | .a >> .a 121 | | .b){not} >> .b) 122 | | ) {} >> ) {}`, false, 0) 123 | 124 | QuickPick(`not template - whitespaces - second expression 125 | | if ( >> if ( 126 | | a && (b && >> !a || (!b || 127 | | a >> !a 128 | | .a >> .a 129 | | .b){not} >> .b) 130 | | ) {} >> ) {}`, false, 1) 131 | }) 132 | -------------------------------------------------------------------------------- /test/extension.singleline.test.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import { runTest as Test, runTestQuickPick as QuickPick, Options } from './runner' 3 | import { describe, before, after } from 'mocha' 4 | import { runWithCustomTemplate } from './utils' 5 | 6 | const config = vsc.workspace.getConfiguration('postfix') 7 | const withTrimWhitespaces: Options = { trimWhitespaces: true } 8 | 9 | describe('Single line template tests', () => { 10 | before(setInferVarName(config, false)) 11 | after(setInferVarName(config, true)) 12 | 13 | Test('not template - already negated expression | !expr{not} >> expr') 14 | Test('let template - binary expression #1 | a * 3{let} >> let name = a * 3') 15 | Test('let template - binary expression #2 | a * b{let} >> let name = a * b') 16 | Test('let template - binary expression - nested | x && a * b{let} >> let name = x && a * b') 17 | Test('let template - non-null as assertion | test!{let} >> let name = test!') 18 | Test('let template - method call | obj.call(){let} >> let name = obj.call()') 19 | Test('let template - method call with non-null | obj.call()!{let} >> let name = obj.call()!') 20 | Test('let template - property access expression | obj.a.b{let} >> let name = obj.a.b') 21 | Test('let template - property access expression!| obj.a.b!{let} >> let name = obj.a.b!') 22 | Test('let template - element access expression | obj.a[b]{let} >> let name = obj.a[b]') 23 | Test('let template - postifx unary operator | counter++{let} >> let name = counter++') 24 | Test('let template - new expression | new Type(1, 2, 3){let} >> let name = new Type(1, 2, 3)') 25 | Test('let template - new expression ! | new Type(1, 2, 3)!{let} >> let name = new Type(1, 2, 3)!') 26 | Test('let template - awaited expression | await expr{let} >> let name = await expr') 27 | Test('let template - escape dollar sign | $expr.$a{let} >> let name = $expr.$a') 28 | Test('let template - string literal #1 | "a string"{let} >> let name = "a string"') 29 | Test('let template - string literal #2 | \'a string\'{let} >> let name = \'a string\'') 30 | Test('let template - string literal #3 | `a string`{let} >> let name = `a string`') 31 | Test('let template - string literal #4 | `a ${value} string`{let} >> let name = `a ${value} string`') 32 | Test('let template - string literal #5 | `a string ${value}`{let} >> let name = `a string ${value}`') 33 | Test('let template - escape characters | `\\\\\\\\`{let} >> let name = `\\\\\\\\`') 34 | 35 | Test('var template | a.b{var} >> var name = a.b') 36 | Test('var template (indent) | \ta.b{var} >> \tvar name = a.b') 37 | Test('const template | a.b{const} >> const name = a.b') 38 | 39 | Test('log template | expr{log} >> console.log(expr)') 40 | Test('warn template | expr{warn} >> console.warn(expr)') 41 | Test('error template | expr{error} >> console.error(expr)') 42 | Test('log template - binary | x > y{log} >> console.log(x > y)') 43 | 44 | Test('log template - obj literal (empty) | {}{log} >> console.log({})') 45 | Test('log template - obj literal | {foo:"foo"}{log} >> console.log({foo:"foo"})') 46 | 47 | Test('return template | expr{return} >> return expr') 48 | Test('return template | x > 1{return} >> return x > 1') 49 | Test('return template | x > y{return} >> return x > y') 50 | Test('return template | new Type(){return} >> return new Type()') 51 | Test('return template | `\\\\\\\\`{return} >> return `\\\\\\\\`') 52 | 53 | Test('if template | expr{if} >> if(expr){}', withTrimWhitespaces) 54 | Test('if template - binary expression | a > b{if} >> if(a>b){}', withTrimWhitespaces) 55 | Test('if template - binary in parens | (a > b){if} >> if(a>b){}', withTrimWhitespaces) 56 | Test('else template | expr{else} >> if(!expr){}', withTrimWhitespaces) 57 | Test('else template - binary expression | x * 100{else} >> if(!(x*100)){}', withTrimWhitespaces) 58 | Test('else template - binary expression | a > b{else} >> if(a<=b){}', withTrimWhitespaces) 59 | Test('else template - binary in parens | (a > b){else} >> if(a<=b){}', withTrimWhitespaces) 60 | 61 | Test('null template | expr{null} >> if(expr===null){}', withTrimWhitespaces) 62 | Test('notnull template | expr{notnull} >> if(expr!==null){}', withTrimWhitespaces) 63 | Test('undefined template | expr{undefined} >> if(expr===undefined){}', withTrimWhitespaces) 64 | Test('notundefined template | expr{notundefined} >> if(expr!==undefined){}', withTrimWhitespaces) 65 | 66 | Test('null template - inside if | if (x & expr{null}) >> if(x&expr===null)', withTrimWhitespaces) 67 | Test('notnull template - inside if | if (x & expr{notnull}) >> if(x&expr!==null)', withTrimWhitespaces) 68 | Test('undefined template - inside if | if (x & expr{undefined}) >> if(x&expr===undefined)', withTrimWhitespaces) 69 | Test('notundefined template - inside if | if (x & expr{notundefined}) >> if(x&expr!==undefined)', withTrimWhitespaces) 70 | 71 | Test('for template | expr{for} >> for(leti=0;i> for(leti=0;i<(awaitexpr).length;i++){}', withTrimWhitespaces) 73 | Test('forof template | expr{forof} >> for(constitemofexpr){}', withTrimWhitespaces) 74 | Test('forin template | expr{forin} >> for(constkeyinexpr){}', withTrimWhitespaces) 75 | Test('foreach template | expr{foreach} >> expr.forEach(item=>)', withTrimWhitespaces) 76 | Test('awaited foreach | await expr{foreach} >> (await expr).forEach(item => )') 77 | 78 | Test('cast template | expr{cast} >> (<>expr)') 79 | Test('castas template | expr{castas} >> (expr as )') 80 | Test('call template | expr{call} >> (expr)') 81 | 82 | Test('new template - identifier | Type{new} >> new Type()') 83 | Test('new template - property access expression | namespace.Type{new} >> new namespace.Type()') 84 | Test('new template - assignment binary expression | a = B{new} >> a = new B()') 85 | 86 | Test('not template | expr{not} >> !expr') 87 | Test('not template - strict equality | if (a === b{not}) >> if (a !== b)') 88 | Test('not template - instanceof | if (a instanceof b{not}) >> if (!(a instanceof b))') 89 | Test('not template - ??= | a ??= b{not} >> a ??= !b') 90 | Test('not template - with non-null assertion | expr!{not} >> !expr!') 91 | Test('not template - inside a call expression | call.expression(expr{not}) >> call.expression(!expr)') 92 | Test('not template - inside a call expression - negated | call.expression(!expr{not}) >> call.expression(expr)') 93 | Test('not template - binary expression | x * 100{not} >> !(x * 100)') 94 | Test('not template - inside an if - identifier | if (expr{not}) >> if(!expr)', withTrimWhitespaces) 95 | Test('not template - inside an if - binary | if (x * 100{not}) >> if(!(x*100))', withTrimWhitespaces) 96 | Test('not template - inside an if - brackets | if ((x * 100){not}) >> if(!(x*100))', withTrimWhitespaces) 97 | Test('not template - already negated expression - method call | !x.method(){not} >> x.method()') 98 | 99 | Test('promisify template - boolean | const x:boolean{promisify} >> const x:Promise') 100 | Test('promisify template - string | const x:string{promisify} >> const x:Promise') 101 | Test('promisify template - custom type | const x:A.B{promisify} >> const x:Promise') 102 | Test('promisify template - custom type 2 | const x:A.B.C.D{promisify} >> const x:Promise') 103 | 104 | Test('await template - expression | expr{await} >> await expr') 105 | Test('await template - method call | obj.call(){await} >> await obj.call()') 106 | Test('await template - property access expression | obj.a.b{await} >> await obj.a.b') 107 | 108 | QuickPick('not template - complex conditions - first expression | if (a > b && x * 100{not}) >> if(a>b&&!(x*100))', true, 0) 109 | QuickPick('not template - complex conditions - second expression | if (a > b && x * 100{not}) >> if(a<=b||!(x*100))', true, 1) 110 | QuickPick('not template - complex conditions with parens - first expression | if (a > b && (x * 100){not}) >> if(a>b&&!(x*100))', true, 0) 111 | QuickPick('not template - complex conditions with parens - second expression | if (a > b && (x * 100){not}) >> if(a<=b||!(x*100))', true, 1) 112 | QuickPick('not template - complex conditions - cancel quick pick | if (a > b && x * 100{not}) >> if(a>b&&x*100.)', true, 0, true) 113 | QuickPick('not template - complex conditions - first expression - alt | if (a > b && x * 100{not}) >> if(a>b&&!(x*100))', true, 0) 114 | QuickPick('not template - complex conditions - second expression - alt | if (a > b && x * 100{not}) >> if(a<=b||!(x*100))', true, 1) 115 | QuickPick('not template - complex conditions - cancel quick pick - alt | if (a > b && x * 100{not}) >> if(a>b&&x*100.)', true, 0, true) 116 | 117 | describe('undefined templates in `typeof` mode', () => { 118 | before(setUndefinedMode(config, 'Typeof')) 119 | after(setUndefinedMode(config, undefined)) 120 | 121 | Test('undefined template | expr{undefined} >> if(typeofexpr==="undefined"){}', withTrimWhitespaces) 122 | Test('notundefined template | expr{notundefined} >> if(typeofexpr!=="undefined"){}', withTrimWhitespaces) 123 | 124 | Test('undefined template - inside if | if (x & expr{undefined}) >> if(x&typeofexpr==="undefined")', withTrimWhitespaces) 125 | Test('notundefined template - inside if | if (x & expr{notundefined}) >> if(x&typeofexpr!=="undefined")', withTrimWhitespaces) 126 | }) 127 | 128 | describe('Infer variable name', () => { 129 | before(setInferVarName(config, true)) 130 | after(setInferVarName(config, false)) 131 | 132 | Test('let template with name - new expression | new Type(1, 2, 3){let} >> let type = new Type(1, 2, 3)') 133 | Test('let template with name - new expression | new namespace.Type(1, 2, 3){let} >> let type = new namespace.Type(1, 2, 3)') 134 | Test('let template with name - call expression | getSomethingCool(1, 2, 3){let} >> let somethingCool = getSomethingCool(1, 2, 3)') 135 | Test('let template with name - call expression | this.getSomethingCool(1, 2, 3){let} >> let somethingCool = this.getSomethingCool(1, 2, 3)') 136 | Test('forof template with array item name #1 | usersList{forof} >> for(constuserofusersList){}', withTrimWhitespaces) 137 | Test('forof template with array item name #2 | cookies{forof} >> for(constcookieofcookies){}', withTrimWhitespaces) 138 | Test('forof template with array item name #3 | order.items{forof} >> for(constitemoforder.items){}', withTrimWhitespaces) 139 | Test('forof template with array item name #4 | object.getCommands(){forof} >> for(constcommandofobject.getCommands()){}', withTrimWhitespaces) 140 | }) 141 | 142 | describe('custom template tests', () => { 143 | const run = runWithCustomTemplate('!{{expr}}') 144 | 145 | run('identifier', 'expr{custom} | expr{custom} >> !expr') 146 | run('string-literal', 'expr{custom} | "expr"{custom} >> !"expr"') 147 | run('expression', 148 | ' expr.test{custom} | expr.test{custom} >> !expr.test', 149 | ' expr[index]{custom} | expr[index]{custom} >> !expr[index]') 150 | run('binary-expression', 151 | 'x > 100{custom} | x > 100{custom} >> !x > 100', 152 | 'x > y{custom} | x > y{custom} >> !x > y') 153 | run('unary-expression', ' !x{custom} | !x{custom} >> !!x') 154 | run('function-call', 155 | ' call(){custom} | call(){custom} >> !call()', 156 | ' test.call(){custom} | test.call(){custom} >> !test.call()') 157 | run('new-expression', 'new Type(){custom} | new Type(){custom} >> !new Type()') 158 | run('type', 159 | ' const x:boolean{custom} | const x:boolean{custom} >> const x:!boolean', 160 | ' const x:A.B{custom} | const x:A.B{custom} >> const x:!A.B', 161 | ' const arrow=():string{custom} | const arrow=():string{custom} >> const arrow=():!string', 162 | ' function f():boolean{custom} | function f():boolean{custom} >> function f():!boolean', 163 | ' function f():A.B.C{custom} | function f():A.B.C{custom} >> function f():!A.B.C', 164 | ' function f(arg: A.B.C{custom}){} | function f(arg: A.B.C{custom}){} >> function f(arg: !A.B.C){}', 165 | ' const arrow=(arg:A.B.C{custom})=>{} | const arrow=(arg:A.B.C{custom})=>{} >> const arrow=(arg:!A.B.C)=>{}', 166 | ' function f({}: A.B.C{custom}){} | function f({}: A.B.C{custom}){} >> function f({}: !A.B.C){}', 167 | ' const arrow=([]: A.B.C{custom})=>{} | const arrow=([]: A.B.C{custom})=>{} >> const arrow=([]: !A.B.C)=>{}') 168 | }) 169 | 170 | describe('custom template with multiple expr tests', () => { 171 | const run = runWithCustomTemplate('{{expr}} + {{expr}}') 172 | 173 | run('identifier', 'expr{custom} | expr{custom} >> expr + expr') 174 | run('expression', 175 | ' expr.test{custom} | expr.test{custom} >> expr.test + expr.test', 176 | ' expr[index]{custom} | expr[index]{custom} >> expr[index] + expr[index]') 177 | run('binary-expression', 'x > 100{custom} | x > 100{custom} >> x > 100 + x > 100') 178 | run('unary-expression', '!x{custom} | !x{custom} >> !x + !x') 179 | run('function-call', 180 | ' call(){custom} | call(){custom} >> call() + call()', 181 | ' test.call(){custom} | test.call(){custom} >> test.call() + test.call()') 182 | }) 183 | 184 | describe('custom template with :lower filter', () => { 185 | const run = runWithCustomTemplate('{{expr:lower}}') 186 | 187 | run('identifier', 'expr{custom} | expr{custom} >> expr') 188 | run('identifier', 'EXPR{custom} | EXPR{custom} >> expr') 189 | run('identifier', 'eXPr{custom} | eXPr{custom} >> expr') 190 | }) 191 | 192 | describe('custom template with :upper filter', () => { 193 | const run = runWithCustomTemplate('{{expr:upper}}') 194 | 195 | run('identifier', 'expr{custom} | expr{custom} >> EXPR') 196 | run('identifier', 'EXPR{custom} | EXPR{custom} >> EXPR') 197 | run('identifier', 'eXPr{custom} | eXPr{custom} >> EXPR') 198 | }) 199 | 200 | describe('custom template with :capitalize filter', () => { 201 | const run = runWithCustomTemplate('{{expr:capitalize}}') 202 | 203 | run('identifier', 'expr{custom} | expr{custom} >> Expr') 204 | run('identifier', 'EXPR{custom} | EXPR{custom} >> EXPR') 205 | run('identifier', 'eXPr{custom} | eXPr{custom} >> EXPr') 206 | }) 207 | 208 | describe('custom template with snippet variables', () => { 209 | const run = runWithCustomTemplate('console.log($TM_LINE_NUMBER, {{expr}})') 210 | 211 | run('identifier', 'expr{custom} | expr{custom} >> console.log(1, expr)') 212 | }) 213 | 214 | describe('custom template with escaped variable syntax', () => { 215 | const run = runWithCustomTemplate('console.log("\\$TM_LINE_NUMBER", \\$1.{{expr}})') 216 | 217 | run('identifier', 'expr{custom} | expr{custom} >> console.log("$TM_LINE_NUMBER", $1.expr)') 218 | }) 219 | 220 | describe('custom template defined as array', () => { 221 | const run = runWithCustomTemplate(['Line 1 {{expr}}', ' Line 2 {{expr}}', ' Line 3 {{expr}}']) 222 | 223 | run('identifier', `expr{custom} | expr{custom} >> Line 1 expr 224 | >> Line 2 expr 225 | >> Line 3 expr`) 226 | }) 227 | }) 228 | 229 | function setUndefinedMode(config: vsc.WorkspaceConfiguration, value: 'Equal' | 'Typeof' | undefined) { 230 | return (done: Mocha.Done) => { 231 | config.update('undefinedMode', value, true).then(done, done) 232 | } 233 | } 234 | 235 | function setInferVarName(config: vsc.WorkspaceConfiguration, value: boolean) { 236 | return (done: Mocha.Done) => { 237 | config.update('inferVariableName', value, true).then(done, done) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /test/extension.svelte-vue-html.test.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import { Options, runTest } from './runner' 3 | import { describe, before, after } from 'mocha' 4 | import { runWithCustomTemplate } from './utils' 5 | 6 | const config = vsc.workspace.getConfiguration('postfix') 7 | const withTrimWhitespaces: Options = { trimWhitespaces: true } 8 | const html: Options = { 9 | fileLanguage: 'html', 10 | fileContext: ` 11 | 15 |

Hello {name}!

`, 16 | extraDelay: 1500 17 | } 18 | 19 | const Test = (test: string, options?: Pick) => runTest(test, { ...html, ...options }) 20 | 21 | describe('HTML/Svelte/Vue - smoke tests', () => { 22 | before(setInferVarName(config, false)) 23 | after(setInferVarName(config, true)) 24 | 25 | Test('log template | expr{log} >> console.log(expr)') 26 | 27 | Test('return template | expr{return} >> return expr') 28 | Test('return template | x > 1{return} >> return x > 1') 29 | Test('return template | x > y{return} >> return x > y') 30 | 31 | Test('if template | expr{if} >> if(expr){}', withTrimWhitespaces) 32 | Test('else template | expr{else} >> if(!expr){}', withTrimWhitespaces) 33 | 34 | Test('let template - binary expression #1 | a * 3{let} >> let name = a * 3') 35 | Test('let template - method call | obj.call(){let} >> let name = obj.call()') 36 | Test('let template - property access expression | obj.a.b{let} >> let name = obj.a.b') 37 | Test('let template - new expression | new Type(1, 2, 3){let} >> let name = new Type(1, 2, 3)') 38 | Test('let template - string literal #1 | "a string"{let} >> let name = "a string"') 39 | Test('let template - escape characters | `\\\\\\\\`{let} >> let name = `\\\\\\\\`') 40 | 41 | Test('null template | expr{null} >> if(expr===null){}', withTrimWhitespaces) 42 | Test('notnull template | expr{notnull} >> if(expr!==null){}', withTrimWhitespaces) 43 | Test('undefined template | expr{undefined} >> if(expr===undefined){}', withTrimWhitespaces) 44 | Test('notundefined template | expr{notundefined} >> if(expr!==undefined){}', withTrimWhitespaces) 45 | 46 | Test('for template | expr{for} >> for(leti=0;i> for(leti=0;i<(awaitexpr).length;i++){}', withTrimWhitespaces) 48 | Test('forof template | expr{forof} >> for(constitemofexpr){}', withTrimWhitespaces) 49 | Test('foreach template | expr{foreach} >> expr.forEach(item=>)', withTrimWhitespaces) 50 | Test('awaited foreach | await expr{foreach} >> (await expr).forEach(item => )') 51 | 52 | Test('cast template | expr{cast} >> (<>expr)') 53 | Test('castas template | expr{castas} >> (expr as )') 54 | 55 | Test('new template - identifier | Type{new} >> new Type()') 56 | Test('new template - property access expression | namespace.Type{new} >> new namespace.Type()') 57 | 58 | Test('not template | expr{not} >> !expr') 59 | 60 | Test('promisify template - boolean | const x:boolean{promisify} >> const x:Promise') 61 | 62 | describe('Infer variable name', () => { 63 | before(setInferVarName(config, true)) 64 | after(setInferVarName(config, false)) 65 | 66 | Test('let template with name - new expression | new Type(1, 2, 3){let} >> let type = new Type(1, 2, 3)') 67 | Test('let template with name - call expression | getSomethingCool(1, 2, 3){let} >> let somethingCool = getSomethingCool(1, 2, 3)') 68 | Test('forof template with array item name #1 | usersList{forof} >> for(constuserofusersList){}', withTrimWhitespaces) 69 | }) 70 | 71 | describe('custom template tests', () => { 72 | const run = runWithCustomTemplate('!{{expr}}') 73 | 74 | run('identifier', 'expr{custom} | expr{custom} >> !expr') 75 | run('string-literal', 'expr{custom} | "expr"{custom} >> !"expr"') 76 | run('expression', 77 | ' expr.test{custom} | expr.test{custom} >> !expr.test', 78 | ' expr[index]{custom} | expr[index]{custom} >> !expr[index]') 79 | run('binary-expression', 80 | 'x > 100{custom} | x > 100{custom} >> !x > 100', 81 | 'x > y{custom} | x > y{custom} >> !x > y') 82 | run('unary-expression', ' !x{custom} | !x{custom} >> !!x') 83 | run('function-call', 84 | ' call(){custom} | call(){custom} >> !call()', 85 | ' test.call(){custom} | test.call(){custom} >> !test.call()') 86 | run('new-expression', 'new Type(){custom} | new Type(){custom} >> !new Type()') 87 | run('type', 88 | ' const x:boolean{custom} | const x:boolean{custom} >> const x:!boolean', 89 | ' const x:A.B{custom} | const x:A.B{custom} >> const x:!A.B', 90 | ' const arrow=():string{custom} | const arrow=():string{custom} >> const arrow=():!string', 91 | ' function f():boolean{custom} | function f():boolean{custom} >> function f():!boolean', 92 | ' function f():A.B{custom} | function f():A.B{custom} >> function f():!A.B', 93 | ' function f():A.B.C.D{custom} | function f():A.B.C.D{custom} >> function f():!A.B.C.D') 94 | }) 95 | }) 96 | 97 | function setInferVarName(config: vsc.WorkspaceConfiguration, value: boolean) { 98 | return (done: Mocha.Done) => { 99 | config.update('inferVariableName', value, true).then(done, done) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/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 | mocha.timeout(5000) 13 | 14 | const testsRoot = path.resolve(__dirname, '..') 15 | 16 | return new Promise((c, e) => { 17 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 18 | if (err) { 19 | return e(err) 20 | } 21 | 22 | // Add files to the test suite 23 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))) 24 | 25 | try { 26 | // Run the mocha test 27 | mocha.run(failures => { 28 | if (failures > 0) { 29 | e(new Error(`${failures} tests failed.`)) 30 | } else { 31 | c() 32 | } 33 | }) 34 | } catch (err) { 35 | console.error(err) 36 | e(err) 37 | } 38 | }) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /test/runTests.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 the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './index') 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }) 17 | } catch (err) { 18 | console.error('Failed to run tests') 19 | process.exit(1) 20 | } 21 | } 22 | 23 | main() 24 | -------------------------------------------------------------------------------- /test/runner.ts: -------------------------------------------------------------------------------- 1 | import { makeTestFunction, testTemplate, TestTemplateOptions, testTemplateWithQuickPick } from './utils' 2 | import { EOL } from 'os' 3 | import { TestFunction } from 'mocha' 4 | 5 | export type Options = Omit 6 | 7 | export const runTest = makeTestFunction(__runTest) 8 | export const runTestMultiline = makeTestFunction(__runTestMultiline) 9 | export const runTestQuickPick = makeTestFunction(__runTestQuickPick) 10 | export const runTestMultilineQuickPick = makeTestFunction(__runTestMultilineQuickPick) 11 | 12 | function __runTest(func: TestFunction, test: string, options: Options = {}) { 13 | const [title, ...dsl] = test.split('|') 14 | func(title.trim(), testTemplate('|' + dsl.join('|'), options)) 15 | } 16 | 17 | function __runTestMultiline(func: TestFunction, test: string, options: Options = {}) { 18 | const [title, ...dsl] = test.split(/\r?\n/) 19 | func(title.trim(), testTemplate(dsl.join(EOL), options)) 20 | } 21 | 22 | function __runTestQuickPick(func: TestFunction, test: string, trimWhitespaces?: boolean, skipSuggestions?: number, cancelQuickPick?: boolean) { 23 | const [title, ...dsl] = test.split('|') 24 | func(title.trim(), testTemplateWithQuickPick('|' + dsl.join('|'), trimWhitespaces, skipSuggestions, cancelQuickPick)) 25 | } 26 | 27 | function __runTestMultilineQuickPick(func: TestFunction, test: string, trimWhitespaces?: boolean, skipSuggestions?: number, cancelQuickPick?: boolean) { 28 | const [title, ...dsl] = test.split(/\r?\n/) 29 | func(title.trim(), testTemplateWithQuickPick(dsl.join(EOL), trimWhitespaces, skipSuggestions, cancelQuickPick)) 30 | } 31 | -------------------------------------------------------------------------------- /test/template.usage.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as _ from 'lodash' 3 | import * as vsc from 'vscode' 4 | import { describe, afterEach, before, after, TestFunction } from 'mocha' 5 | 6 | import { getCurrentSuggestion, resetCurrentSuggestion, overrideTsxEnabled } from '../src/postfixCompletionProvider' 7 | import { getCurrentDelay, delay, makeTestFunction } from './utils' 8 | 9 | const LANGUAGE = 'postfix' 10 | 11 | const VAR_TEMPLATES = ['var', 'let', 'const'] 12 | const FOR_TEMPLATES = ['for', 'forin', 'forof', 'foreach'] 13 | const CONSOLE_TEMPLATES = ['log', 'warn', 'error'] 14 | const EQUALITY_TEMPLATES = ['null', 'notnull', 'undefined', 'notundefined', 'new'] 15 | const IF_TEMPLATES = ['if', 'else', 'null', 'notnull', 'undefined', 'notundefined'] 16 | const CAST_TEMPLATES = ['cast', 'castas'] 17 | const TYPE_TEMPLATES = ['promisify'] 18 | const ALL_TEMPLATES = [ 19 | ...VAR_TEMPLATES, 20 | ...FOR_TEMPLATES, 21 | ...CONSOLE_TEMPLATES, 22 | ...IF_TEMPLATES, 23 | ...CAST_TEMPLATES, 24 | 'not', 25 | 'return', 26 | 'new', 27 | 'await', 28 | 'call' 29 | ] 30 | const STRING_LITERAL_TEMPLATES = [ 31 | ...VAR_TEMPLATES, 32 | ...CONSOLE_TEMPLATES, 33 | 'return' 34 | ] 35 | 36 | const BINARY_EXPRESSION_TEMPLATES = [ 37 | ...VAR_TEMPLATES, 38 | ...CONSOLE_TEMPLATES, 39 | ...CAST_TEMPLATES, 40 | 'if', 41 | 'else', 42 | 'not', 43 | 'return', 44 | 'call' 45 | ] 46 | 47 | const config = vsc.workspace.getConfiguration('postfix') 48 | const testTemplateUsage = makeTestFunction(__testTemplateUsage) 49 | 50 | describe('Template usage', () => { 51 | afterEach(done => { 52 | vsc.commands.executeCommand('workbench.action.closeOtherEditors').then(() => done(), err => done(err)) 53 | }) 54 | 55 | testTemplateUsage('identifier expression', 'expr', ALL_TEMPLATES) 56 | testTemplateUsage('awaited expression', 'await expr', _.difference(ALL_TEMPLATES, ['new', 'await', 'forin'])) 57 | testTemplateUsage('method call expression', 'expr.call()', _.difference(ALL_TEMPLATES, ['for', 'new'])) 58 | testTemplateUsage('property access expression', 'expr.a.b.c', ALL_TEMPLATES) 59 | testTemplateUsage('element access expression', 'expr.a.b[c]', _.difference(ALL_TEMPLATES, ['new'])) 60 | testTemplateUsage('binary expression', 'x > y', BINARY_EXPRESSION_TEMPLATES) 61 | testTemplateUsage('binary expression', '(x > y)', BINARY_EXPRESSION_TEMPLATES) 62 | testTemplateUsage('unary expression', 'expr++', _.difference(ALL_TEMPLATES, [...FOR_TEMPLATES, 'new', 'await'])) 63 | testTemplateUsage('conditional expression', 'if (x * 100{cursor})', ['not']) 64 | testTemplateUsage('return expression', 'return x * 100', [...CAST_TEMPLATES, 'not', 'call']) 65 | testTemplateUsage('object literal expression', '{}', [...VAR_TEMPLATES, ...CONSOLE_TEMPLATES, 'return']) 66 | testTemplateUsage('object literal expression', '{foo:"foo"}', [...VAR_TEMPLATES, ...CONSOLE_TEMPLATES, 'return']) 67 | testTemplateUsage('new expression', 'new Class()', [...VAR_TEMPLATES, ...CONSOLE_TEMPLATES, ...CAST_TEMPLATES, 'return', 'call']) 68 | testTemplateUsage('expression as argument', 'function.call("arg", expr{cursor})', [...CAST_TEMPLATES, 'not', 'new', 'await', 'call']) 69 | 70 | testTemplateUsage('string literal - single quote', '\'a string\'', STRING_LITERAL_TEMPLATES) 71 | testTemplateUsage('string literal - double quote', '"a string"', STRING_LITERAL_TEMPLATES) 72 | testTemplateUsage('string literal - backtick', '`a string`', STRING_LITERAL_TEMPLATES) 73 | testTemplateUsage('string literal - backtick with var #1', '`a ${value} string`', STRING_LITERAL_TEMPLATES) 74 | testTemplateUsage('string literal - backtick with var #2', '`a string ${value}`', STRING_LITERAL_TEMPLATES) 75 | 76 | testTemplateUsage('function type - built-in', 'function f(): boolean', TYPE_TEMPLATES) 77 | testTemplateUsage('function type - custom', 'function f(): Type', TYPE_TEMPLATES) 78 | testTemplateUsage('var type - built-in', 'const x: boolean', TYPE_TEMPLATES) 79 | testTemplateUsage('var type - custom', 'const x: Type', TYPE_TEMPLATES) 80 | 81 | testTemplateUsage('inside return - arrow function', 'return items.map(x => { result{cursor} })', ALL_TEMPLATES) 82 | testTemplateUsage('inside return - function', 'return items.map(function(x) { result{cursor} })', ALL_TEMPLATES) 83 | 84 | testTemplateUsage('inside variable declaration', 'var test = expr{cursor}', [...CAST_TEMPLATES, ...EQUALITY_TEMPLATES, 'not', 'await', 'call']) 85 | testTemplateUsage('inside assignment statement', 'test = expr{cursor}', [...CAST_TEMPLATES, ...EQUALITY_TEMPLATES, 'not', 'call']) 86 | testTemplateUsage('inside assignment statement - short-circuit', 'test *= expr{cursor}', [...CAST_TEMPLATES, ...EQUALITY_TEMPLATES, 'not', 'call']) 87 | testTemplateUsage('inside return', 'return expr{cursor}', [...CAST_TEMPLATES, ...EQUALITY_TEMPLATES, 'not', 'await', 'call']) 88 | testTemplateUsage('inside single line comment', '// expr', []) 89 | testTemplateUsage('inside multi line comment', '/* expr{cursor} */', []) 90 | 91 | describe('JSX tests', () => { 92 | before(() => overrideTsxEnabled.value = true) 93 | 94 | testTemplateUsage('inside JSX fragment', '<>a{cursor}', []) 95 | testTemplateUsage('inside JSX element', '

a{cursor}

', []) 96 | testTemplateUsage('inside JSX expression', '', ALL_TEMPLATES) 97 | 98 | after(() => overrideTsxEnabled.value = false) 99 | }) 100 | 101 | testTemplateUsage('inside var declaration - function', 'const f1 = function () { expr{cursor}', ALL_TEMPLATES) 102 | testTemplateUsage('inside var declaration - arrow function', 'const f3 = () => { expr{cursor}', ALL_TEMPLATES) 103 | testTemplateUsage('inside function', 'function f2() { expr{cursor}', ALL_TEMPLATES) 104 | testTemplateUsage('inside arrow function', '() => { expr{cursor}', ALL_TEMPLATES) 105 | 106 | testTemplateUsage('cursor in wrong place #1', 'test.something = {cursor-no-dot}', []) 107 | testTemplateUsage('cursor in wrong place #2', 'test.something = new{cursor-no-dot}', []) 108 | 109 | describe('when some templates are disabled', () => { 110 | before(setDisabledTemplates(config, ['var', 'forof'])) 111 | after(setDisabledTemplates(config, [])) 112 | 113 | testTemplateUsage('identifier expression', 'expr', _.difference(ALL_TEMPLATES, ['var', 'forof'])) 114 | }) 115 | }) 116 | 117 | function setDisabledTemplates(config: vsc.WorkspaceConfiguration, value: string[]) { 118 | return (done: Mocha.Done) => { 119 | config.update('disabledBuiltinTemplates', value, true).then(done, done) 120 | } 121 | } 122 | 123 | function __testTemplateUsage(func: TestFunction, testDescription: string, initialText: string, expectedTemplates: string[]) { 124 | func(testDescription, (done: Mocha.Done) => { 125 | vsc.workspace.openTextDocument({ language: LANGUAGE }).then((doc) => { 126 | return getAvailableSuggestions(doc, initialText).then(templates => { 127 | assert.deepStrictEqual(_.sortBy(templates), _.sortBy(expectedTemplates)) 128 | done() 129 | }).then(undefined, (reason) => { 130 | done(reason) 131 | }) 132 | }) 133 | }) 134 | } 135 | 136 | async function getAvailableSuggestions(doc: vsc.TextDocument, initialText: string) { 137 | const editor = await vsc.window.showTextDocument(doc, vsc.ViewColumn.One) 138 | 139 | let cursorIdx = initialText.indexOf('{cursor}') 140 | if (cursorIdx > -1) { 141 | initialText = initialText.replace('{cursor}', '.') 142 | } else { 143 | cursorIdx = initialText.indexOf('{cursor-no-dot}') 144 | if (cursorIdx > -1) { 145 | initialText = initialText.replace('{cursor-no-dot}', '') 146 | } else { 147 | initialText += '.' 148 | cursorIdx = initialText.length 149 | } 150 | } 151 | 152 | if (await editor.edit(edit => edit.insert(new vsc.Position(0, 0), initialText))) { 153 | const pos = new vsc.Position(0, cursorIdx + 1) 154 | editor.selection = new vsc.Selection(pos, pos) 155 | 156 | resetCurrentSuggestion() 157 | await vsc.commands.executeCommand('editor.action.triggerSuggest') 158 | await delay(getCurrentDelay()) 159 | 160 | const firstSuggestion = getCurrentSuggestion() 161 | const suggestions = firstSuggestion ? [firstSuggestion] : [] 162 | 163 | while (true) { 164 | await vsc.commands.executeCommand('selectNextSuggestion') 165 | 166 | const current = getCurrentSuggestion() 167 | 168 | if (current === undefined || suggestions.indexOf(current) > -1) { 169 | break 170 | } 171 | 172 | suggestions.push(current) 173 | } 174 | 175 | return suggestions 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as vsc from 'vscode' 3 | import * as ts from 'typescript' 4 | import { describe, it } from 'mocha' 5 | 6 | import { getIndentCharacters } from '../src/utils' 7 | import { invertBinaryExpression, invertExpression } from '../src/utils/invert-expression' 8 | 9 | describe('Utils tests', () => { 10 | it('getIndentCharacters when spaces', () => { 11 | vsc.window.activeTextEditor.options.insertSpaces = true 12 | vsc.window.activeTextEditor.options.tabSize = 4 13 | 14 | const result = getIndentCharacters() 15 | assert.strictEqual(result, ' ') 16 | }) 17 | 18 | it('getIndentCharacters when tabs', () => { 19 | vsc.window.activeTextEditor.options.insertSpaces = false 20 | 21 | const result = getIndentCharacters() 22 | assert.strictEqual(result, '\t') 23 | }) 24 | 25 | describe('invertExpression', () => { 26 | testInvertExpression('x >> !x') 27 | testInvertExpression('!x >> x') 28 | testInvertExpression('x * 100 >> !(x * 100)') 29 | testInvertExpression('!(x * 100) >> x * 100') 30 | testInvertExpression('x && y * 100 >> !x || !(y * 100)') 31 | testInvertExpression('(x > y) >> (x <= y)') 32 | }) 33 | 34 | describe('invertBinaryExpression', () => { 35 | 36 | describe('operators', () => { 37 | testInvertBinaryExpression('x > y >> x <= y') 38 | testInvertBinaryExpression('x < y >> x >= y') 39 | testInvertBinaryExpression('x >= y >> x < y') 40 | testInvertBinaryExpression('x <= y >> x > y') 41 | testInvertBinaryExpression('x == y >> x != y') 42 | testInvertBinaryExpression('x === y >> x !== y') 43 | testInvertBinaryExpression('x != y >> x == y') 44 | testInvertBinaryExpression('x !== y >> x === y') 45 | }) 46 | 47 | describe('complex expressions', () => { 48 | testInvertBinaryExpression('x > y && a >> x <= y || !a') 49 | testInvertBinaryExpression('x && a == b >> !x || a != b') 50 | testInvertBinaryExpression('x && y >> !x || !y') 51 | testInvertBinaryExpression('!x && !y >> x || y') 52 | testInvertBinaryExpression('x > y && a >= b >> x <= y || a < b') 53 | testInvertBinaryExpression('x > y || a >= b >> x <= y && a < b') 54 | testInvertBinaryExpression('x > y && a >= b || c == d >> (x <= y || a < b) && c != d') 55 | testInvertBinaryExpression('x || y && z >> !x && (!y || !z)') 56 | testInvertBinaryExpression('a && b && c >> !a || !b || !c') 57 | testInvertBinaryExpression('a && b && c && d >> !a || !b || !c || !d') 58 | testInvertBinaryExpression('a || b && c && d >> !a && (!b || !c || !d)') 59 | testInvertBinaryExpression('a && b || c && d >> (!a || !b) && (!c || !d)') 60 | testInvertBinaryExpression('!(a && b) || !(c && d) >> a && b && c && d') 61 | }) 62 | }) 63 | }) 64 | 65 | function testInvertBinaryExpression(dsl: string) { 66 | const [input, expected] = dsl.split('>>').map(x => x.trim()) 67 | 68 | it(`${input} should invert to ${expected}`, () => { 69 | const source = ts.createSourceFile('invertBinaryExpression.ts', input, ts.ScriptTarget.ES5, true) 70 | const expr = (source.statements[0] as ts.ExpressionStatement).expression as ts.BinaryExpression 71 | 72 | const result = invertBinaryExpression(expr) 73 | 74 | assert.strictEqual(result, expected) 75 | }) 76 | } 77 | 78 | function testInvertExpression(dsl: string) { 79 | const [input, expected] = dsl.split('>>').map(x => x.trim()) 80 | 81 | it(`${input} should invert to ${expected}`, () => { 82 | const source = ts.createSourceFile('invertBinaryExpression.ts', input, ts.ScriptTarget.ES5, true) 83 | const expr = (source.statements[0] as ts.ExpressionStatement).expression 84 | 85 | const result = invertExpression(expr) 86 | 87 | assert.strictEqual(result, expected) 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import * as assert from 'assert' 3 | import { describe, before, after, TestFunction, it } from 'mocha' 4 | import { getCurrentSuggestion } from '../src/postfixCompletionProvider' 5 | import { parseDSL, ITestDSL } from './dsl' 6 | import { runTest } from './runner' 7 | import { EOL } from 'node:os' 8 | import { CustomTemplateBodyType } from '../src/utils/templates' 9 | 10 | const LANGUAGE = 'postfix' 11 | 12 | const config = vsc.workspace.getConfiguration('editor', null) 13 | export const TabSize = config.get('tabSize') ?? 4 14 | 15 | export function delay(timeout: number) { 16 | return new Promise(resolve => setTimeout(resolve, timeout)) 17 | } 18 | 19 | // for some reason editor.action.triggerSuggest needs more delay at the beginning when the process is not yet "warmed up" 20 | // let's start from high delays and then slowly go to lower delays 21 | const delaySteps = [2000, 1200, 700, 400, 300, 250] 22 | 23 | export const getCurrentDelay = () => (delaySteps.length > 1) ? delaySteps.shift() : delaySteps[0] 24 | 25 | export type TestTemplateOptions = Partial<{ 26 | trimWhitespaces: boolean 27 | preAssertAction: () => Thenable 28 | fileContext: string 29 | fileLanguage: string 30 | extraDelay: number 31 | }> 32 | 33 | export function testTemplate(dslString: string, options: TestTemplateOptions = {}) { 34 | const dsl = parseDSL(dslString) 35 | 36 | return (done: Mocha.Done) => { 37 | vsc.workspace.openTextDocument({ language: options.fileLanguage || LANGUAGE }).then(async (doc) => { 38 | try { 39 | await selectAndAcceptSuggestion(doc, dsl, options.fileContext) 40 | await delay(options.extraDelay || 0) 41 | await options.preAssertAction?.() 42 | 43 | const expected = options.fileContext 44 | ? options.fileContext.trim().replace('{{CODE}}', dsl.expected) 45 | : dsl.expected 46 | 47 | assertText(doc, expected, options.trimWhitespaces) 48 | await vsc.commands.executeCommand('workbench.action.closeActiveEditor') 49 | done() 50 | } catch (reason) { 51 | await vsc.commands.executeCommand('workbench.action.closeActiveEditor') 52 | done(reason) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | export function testTemplateWithQuickPick(dslString: string, trimWhitespaces?: boolean, skipSuggestions = 0, cancelQuickPick = false) { 59 | return testTemplate(dslString, { 60 | trimWhitespaces, preAssertAction: async () => { 61 | if (cancelQuickPick) { 62 | await vsc.commands.executeCommand('workbench.action.closeQuickOpen') 63 | } else { 64 | await delay(100) 65 | 66 | for (let i = 0; i < skipSuggestions; i++) { 67 | await vsc.commands.executeCommand('workbench.action.quickOpenSelectNext') 68 | } 69 | 70 | await vsc.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem') 71 | } 72 | 73 | await delay(100) 74 | } 75 | }) 76 | } 77 | 78 | async function selectAndAcceptSuggestion(doc: vsc.TextDocument, dsl: ITestDSL, fileContext?: string) { 79 | const editor = await vsc.window.showTextDocument(doc, vsc.ViewColumn.One) 80 | 81 | let startPosition = new vsc.Position(0, 0) 82 | 83 | if (fileContext) { 84 | fileContext = fileContext.trim() 85 | const [before, after] = fileContext.split('{{CODE}}') 86 | await editor.edit(edit => edit.insert(new vsc.Position(0, 0), before)) 87 | startPosition = editor.selection.start 88 | await editor.edit(edit => edit.insert(startPosition, after)) 89 | } 90 | 91 | if (await editor.edit(edit => edit.insert(startPosition, dsl.input))) { 92 | const { character, line } = dsl.cursorPosition 93 | const pos = startPosition.translate(line, character) 94 | 95 | editor.selection = new vsc.Selection(pos, pos) 96 | 97 | await vsc.commands.executeCommand('editor.action.triggerSuggest') 98 | await delay(getCurrentDelay()) 99 | 100 | let current = getCurrentSuggestion() 101 | const first = current 102 | 103 | while (current !== dsl.template) { 104 | await vsc.commands.executeCommand('selectNextSuggestion') 105 | current = getCurrentSuggestion() 106 | 107 | if (current === first) { 108 | break 109 | } 110 | } 111 | 112 | return vsc.commands.executeCommand('acceptSelectedSuggestion') 113 | } 114 | } 115 | 116 | function assertText(doc: vsc.TextDocument, expectedResult: string, trimWhitespaces = false) { 117 | let result = doc.getText() 118 | 119 | if (trimWhitespaces) { 120 | result = result.replaceAll(/\s/g, '') 121 | expectedResult = expectedResult.replaceAll(/\s/g, '') 122 | } 123 | 124 | assert.strictEqual(normalizeWhitespaces(result), normalizeWhitespaces(expectedResult)) 125 | } 126 | 127 | function normalizeWhitespaces(text: string) { 128 | return text 129 | .split(/\r?\n/g) 130 | .map(line => line.replace(/\t/g, ' '.repeat(TabSize))) 131 | .join(EOL) 132 | } 133 | 134 | export function runWithCustomTemplate(template: CustomTemplateBodyType) { 135 | const postfixConfig = vsc.workspace.getConfiguration('postfix') 136 | return (when: string, ...tests: string[]) => 137 | describe(when, () => { 138 | before(setCustomTemplate(postfixConfig, 'custom', template, [when])) 139 | after(resetCustomTemplates(postfixConfig)) 140 | 141 | tests.forEach(t => runTest(t)) 142 | }) 143 | } 144 | 145 | function setCustomTemplate(config: vsc.WorkspaceConfiguration, name: string, body: CustomTemplateBodyType, when: string[]) { 146 | return (done: Mocha.Done) => { 147 | config.update('customTemplates', [{ 148 | 'name': name, 149 | 'body': body, 150 | 'description': 'custom description', 151 | 'when': when 152 | }], true).then(done, done) 153 | } 154 | } 155 | 156 | function resetCustomTemplates(config: vsc.WorkspaceConfiguration) { 157 | return (done: Mocha.Done) => { 158 | config.update('customTemplates', undefined, true).then(done, done) 159 | } 160 | } 161 | 162 | type ParametersSkipFirst void> = T extends (first: TestFunction, ...args: infer P) => void ? P : never 163 | type TestFnSkipFirstParam void> = (...args: ParametersSkipFirst) => void 164 | type RunTestFn void> = TestFnSkipFirstParam & { 165 | only: TestFnSkipFirstParam 166 | skip: TestFnSkipFirstParam 167 | } 168 | 169 | export function makeTestFunction void>(testFn: T) { 170 | const result = testFn.bind(null, it) as RunTestFn 171 | result.only = testFn.bind(null, it.only.bind(it)) as RunTestFn 172 | result.skip = testFn.bind(null, it.skip.bind(it)) as RunTestFn 173 | 174 | return result 175 | } 176 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2021", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2021" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": ".", 11 | "noImplicitOverride": true, 12 | "strictBindCallApply": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test", 17 | "build.mjs" 18 | ] 19 | } 20 | --------------------------------------------------------------------------------