├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.MD ├── README.md ├── assets ├── extension_icon.png ├── text-tool.png ├── text_icon_regular.png └── text_icon_reverse.png ├── package-lock.json ├── package.json ├── src ├── extension.ts ├── modules │ ├── alignText.ts │ ├── caseConversion.ts │ ├── controlCharacters.ts │ ├── copyText.ts │ ├── decorations.ts │ ├── delimiters.ts │ ├── filterText.ts │ ├── helpers.ts │ ├── indentation.ts │ ├── insertText.ts │ ├── json.ts │ ├── sortText.ts │ ├── statusBarSelection.ts │ ├── tabOut.ts │ └── textManipulation.ts └── test │ ├── runTest.ts │ └── suite │ ├── alignText.test.ts │ ├── caseConversion.test.ts │ ├── controlCharacters.test.ts │ ├── delimiters.ts │ ├── filterText.test.ts │ ├── index.ts │ ├── insertText.test.ts │ ├── json.test.ts │ ├── sortText.test.ts │ ├── splitText.test.ts │ └── textManipulation.test.ts ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | // // Commend out next line due to https://github.com/typescript-eslint/typescript-eslint/issues/2054 13 | // "@typescript-eslint/class-na me-casing": "warn", 14 | "@typescript-eslint/semi": "warn", 15 | "curly": "warn", 16 | "eqeqeq": "warn", 17 | "no-throw-literal": "warn", 18 | "semi": "off" 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | node_modules/ 3 | vsc-extension-quickstart.md 4 | .vscode-test/ 5 | *.vsix 6 | dist/ 7 | *.zip 8 | *.tgz 9 | # .vscode 10 | 11 | # Local History for Visual Studio Code 12 | .history/ 13 | ./package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "printWidth": 160 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run MY Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-extensions"], 14 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 15 | "preLaunchTask": "${defaultBuildTask}" 16 | }, 17 | { 18 | "name": "Run All Extensions", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 23 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 24 | "preLaunchTask": "${defaultBuildTask}" 25 | }, 26 | { 27 | "name": "Run Web Extension in VS Code", 28 | "type": "pwa-extensionHost", 29 | "debugWebWorkerHost": true, 30 | "request": "launch", 31 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web", "--disable-extensions"], 32 | "outFiles": ["${workspaceFolder}/dist/web/**/*.js"], 33 | "preLaunchTask": "npm: watch-web" 34 | }, 35 | { 36 | "name": "Run Extension on Temp folder", 37 | "type": "extensionHost", 38 | "request": "launch", 39 | "runtimeExecutable": "${execPath}", 40 | "args": [ 41 | "--extensionDevelopmentPath=${workspaceFolder}", 42 | "--disable-extensions", 43 | "C:/Temp" 44 | ], 45 | "outFiles": [ 46 | "${workspaceFolder}/dist/**/*.js" 47 | ], 48 | "preLaunchTask": "${defaultBuildTask}" 49 | }, 50 | { 51 | "name": "Extension Tests", 52 | "type": "extensionHost", 53 | "request": "launch", 54 | "runtimeExecutable": "${execPath}", 55 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test/suite/index", "--disable-extensions"], 56 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 57 | "preLaunchTask": "npm: unit-tests" 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.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 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "workbench.colorCustomizations": { 12 | "activityBar.activeBackground": "#ab307e", 13 | "activityBar.activeBorder": "#25320e", 14 | "activityBar.background": "#ab307e", 15 | "activityBar.foreground": "#e7e7e7", 16 | "activityBar.inactiveForeground": "#e7e7e799", 17 | "activityBarBadge.background": "#25320e", 18 | "activityBarBadge.foreground": "#e7e7e7", 19 | "statusBar.background": "#832561", 20 | "statusBar.foreground": "#e7e7e7", 21 | "statusBarItem.hoverBackground": "#ab307e", 22 | "titleBar.activeBackground": "#832561", 23 | "titleBar.activeForeground": "#e7e7e7", 24 | "titleBar.inactiveBackground": "#83256199", 25 | "titleBar.inactiveForeground": "#e7e7e799", 26 | "statusBarItem.remoteBackground": "#832561", 27 | "statusBarItem.remoteForeground": "#e7e7e7", 28 | "sash.hoverBorder": "#ab307e", 29 | "commandCenter.border": "#e7e7e799", 30 | "tab.activeBorder": "#ab307e" 31 | }, 32 | "peacock.color": "#832561", 33 | "cSpell.words": ["carlocardella", "darkred", "luxon", "pickone", "submenu", "texttoolbox", "untrusted"], 34 | // MOCHA EXPLORER 35 | "mochaExplorer.require": "ts-node/register", 36 | "mochaExplorer.files": "src/test/**/*.ts", 37 | "mochaExplorer.logpanel": true, 38 | "task.allowAutomaticTasks": "on" 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "webpack-dev", 9 | "runOptions": { 10 | "runOn": "folderOpen" 11 | }, 12 | "isBackground": true, 13 | "presentation": { 14 | "echo": true, 15 | "reveal": "always", 16 | "focus": false, 17 | "panel": "shared", 18 | "showReuseMessage": true, 19 | "clear": false 20 | }, 21 | "group": { 22 | "kind": "build", 23 | "isDefault": true 24 | } 25 | }, 26 | { 27 | "type": "npm", 28 | "script": "unit-tests", 29 | "problemMatcher": "$tsc-watch", 30 | "isBackground": true 31 | }, 32 | { 33 | "type": "npm", 34 | "script": "watch-web", 35 | "group": "build", 36 | "isBackground": true, 37 | "problemMatcher": ["$ts-webpack-watch"] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/tsconfig.json 2 | !file.ts 3 | todo 4 | out/ 5 | src/ 6 | **/*.map 7 | webpack.config.js 8 | node_modules/ 9 | .vscode/ 10 | .github/ 11 | .gitignore 12 | package.json 13 | *.drawio -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Carlo Cardella 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VSCode Text Toolbox 2 | 3 | ![.github/workflows/BuildAndPublish.yml](https://github.com/carlocardella/vscode-TextToolbox/workflows/.github/workflows/BuildAndPublish.yml/badge.svg?branch=master) 4 | ![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/carlocardella.vscode-texttoolbox) 5 | ![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/carlocardella.vscode-texttoolbox) 6 | ![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/carlocardella.vscode-texttoolbox) 7 | ![Visual Studio Marketplace Rating](https://img.shields.io/visual-studio-marketplace/r/carlocardella.vscode-texttoolbox) 8 | [![GitHub issues](https://img.shields.io/github/issues/carlocardella/vscode-TextToolbox.svg)](https://github.com/carlocardella/vscode-TextToolbox/issues) 9 | [![GitHub license](https://img.shields.io/github/license/carlocardella/vscode-TextToolbox.svg)](https://github.com/carlocardella/vscode-TextToolbox/blob/master/LICENSE.md) 10 | [![Twitter](https://img.shields.io/twitter/url/https/github.com/carlocardella/vscode-TextToolbox.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fcarlocardella%2Fvscode-TextToolbox) 11 | 12 | 13 | [Download for VS Code](https://marketplace.visualstudio.com/items?itemName=CarloCardella.vscode-texttoolbox) 14 | 15 | [Download for VS Codium](https://open-vsx.org/extension/carlocardella/vscode-texttoolbox) 16 | 17 | Collection of tools for text manipulation, filtering, sorting etc... 18 | 19 | The [VS Code Marketplace](https://marketplace.visualstudio.com/vscode) has a number of great extensions for text manipulation, I installed a few of them to cover the entire range of actions I normally use, unfortunately that means there is some overlapping between them, basically the same action is contributed by multiple extensions (case conversion, for example). That is what motivated me to build this extension: I like the idea to have a single extension for all those operations and without duplicates; plus, it is a good pastime 😊. 20 | 21 | Please open an issue to leave a comment, report a bug, request a feature etc... (you know the drill). 22 | 23 | ## Workspace Trust 24 | 25 | The extension does not require any special permission, therefore is enabled to run in an [Untrusted Workspace](https://github.com/microsoft/vscode/issues/120251) 26 | 27 | ## Visual Studio Code for the Web 28 | 29 | Text-Toolbox works in [Visual Studio Code for the Web](https://code.visualstudio.com/docs/editor/vscode-web) ( and ) 30 | 31 | ## Current list of commands 32 | 33 | Notable absents: `Convert to Uppercase` and `Convert to Lowercase` have been removed in favor of the built-in commands in VScode. 34 | Likewise, `Convert to CapitalCase` has been removed in favor of the built-in VSCode command `Transform to Title Case`. 35 | 36 | ### Text conversions 37 | 38 | * PascalCase 39 | * Lorem ipsum dolor sit amet => LoremIpsumDolorSitAmet 40 | * camelCase 41 | * Lorem ipsum dolor sit amet => loremIpsumDolorSitAmet 42 | * CONSTANT_CASE 43 | * Lorem ipsum dolor sit amet => LOREM_IPSUM_DOLOR_SIT_AMET 44 | * HEADER-CASE 45 | * Lorem ipsum dolor sit amet => LOREM-IPSUM-DOLOR-SIT-AMET 46 | * dot.case 47 | * Lorem ipsum dolor sit amet => lorem.ipsum.dolor.sit.amet 48 | * kebab-case 49 | * Lorem ipsum dolor sit amet => lorem-ipsum-dolor-sit-amet 50 | * Sentence case 51 | * Lorem ipsum dolor sit amet => Lorem ipsum dolor sit amet 52 | * snake_case 53 | * Lorem ipsum dolor sit amet => lorem_ipsum_dolor_sit_amet 54 | * Invert case 55 | * Lorem Ipsum Dolor Sit Amet => lOREM iPSUM dOLOR sIT aMET 56 | * Convert path string to posix format 57 | * Convert path string to win32 format 58 | * Convert integer to hexadecimal 59 | * Convert hexadecimal to integer 60 | * HTML encode/decide 61 | * Uri encode/decode 62 | * Decode JWT token 63 | 64 | ### Insert text 65 | 66 | * Insert UUID 67 | * 0d7196e1-df50-ea89-b518-9335ecc62a20 68 | * Insert GUID 69 | * 14854fc2-f782-5136-aebb-a121b9ba6af1 70 | * Insert GUID all zeros 71 | * 00000000-0000-0000-0000-000000000000 72 | * Insert Date 73 | * DATE_SHORT => 8/25/2020 74 | * DATE_LONG => Tuesday, August 25, 2020 75 | * TIME_SIMPLE => 5:34 PM 76 | * TIME_WITH_SECONDS => 5:34:45 PM 77 | * DATETIME_SHORT => 8/25/2020, 5:34 PM 78 | * DATETIME_SHORT_WITH_SECONDS => 8/25/2020, 5:35:17 PM 79 | * DATETIME_FULL_WITH_SECONDS => August 25, 2020, 5:35 PM PDT 80 | * DATETIME_HUGE => Sunday, May 30, 2021, 5:59 PM PDT 81 | * SORTABLE => 2020-08-25T17:34:58 82 | * UNIVERSAL_SORTABLE => 2020-08-26T00:35:01Z 83 | * ROUNDTRIP => 2021-05-31T00:52:12.057Z 84 | * ISO8601 => 2020-08-25T17:35:05.818-07:00 85 | * ISO8601_DATE => 2020-08-25 86 | * ISO8601_TIME => 17:35:05.818-07:00 87 | * RFC2822 => Tue, 25 Aug 2020 17:35:10 -0700 88 | * HTTP => Wed, 26 Aug 2020 00:35:13 GMT 89 | * UNIX_SECONDS => 1598402124 90 | * UNIX_MILLISECONDS =>1598402132390 91 | * Insert Random 92 | * IPV4 => 123.75.174.203 93 | * IPV6 => 7c50:a61a:5ee0:4562:0dda:b41d:114b:71e0 94 | * NUMBER => 6739440105947136 95 | * PERSON_NAME 96 | * random => May Osborne 97 | * male => Jeffery Ramos 98 | * female => Theresa Boone 99 | * SSN => 956-68-2442 100 | * PROFESSION => Senior Art Director 101 | * ANIMAL => Grison 102 | * COMPANY => SBC Communications Inc 103 | * DOMAIN => cuzkiwpi.sy 104 | * EMAIL => uve@muvkefcib.cd 105 | * COLOR 106 | * hex => #4461ae 107 | * rgb => rgb(87,199,246) 108 | * TWITTER => @zatbiini 109 | * URL => 110 | * CITY => Ecicezjev 111 | * ADDRESS => 1784 Kaolo Grove 112 | * COUNTRY => IT 113 | * COUNTRY_FULL_NAME => Italy 114 | * PHONE => (923) 447-6974 115 | * ZIP_CODE => 35691 116 | * STATE => WA 117 | * STATE_FULL_NAME => Washington 118 | * STREET => Pase Manor 119 | * TIMEZONE => Kamchatka Standard Time 120 | * PARAGRAPH => Cij wam lijoso fa molah il nasiskil ho andot akbuh uku zikahek. Ji balsiffe puzmaano nuug bofevu ra tehar heuwa zorjul hej na heci aka webo lorresu uwage uhe nirsiam. 121 | * HASH => 61960319307b5f8d298141627 122 | * Insert Lorem Ipsum 123 | * Paragraphs 124 | * Sentences 125 | * Words 126 | * Insert random number 127 | * Insert random currency amount 128 | * US Dollar 129 | * Euro 130 | * British Pound 131 | * Japanese Yen 132 | * Chinese Yuan 133 | * Indian Rupee 134 | * Mexican Peso 135 | * Israeli New Shequel 136 | * Bitcoin 137 | * South Korean Won 138 | * South African Rand 139 | * Swiss Franc 140 | * Insert line numbers 141 | * Insert numbers sequence 142 | * Pad Selection Right 143 | * Pad Selection Left 144 | * Prefix with... 145 | * Suffix with... 146 | * Surround with... 147 | * Insert line separator... 148 | 149 | _Note_: If multiple cursors are active, ask the user if to insert the same random value or unique random values at each cursor's position 150 | 151 | ### Filter 152 | 153 | * Filter lines, result in new Editor 154 | * use RegExp (default) or set `TextToolbox.filtersUseRegularExpressions` to `false` to use simple string match instead 155 | * RegExp allow for a more targeted search; use global flags to fine tune your search. RegExp must use forward slashes (`/`) to delimit the expression and the global flags (optional): `//flags` 156 | * `/\d.*/gm` 157 | * string match allows to find all lines containing the string you are looking for, the string must match exactly 158 | 159 | ### Open 160 | 161 | * Open under cursor 162 | * Position the cursor on a file path, URL or email address and open it with `alt+o` on Windows (`cmd+o` on Mac) 163 | 164 | ### Remove 165 | 166 | * Remove all empty lines 167 | * remove all empty lines from the current document 168 | * Remove redundant empty line 169 | * remove all redundant empty lines from the current document: reduces multiple empty lines to one 170 | * Remove duplicate lines 171 | * Remove duplicate lines, result in new Editor 172 | * Remove brackets 173 | * Remove quotes 174 | * Cycle brackets 175 | * Cycle quotes 176 | 177 | ### Sort 178 | 179 | * Sort lines 180 | * Ascending 181 | * Descending 182 | * Reverse 183 | * Sort lines by length 184 | * Ascending 185 | * Descending 186 | * Reverse 187 | 188 | ### Control characters 189 | 190 | * Highlight control characters 191 | * By default control characters are highlighted with a red box but color and border shape can be customized through `TextToolbox.decorateControlCharacters` 192 | * Remove control characters 193 | * By default control characters are replaced with an empty string but this can be changed through `TextToolbox.replaceControlCharactersWith`. 194 | * Removes control characters from the current selection(s) or from the entire document if no text is selected 195 | 196 | ### Split 197 | 198 | * Split selection based on delimiter 199 | * Split and open in new editor 200 | 201 | ### Json 202 | 203 | * Stringify Json 204 | * Fix and Format Json 205 | * Minify Json 206 | * Fix Win32 path in Json 207 | 208 | ### Highlight text 209 | 210 | * Highlight 211 | * Highlight with color... 212 | * Highlight all matches, case sensitive 213 | * Highlight all matches, case insensitive 214 | * Highlight all matches, case sensitive, with color... 215 | * Highlight all matches, case insensitive, with color... 216 | * Highlight with RegExp 217 | * Highlight with RegExp with color... 218 | * Remove all highlights 219 | * Remove highlight 220 | 221 | _Note_: In this release, highlights and decorations are persisted as long as the VSCode instance is running but are not restored if VSCode is restarted. Persistence across restarts will be added in a future release. 222 | 223 | ### Align 224 | 225 | * Align as table 226 | * Formats a CSV selection as a markdown style table: does not need to be a markdown file, but the table is formatted using the markdown stile 227 | * Align as table with headers 228 | * Uses the first line in the CSV selection as table headers, markdown style 229 | * Align to separator 230 | 231 | Align commands can use RegExp to identify the separator; for example you can use `\s` for space and `\t` for tabs 232 | 233 | ### Tab Out 234 | 235 | * Toggle Tab Out 236 | * Tab Out of brackets, quotes and some punctuation 237 | * _Note_: Tab Out is always disabled it the cursor is at the beginning of a line (position 0) to allow to indent its text 238 | * Enable or disable the feature for all languages with `TextToolbox.tabOut.enabled`: default `true` 239 | * Show or hide a message in the Status Bar when Tab Out is enabled with `TextToolbox.tabOut.showInStatusBar`: default `true` 240 | * Control which characters (brackets, quotes etc.) can be _tabbed out_ of 241 | * Choose for which language types Tab Out is enabled, with `TextToolbox.tabOut.enabledLanguages`: default `*` (enabled for all languages) 242 | * Choose for which language types Tab Out is disabled, with `TextToolbox.tabOut.disableLanguages`: default empty, meaning Tab Out is not explicitly disabled for any language 243 | * You can combine `TextToolbox.tabOut.disableLanguages` and `TextToolbox.tabOut.enableLanguages` to fine-tune how Tab Out should work: for example you can enable Tab Out for all language types except `plaintext` with: 244 | 245 | ```json 246 | { 247 | "TextToolbox.tabOut.enableLanguages": ["*"], 248 | "TextToolbox.tabOut.disableLanguages": ["plaintext"] 249 | } 250 | ``` 251 | 252 | ### Indentation 253 | 254 | * Indent using 2 spaces 255 | * Indent using 4 spaces 256 | 257 | ### Ordered List 258 | 259 | * Transform to Ordered List 260 | * Currently supports the following ordered list types: 261 | * `1.` => number. 262 | * `1)` => number) 263 | * `a.` => lowercase. 264 | * `a)` => lowercase) 265 | * `A.` => UPPERCASE. 266 | * `A)` => UPPPERCASE) 267 | * `i.` => Roman lowercase. 268 | * `i)` => Roman lowercase) 269 | * `I.` => Roman UPPERCASE. 270 | * `I)` => Roman UPPERCASE) 271 | 272 | ### Others 273 | 274 | * Open selection(s) in new editor 275 | * Duplicate tab (open the current document's text in a new unsaved document) 276 | * Select brackets content 277 | * Select quotes content 278 | 279 | ### Status Bar 280 | 281 | * Show the number of lines in a selection 282 | * Show the number of words in a selection or the number of words in the document if there is no selection 283 | * Show the cursor position (active offset) 284 | * Show if Tab Out is enabled for the document 285 | 286 | ## My other extensions 287 | 288 | * [Virtual Repos](https://github.com/carlocardella/vscode-VirtualRepos): Virtual Repos is a Visual Studio Code extension that allows to open and edit a remote repository (e.g. on GitHub) without cloning, committing or pushing your changes. It all happens automatically 289 | * [Virtual Gists](https://github.com/carlocardella/vscode-VirtualGists): Virtual Gists is a Visual Studio Code extension that allows to open and edit a remote gist (e.g. on GitHub) without cloning, committing or pushing your changes. It all happens automatically 290 | * [Virtual Git](https://github.com/carlocardella/vscode-VirtualGit): VSCode extension path with my extensions to work with virtual repositories and gists based on a virtual file system 291 | 292 | * [File System Toolbox](https://github.com/carlocardella/vscode-FileSystemToolbox): VSCode extension to work with the file system, path auto-complete on any file type 293 | * [Changelog Manager](https://github.com/carlocardella/vscode-ChangelogManager): VSCode extension, helps to build a changelog for your project, either in markdown or plain text files. The changelog format follows Keep a changelog 294 | * [Hogwarts colors for Visual Studio Code](https://github.com/carlocardella/hogwarts-colors-for-vscode): Visual Studio theme colors inspired by Harry Potter, Hogwarts and Hogwarts Houses colors and banners 295 | 296 | ## Acknowledgements 297 | 298 | Text Toolbox is freely inspired by these fine extensions: 299 | 300 | * [gurayyarar DummyTextGenerator](https://marketplace.visualstudio.com/items?itemName=gurayyarar.dummytextgenerator) 301 | * [qcz vscode-text-power-tools](https://marketplace.visualstudio.com/items?itemName=qcz.text-power-tools) 302 | * [tomoki1207 selectline-statusbar](https://marketplace.visualstudio.com/items?itemName=tomoki1207.selectline-statusbar) 303 | * [adamwalzer string-converter](https://marketplace.visualstudio.com/items?itemName=adamwalzer.string-converter) 304 | * [Tyriar vscode-sort-lines](https://marketplace.visualstudio.com/items?itemName=Tyriar.sort-lines) 305 | * [rpeshkov vscode-text-tables](https://marketplace.visualstudio.com/items?itemName=RomanPeshkov.vscode-text-tables) 306 | * [wmaurer vscode-change-case](https://github.com/wmaurer/vscode-change-case) 307 | * [volkerdobler insertnums](https://marketplace.visualstudio.com/items?itemName=volkerdobler.insertnums) 308 | * [WengerK vscode-highlight-bad-chars](https://marketplace.visualstudio.com/items?itemName=wengerk.highlight-bad-chars#overview) 309 | * [nhoizey vscode-gremlins](https://marketplace.visualstudio.com/items?itemName=nhoizey.gremlins) 310 | * [Pustelto Bracketeer](https://marketplace.visualstudio.com/items?itemName=pustelto.bracketeer) 311 | * [albertromkes tabout](https://github.com/albertromkes/tabout) 312 | * [Compulim indent4to2](https://marketplace.visualstudio.com/items?itemName=Compulim.indent4to2) 313 | -------------------------------------------------------------------------------- /assets/extension_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlocardella/vscode-TextToolbox/57010c9474d70787f67137189d1d977941ef7b56/assets/extension_icon.png -------------------------------------------------------------------------------- /assets/text-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlocardella/vscode-TextToolbox/57010c9474d70787f67137189d1d977941ef7b56/assets/text-tool.png -------------------------------------------------------------------------------- /assets/text_icon_regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlocardella/vscode-TextToolbox/57010c9474d70787f67137189d1d977941ef7b56/assets/text_icon_regular.png -------------------------------------------------------------------------------- /assets/text_icon_reverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlocardella/vscode-TextToolbox/57010c9474d70787f67137189d1d977941ef7b56/assets/text_icon_reverse.png -------------------------------------------------------------------------------- /src/modules/alignText.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, TextLine, window, workspace } from "vscode"; 2 | import { getActiveEditor, getLinesFromSelection } from "./helpers"; 3 | 4 | /** 5 | * Asks the user which separator to use for the alignment. Default is comma (","). 6 | * @return {*} {Promise} 7 | * @async 8 | */ 9 | export async function aksForSeparator(): Promise { 10 | const config = workspace.getConfiguration().get("TextToolbox.alignTextDefaultSeparator"); 11 | const separator = await window.showInputBox({ ignoreFocusOut: true, value: config, prompt: "Choose a separator" }); 12 | if (!separator) { 13 | return Promise.reject("Operation cancelled by the user"); 14 | } 15 | 16 | return Promise.resolve(separator); 17 | } 18 | 19 | /** 20 | * Returns a lineElements array from the selection; each lineElement is a tuple [TextLine, string[]] 21 | * @param {TextEditor} editor The active text editor to use to get the selections from 22 | * @param {string} separator the separator character to use to split each line and retrieve its elements 23 | * @return {*} {Promise} 24 | * @async 25 | */ 26 | async function getLineElements(editor: TextEditor, separator: string): Promise { 27 | const lines = getLinesFromSelection(editor); 28 | if (!lines) { 29 | return Promise.reject(); 30 | } 31 | 32 | let elements: any[] = []; 33 | 34 | lines.forEach((line) => { 35 | let element = new LineElement(line, separator); 36 | elements.push(element); 37 | }); 38 | 39 | return Promise.resolve(elements); 40 | } 41 | 42 | /** 43 | * Returns the length of the longest element; it can be used to pad other string elements 44 | * @param {LineElement[]} lineElements Array of line elements to measure and retrieve the max length from 45 | * @return {*} {Promise} 46 | * @async 47 | */ 48 | async function getElementMaxLength(lineElements: LineElement[]): Promise { 49 | let maxElementLength: number = 0; 50 | lineElements.forEach((lineElement) => { 51 | lineElement.Elements.forEach((element) => { 52 | if (element.length > maxElementLength) { 53 | maxElementLength = element.length; 54 | } 55 | }); 56 | }); 57 | 58 | return Promise.resolve(maxElementLength); 59 | } 60 | 61 | /** 62 | * Align the text in the active editor 63 | * @export 64 | * @param {string} [separator] The separator to use to align the text 65 | * @return {*} {Promise} 66 | */ 67 | export function alignText(separator?: string, formatTable?: boolean, withHeaders?: boolean): Promise { 68 | return new Promise(async (resolve, reject) => { 69 | if (!separator) { 70 | separator = await aksForSeparator(); 71 | } 72 | if (!separator) { 73 | return reject(false); 74 | } 75 | 76 | const editor = getActiveEditor(); 77 | if (!editor) { 78 | return reject(false); 79 | } 80 | 81 | if (editor.selection.isEmpty) { 82 | return reject(false); 83 | } 84 | 85 | // fix: ^ rejected promise not handled within 1 second 86 | 87 | const columns = await getColumns(editor, separator); 88 | let newLineText: string = ""; 89 | editor.edit((editBuilder) => { 90 | formatTable ? (newLineText = padAsTable(columns, withHeaders)) : (newLineText = padToSeparator(columns, separator!)); 91 | editBuilder.replace(editor.selection, newLineText); 92 | }); 93 | 94 | return resolve(true); 95 | }); 96 | } 97 | 98 | /** 99 | * Returns an array of ColumnElements, each ColumnElement is a tuple [LineNumber, Length, Text]. 100 | * LineNumber indicates to which line number each column belongs to. 101 | * Length indicates the max length of the column: this is used to pad shorter columns. 102 | * Text is the string contained in that particular cell (Column at LineNumber) 103 | * @param {TextEditor} editor The text editor to use to get the selections from 104 | * @param {string} separator The separator to use to split each line and retrieve its elements 105 | * @return {*} {Promise} 106 | */ 107 | async function getColumns(editor: TextEditor, separator: string): Promise { 108 | return new Promise(async (resolve, reject) => { 109 | let lines = await getLineElements(editor, separator); 110 | 111 | let columns: ColumnElement[] = []; 112 | let longestLineLength = getLongestLine(lines); 113 | 114 | // fix: bad alignment and lost column if one of the tokens is empty 115 | // e.g. 116 | // asd, asd, asd, asd 117 | // asd, asd,, asd 118 | // asd, asd, asd, asd 119 | 120 | for (let ii = 0; ii < longestLineLength; ii++) { 121 | let longestColumnElement = 0; 122 | // let lineNumber = 0; 123 | lines.forEach((line) => { 124 | if (line.Elements[ii] && line.Elements[ii].length > longestColumnElement) { 125 | longestColumnElement = line.Elements[ii].length; 126 | } 127 | // lineNumber = line.LineNumber; 128 | }); 129 | 130 | lines.forEach((line) => { 131 | columns.push(new ColumnElement(line.LineNumber, line.Elements[ii], longestColumnElement)); 132 | }); 133 | } 134 | 135 | resolve(columns); 136 | }); 137 | } 138 | 139 | /** 140 | * Get the longest line (in term of number of elements) from the lineElements array 141 | * @param {LineElement[]} lines Array of line elements to measure and retrieve the max length from 142 | * @return {*} {number} 143 | */ 144 | function getLongestLine(lines: LineElement[]): number { 145 | let longestLine: number = 0; 146 | 147 | lines.forEach((line) => { 148 | if (line.Elements.length > longestLine) { 149 | longestLine = line.Elements.length; 150 | } 151 | }); 152 | 153 | return longestLine; 154 | } 155 | 156 | /** 157 | * Build the aligned text from the columnElements array 158 | * @param {ColumnElement[]} columns Array of column elements to build the aligned text from 159 | * @param {string} separator The separator to use to build the aligned text 160 | * @return {*} {string} 161 | */ 162 | function padToSeparator(columns: ColumnElement[], separator: string): string { 163 | let newLineText: string = ""; 164 | const lines = getLinesFromSelection(getActiveEditor()!); 165 | 166 | let paddedElement = ""; 167 | for (let line of lines!) { 168 | let c = columns.filter((column) => column.LineNumber === line.lineNumber); 169 | 170 | for (let ii = 0; ii < c.length; ii++) { 171 | let paddedElement = ""; 172 | 173 | if (c[ii].Text) { 174 | if (ii === c.length - 1) { 175 | paddedElement = `${c[ii].Text}`.padEnd(c[ii].Length + 1, " "); 176 | } else { 177 | separator = separator === "\\s" || separator === "\\t" ? " " : separator; // @hack 178 | paddedElement = `${c[ii].Text}${separator}`.padEnd(c[ii].Length + 2, " "); 179 | } 180 | } else { 181 | paddedElement = separator; 182 | } 183 | newLineText += paddedElement; 184 | } 185 | 186 | newLineText += "\n"; 187 | } 188 | 189 | return newLineText; 190 | } 191 | 192 | /** 193 | * Build the aligned text from the columnElements array, formatted as a markdown table 194 | * @param {ColumnElement[]} columns Array of column elements to build the aligned text from 195 | * @param {(boolean | undefined)} withHeaders If true, the first line will be formatted as a table header 196 | * @return {*} {string} 197 | */ 198 | function padAsTable(columns: ColumnElement[], withHeaders: boolean | undefined): string { 199 | const lines = getLinesFromSelection(getActiveEditor()!); 200 | let sSeparator = "| "; 201 | let rSeparator = " |"; 202 | let newLineText = ""; 203 | 204 | for (let line of lines!) { 205 | let c = columns.filter((column) => column.LineNumber === line.lineNumber); 206 | 207 | for (let ii = 0; ii < c.length; ii++) { 208 | let paddedElement = ""; 209 | let text = ""; 210 | c[ii].Text ? (text = c[ii].Text) : (text = ""); 211 | 212 | if (ii === c.length - 1) { 213 | paddedElement = `${sSeparator}${text}`.padEnd(c[ii].Length + 2, " ") + rSeparator; 214 | } else { 215 | paddedElement = `${sSeparator}${text}`.padEnd(c[ii].Length + 3, " "); 216 | } 217 | newLineText += paddedElement; 218 | } 219 | newLineText += "\n"; 220 | 221 | // add headers separator row 222 | if (withHeaders && line.lineNumber === 0) { 223 | for (let ii = 0; ii < c.length; ii++) { 224 | let paddedElement = ""; 225 | if (ii === c.length - 1) { 226 | paddedElement = `|${"".padEnd(c[ii].Length + 2, "-")}` + "|"; 227 | } else { 228 | paddedElement = `|${"".padEnd(c[ii].Length + 2, "-")}`; 229 | } 230 | newLineText += paddedElement; 231 | } 232 | newLineText += "\n"; 233 | } 234 | } 235 | 236 | return newLineText; 237 | } 238 | 239 | /** 240 | * LineElement type, used to describe line elements to pad and align 241 | * @param line {TextLine} TextLine type 242 | * @param separator {string} Separator to use split the line and extract the elements to align 243 | */ 244 | class LineElement { 245 | public LineNumber: number; 246 | public Text: TextLine; 247 | public Elements: string[]; 248 | 249 | constructor(line: TextLine, separator: string) { 250 | this.LineNumber = line.lineNumber; 251 | this.Text = line; 252 | separator = separator === "\\t" ? "\\s" : separator; // @hack: fix tab separator 253 | this.Elements = line.text.split(new RegExp(separator)).filter((e) => e); 254 | } 255 | } 256 | 257 | /** 258 | * ColumnElement type, used to describe column elements to pad and align 259 | * @param Id: {number} Column index 260 | * @param Text: {string} Text to pad and align 261 | * @param Length: {number} Length of the column padded text 262 | * @class ColumnElement 263 | */ 264 | class ColumnElement { 265 | public LineNumber: number; 266 | public Text: string; 267 | public Length: number; 268 | 269 | constructor(lineNumber: number, text: string, length: number) { 270 | this.LineNumber = lineNumber; 271 | this.Text = text; 272 | this.Length = length; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/modules/caseConversion.ts: -------------------------------------------------------------------------------- 1 | import { validateSelection, getActiveEditor } from "./helpers"; 2 | import { Range } from "vscode"; 3 | import * as os from "os"; 4 | 5 | /** 6 | * Case conversion types 7 | */ 8 | export const enum caseConversions { 9 | camelCase = "camelCase", 10 | constantCase = "constantCase", 11 | dotCase = "dotCase", 12 | headerCase = "headerCase", 13 | kebabCase = "kebabCase", 14 | pascalCase = "pascalCase", 15 | pathCase = "pathCase", 16 | sentenceCase = "sentenceCase", 17 | snakeCase = "snakeCase", 18 | invertCase = "invertCase", 19 | capitalCase = "capitalCase", 20 | // titleCase = "titleCase", 21 | } 22 | 23 | /** 24 | * Converts the active selection according to the selected type 25 | * @param {caseConversions} conversion The type of case conversion to perform on the active selection 26 | */ 27 | export function convertSelection(conversion: caseConversions) { 28 | validateSelection(); 29 | 30 | const editor = getActiveEditor(); 31 | editor?.edit((editBuilder) => { 32 | editor.selections.forEach((selection) => { 33 | switch (conversion) { 34 | case caseConversions.pascalCase: 35 | editBuilder.replace(selection, ConvertCase.toPascalCase(editor.document.getText(new Range(selection.start, selection.end)))); 36 | break; 37 | case caseConversions.camelCase: 38 | editBuilder.replace(selection, ConvertCase.toCamelCase(editor.document.getText(new Range(selection.start, selection.end)))); 39 | break; 40 | case caseConversions.constantCase: 41 | editBuilder.replace(selection, ConvertCase.toConstantCase(editor.document.getText(new Range(selection.start, selection.end)))); 42 | break; 43 | case caseConversions.dotCase: 44 | editBuilder.replace(selection, ConvertCase.toDotCase(editor.document.getText(new Range(selection.start, selection.end)))); 45 | break; 46 | case caseConversions.headerCase: 47 | editBuilder.replace(selection, ConvertCase.toHeaderCase(editor.document.getText(new Range(selection.start, selection.end)))); 48 | break; 49 | case caseConversions.kebabCase: 50 | editBuilder.replace(selection, ConvertCase.toKebabCase(editor.document.getText(new Range(selection.start, selection.end)))); 51 | break; 52 | case caseConversions.pathCase: 53 | editBuilder.replace(selection, ConvertCase.toPathCase(editor.document.getText(new Range(selection.start, selection.end)))); 54 | break; 55 | case caseConversions.sentenceCase: 56 | editBuilder.replace(selection, ConvertCase.toSentenceCase(editor.document.getText(new Range(selection.start, selection.end)))); 57 | break; 58 | case caseConversions.snakeCase: 59 | editBuilder.replace(selection, ConvertCase.toSnakeCase(editor.document.getText(new Range(selection.start, selection.end)))); 60 | break; 61 | case caseConversions.invertCase: 62 | editBuilder.replace(selection, ConvertCase.invertCase(editor.document.getText(new Range(selection.start, selection.end)))); 63 | break; 64 | case caseConversions.capitalCase: 65 | editBuilder.replace(selection, ConvertCase.toCapitalCase(editor.document.getText(new Range(selection.start, selection.end)))); 66 | break; 67 | // case caseConversions.titleCase: 68 | // editBuilder.replace(selection, ConvertCase.toTitleCase(editor.document.getText(new Range(selection.start, selection.end)))); 69 | // break; 70 | } 71 | }); 72 | }); 73 | } 74 | 75 | class ConvertCase { 76 | static toPascalCase(text: string): string { 77 | return text 78 | .replace(/(?<=[a-z])[A-Z]/gm, (match) => match.replace(match, ` ${match}`)) 79 | .toLowerCase() 80 | .replace(/\b\w|[ \t]./g, (match) => match.toUpperCase()) 81 | .split(" ") 82 | .join(""); 83 | } 84 | 85 | static toCamelCase(text: string): string { 86 | return text 87 | .replace(/(?<=[a-z])[A-Z]/gm, (match) => match.replace(match, ` ${match}`)) 88 | .toLowerCase() 89 | .replace(/[ \t]([^A-Z])/g, (match, group1) => group1.toUpperCase()); 90 | } 91 | 92 | static toSnakeCase(text: string): string { 93 | return text 94 | .replace(/(?<=[a-z])[A-Z]/gm, (match) => match.replace(match, `_${match}`)) 95 | .replace(/(? match.replace(match, `-${match}`)) 102 | .replace(/(? match.replace(match, `.${match}`)) 114 | .replace(/(? match.replace(match, `/${match}`)) 120 | .replace(/(? group1.toUpperCase()); 130 | return text 131 | .replace(/(?<=[a-z])[A-Z]/gm, (match) => match.replace(match, ` ${match}`)) 132 | .toLowerCase() 133 | .replace(/^./, (match) => match.toUpperCase()); 134 | } 135 | 136 | static toCapitalCase(text: string): string { 137 | return text.replace(/\b\w/g, (_) => _.toUpperCase()); 138 | } 139 | 140 | // static toTitleCase(text: string): string { 141 | // const exclusions = ["in", "to", "and", "but", "for", "nor", "the", "a", "an", "as", "it"]; 142 | // const theLastWord = text.split(" ").length - 1; 143 | // return text 144 | // .split(" ") 145 | // .map((word, index) => { 146 | // if (index === theLastWord) { 147 | // return word.charAt(0).toUpperCase() + word.slice(1); 148 | // } else if (exclusions.indexOf(word) > -1) { 149 | // return word.toLowerCase(); 150 | // } else { 151 | // return word.charAt(0).toUpperCase() + word.slice(1); 152 | // } 153 | // }) 154 | // .join(" "); 155 | // } 156 | 157 | /** 158 | * Invert the case of the selected text 159 | * @param {string} text The text to invert 160 | * @return {*} {string} 161 | */ 162 | static invertCase(text: string): string { 163 | return text 164 | .split("") 165 | .map((value) => { 166 | let char = value; 167 | if (value === value.toUpperCase()) { 168 | char = value.toLowerCase(); 169 | } else if (value === value.toLowerCase()) { 170 | char = value.toUpperCase(); 171 | } 172 | return char; 173 | }) 174 | .join(""); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/modules/controlCharacters.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, TextEditorDecorationType, workspace, window, DecorationRenderOptions, DecorationOptions, Range } from "vscode"; 2 | import { getSelections, getTextFromSelection, getActiveEditor, getDocumentTextOrSelection } from "./helpers"; 3 | 4 | // todo: test https://weblogs.asp.net/kon/finding-those-pesky-unicode-characters-in-visual-studio 5 | /** 6 | * Array of control (bad) chars 7 | * @type {*} 8 | */ 9 | export const chars = [ 10 | // https://github.com/possan/sublime_unicode_nbsp/blob/master/sublime_unicode_nbsp.py 11 | "(\x82)", // High code comma 12 | "(\x84)", // High code double comma 13 | "(\x85)", // Triple dot 14 | "(\x88)", // High carat 15 | "(\x91)", // Forward single quote 16 | "(\x92)", // Reverse single quote 17 | "(\x93)", // Forward double quote 18 | "(\x94)", // Reverse double quote 19 | "(\x95)", // Message Waiting 20 | "(\x96)", // High hyphen 21 | "(\x97)", // Double hyphen 22 | "(\x99)", // 23 | "(\xA0)", // No-break space 24 | "(\xA6)", // Split vertical bar 25 | "(\xAB)", // Double less than 26 | "(\xBB)", // Double greater than 27 | "(\xBC)", // one quarter 28 | "(\xBD)", // one half 29 | "(\xBE)", // three quarters 30 | "(\xBF)", // c-single quote 31 | "(\xA8)", // modifier - under curve 32 | "(\xB1)", // modifier - under line 33 | 34 | // https://www.cs.tut.fi/~jkorpela/chars/spaces.html 35 | "(\u00A0)", // no-break space 36 | "(\u1680)", // ogham space mark 37 | "(\u180E)", // mongolian vowel separator 38 | "(\u2000)", // en quad 39 | "(\u2001)", // em quad 40 | "(\u2002)", // en space 41 | "(\u2003)", // em space 42 | "(\u2004)", // three-per-em space 43 | "(\u2005)", // four-per-em space 44 | "(\u2006)", // six-per-em space 45 | "(\u2007)", // figure space 46 | "(\u2008)", // punctuation space 47 | "(\u2009)", // thin space 48 | "(\u200A)", // hair space 49 | "(\u200B)", // zero width space 50 | "(\u200D)", // zero width joiner 51 | "(\u2013)", // en dash 52 | "(\u2014)", // em dash 53 | "(\u2028)", // line separator space 54 | "(\u202F)", // narrow no-break space 55 | "(\u205F)", // medium mathematical space 56 | "(\u3000)", // ideographic space 57 | "(\uFEFF)", // zero width no-break space 58 | 59 | // others 60 | "(\u037E)", // greek question mark 61 | "(\u0000)", // 62 | "(\u0011)", // 63 | "(\u0012)", // 64 | "(\u0013)", // 65 | "(\u0014)", // 66 | "(\u001B)", // 67 | "(\u0080)", // 68 | "(\u0090)", // 69 | "(\u009B)", // 70 | "(\u009F)", // 71 | "(\u00B8)", // cedilla 72 | "(\u01C0)", // latin letter dental click 73 | "(\u2223)", // divides 74 | "(\u0008)", 75 | "(\u000c)", 76 | "(\u000e)", 77 | "(\u001f)", 78 | "(\u007f)", 79 | "(\u2018)", // "left single quotation mark", 80 | "(\u2019)", // "right single quotation mark", 81 | "(\u2029)", // "paragraph separator", 82 | "(\u0003)", // "end of text", 83 | "(\u000b)", // "line tabulation", 84 | "(\u00ad)", // "soft hyphen", 85 | "(\u200c)", // "zero width non-joiner", 86 | "(\u200e)", // "left-to-right mark", 87 | "(\u201c)", // "left double quotation mark", 88 | "(\u201d)", // "right double quotation mark", 89 | "(\u202c)", // "pop directional formatting", 90 | "(\u202d)", // "left-to-right override", 91 | "(\u202e)", // "right-to-left override", 92 | "(\ufffc)", // "object replacement character", 93 | ]; 94 | 95 | const replacementMap: { [key: string]: string } = { 96 | "\u2018": "'", 97 | "\u2019": "'", 98 | "\u201C": '"', 99 | "\u201D": '"', 100 | "\x85": "...", // Triple dot 101 | "\x91": "'", // Forward single quote 102 | "\x92": "'", // Reverse single quote 103 | "\x93": '"', // Forward double quote 104 | "\x94": '"', // Reverse double quote 105 | "\u00a0": " ", // invisible space 106 | "\u200c": " ", // invisible space 107 | "\u200B": "", // zero width space 108 | "\u00ad": "", // zero width space 109 | "\u200e": "", // zero width space 110 | "\u2013": "-", // en dash 111 | "\u2014": "-", // em dash 112 | }; 113 | 114 | // const regExpText = "[" + chars.join("") + "]"; 115 | const regExpText = chars.join("|"); 116 | const regexp = new RegExp(regExpText, "g"); 117 | let textEditorDecorationType: TextEditorDecorationType; 118 | 119 | let decorator: any; 120 | 121 | /** 122 | * Decorate control (bad) characters 123 | * @param {TextEditor} editor The Text Editor to decorate 124 | * @param {boolean} configurationChanged If true, reads the TextToolbox.decorateControlCharacters configuration 125 | * @async 126 | */ 127 | export async function decorateControlCharacters(editor: TextEditor, configurationChanged?: boolean): Promise { 128 | if (configurationChanged || !textEditorDecorationType) { 129 | // update textEditorDecorationType only if the value is empty or if TextToolbox.decorateControlCharacters has changed; 130 | // this is to properly maintain/update/remove existing decorations. 131 | // If textEditorDecorationType is updated unnecessarily, the decorations become all confused 132 | const decorationRenderOptions = workspace.getConfiguration("TextToolbox").decorateControlCharacters; 133 | textEditorDecorationType = await newDecorator(decorationRenderOptions); 134 | } 135 | updateDecorations(editor, regexp, textEditorDecorationType); 136 | return Promise.resolve(); 137 | } 138 | 139 | /** 140 | * Replace Unicode characters with their ASCII equivalent 141 | * @param {TextEditor} [editor] The active text editor 142 | * @async 143 | */ 144 | export async function replaceControlCharacters(editor?: TextEditor): Promise { 145 | if (!editor) { 146 | editor = getActiveEditor(); 147 | } 148 | if (!editor) { 149 | return Promise.reject(); 150 | } 151 | 152 | let selections = await getSelections(editor); 153 | let text: string | undefined; 154 | let newText: string; 155 | 156 | editor.edit((editBuilder) => { 157 | selections.forEach(async (selection) => { 158 | text = getTextFromSelection(editor!, selection); 159 | newText = text?.replace(regexp, (match) => replacementMap[match] ?? " ")!; 160 | editBuilder.replace(selection, newText); 161 | }); 162 | }); 163 | 164 | return Promise.resolve(); 165 | } 166 | 167 | /** 168 | * Creates a new Decorator object to decorate text in a Text Editor 169 | * @param {DecorationRenderOptions} decorationRenderOptions Configuration to use with the new decoration 170 | * @return {*} {Promise} 171 | * @async 172 | */ 173 | export async function newDecorator(decorationRenderOptions: DecorationRenderOptions): Promise { 174 | decorator = window.createTextEditorDecorationType(decorationRenderOptions); 175 | return Promise.resolve(decorator); 176 | } 177 | 178 | /** 179 | * Decorates text in the passed in editor, using the passed regular expression 180 | * @param {TextEditor} editor The editor containing the text to decorate 181 | * @param {RegExp} regExp The regular expression to use to decorate the text in the active editor 182 | * @param {TextEditorDecorationType} textEditorDecorationType Decorations style to be used in the active Text Editor 183 | * @return {*} 184 | */ 185 | export async function updateDecorations(editor: TextEditor, regExp: RegExp, textEditorDecorationType: TextEditorDecorationType) { 186 | const text = getDocumentTextOrSelection(); 187 | if (!text) { 188 | return; 189 | } 190 | 191 | const decorationOptions: DecorationOptions[] = []; 192 | let match; 193 | 194 | while ((match = regExp.exec(text))) { 195 | const start = editor.document.positionAt(match.index); 196 | const end = editor.document.positionAt(match.index + match[0].length); 197 | let charCode = text.charCodeAt(match.index).toString(16); 198 | const decoration = { range: new Range(start, end), hoverMessage: `${charCode}` }; 199 | decorationOptions.push(decoration); 200 | } 201 | 202 | editor.setDecorations(textEditorDecorationType, decorationOptions); 203 | } 204 | -------------------------------------------------------------------------------- /src/modules/copyText.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { getActiveEditor } from './helpers'; 3 | 4 | export function copyTextWithMetadata(): Promise { 5 | const editor = getActiveEditor(); 6 | if (!editor) { return Promise.reject(); } 7 | 8 | 9 | return Promise.resolve(); 10 | } -------------------------------------------------------------------------------- /src/modules/decorations.ts: -------------------------------------------------------------------------------- 1 | import { DecorationRenderOptions, Range, Selection, window, workspace, TextEditorDecorationType, Position } from "vscode"; 2 | import { getActiveEditor, getLinesFromDocumentOrSelection, getTextFromSelection, getRegExpObject } from "./helpers"; 3 | import { Chance } from "chance"; 4 | 5 | /** 6 | * TTDecoration type 7 | * 8 | * @class TTDecoration 9 | */ 10 | class TTDecoration { 11 | public Range: Range; 12 | public DecorationType: TextEditorDecorationType; 13 | public Index: any; 14 | public File: string; 15 | 16 | /** 17 | * Creates an instance of TTDecoration. 18 | * @param {Range} range Range to decorate 19 | * @param {DecorationRenderOptions} decoratorRenderOptions Decoration render options 20 | * @memberof TTDecoration 21 | */ 22 | constructor(range: Range, decoratorRenderOptions: DecorationRenderOptions, file: string) { 23 | this.Range = range; 24 | this.DecorationType = window.createTextEditorDecorationType(decoratorRenderOptions!); 25 | this.File = file; 26 | 27 | const chance = new Chance(); 28 | this.Index = chance.guid(); 29 | } 30 | } 31 | 32 | class TTDecorationRange { 33 | Range: Range; 34 | File: string; 35 | 36 | constructor(range: Range, file: string) { 37 | this.Range = range; 38 | this.File = file; 39 | } 40 | } 41 | 42 | interface IRangeToHighlightSettings { 43 | allMatches?: boolean; 44 | matchCase?: boolean; 45 | regex?: boolean; 46 | } 47 | 48 | /** 49 | * Interface for a decoration provider. 50 | * 51 | * @interface TTDecorators 52 | */ 53 | interface IDecorators { 54 | // HighlightText(pickDefaultDecorator: boolean, rangeToHighlight: Range[]): void; 55 | // RefreshHighlights(): void; 56 | // RemoveHighlight(removeAll: boolean): void; 57 | // GetRangeToHighlight(matchCase: boolean, regex?: RegExp): Promise; 58 | } 59 | 60 | /** 61 | * Class to manage text highlight (decorations) 62 | * 63 | * @export 64 | * @class TTDecorations 65 | * @implements {IDecorators} 66 | */ 67 | export default class TTDecorations implements IDecorators { 68 | public Decorators: TTDecoration[] = []; 69 | private config = workspace.getConfiguration("TextToolbox"); 70 | 71 | /** 72 | * Creates an instance of TTDecorations. 73 | * @memberof TTDecorations 74 | */ 75 | constructor() {} 76 | 77 | /** 78 | * Highlight the current word or selection 79 | * 80 | * @param {boolean} pickDefaultDecorator Pick a random decorator from the list of available decorators in Settings or ask the user for a color 81 | * @return {*} 82 | * @memberof TTDecorations 83 | */ 84 | async HighlightText(pickDefaultDecorator: boolean, settings?: IRangeToHighlightSettings, rangeToHighlight?: TTDecorationRange[]): Promise { 85 | const editor = getActiveEditor(); 86 | if (!editor) { 87 | return; 88 | } 89 | 90 | let decoratorRenderOptions: DecorationRenderOptions | undefined; 91 | // default decorator or user input 92 | pickDefaultDecorator ? (decoratorRenderOptions = this.GetRandomHighlight()) : (decoratorRenderOptions = await this.AskForDecorationColor()); 93 | 94 | let decorator: any; 95 | if (!rangeToHighlight) { 96 | rangeToHighlight = await this.GetRangeToHighlight(settings); 97 | } 98 | 99 | rangeToHighlight?.forEach((range) => { 100 | decorator = new TTDecoration(range.Range, decoratorRenderOptions!, editor.document.uri.fsPath); 101 | this.Decorators.push(decorator); 102 | }); 103 | 104 | this.RefreshHighlights(); 105 | return Promise.resolve(); 106 | } 107 | 108 | /** 109 | * Refresh the existing decorations 110 | * 111 | * @return {*} 112 | * @memberof TTDecorations 113 | */ 114 | RefreshHighlights() { 115 | const editor = getActiveEditor(); 116 | if (!editor) { 117 | return; 118 | } 119 | 120 | if (this.Decorators) { 121 | this.Decorators.forEach((d) => { 122 | editor.setDecorations(d.DecorationType, [d.Range]); 123 | }); 124 | } 125 | } 126 | 127 | /** 128 | * Get a random decorator from the list of available decorators in Settings 129 | * 130 | * @private 131 | * @return {*} {DecorationRenderOptions} 132 | * @memberof TTDecorations 133 | */ 134 | private GetRandomHighlight(): DecorationRenderOptions { 135 | const chance = new Chance(); 136 | let decorationColors = this.config.get("decorationDefaults")!; 137 | return chance.pickone(decorationColors); 138 | } 139 | 140 | /** 141 | * Ask for decoration color 142 | * 143 | * @return {*} {(Promise)} 144 | * @memberof TTDecorations 145 | */ 146 | private async AskForDecorationColor(): Promise { 147 | const userColor = await window.showInputBox({ ignoreFocusOut: true, prompt: "Enter a color. Accepted formats: color name, hex, rgba" }); 148 | if (!userColor) { 149 | return Promise.reject(); 150 | } 151 | 152 | const userDecoration: DecorationRenderOptions = { 153 | backgroundColor: userColor, 154 | }; 155 | return Promise.resolve(userDecoration); 156 | } 157 | 158 | /** 159 | * Remove decorations 160 | * 161 | * @return {*} 162 | * @memberof TTDecorations 163 | */ 164 | async RemoveHighlight(removeAll: boolean) { 165 | const editor = getActiveEditor(); 166 | if (!editor) { 167 | return; 168 | } 169 | 170 | if (removeAll) { 171 | this.Decorators.forEach((d) => { 172 | d.Range = new Range(0, 0, 0, 0); // empty range removes the decorator 173 | }); 174 | this.RefreshHighlights(); 175 | this.Decorators = []; 176 | } else { 177 | let rangeToRemove = await this.GetRangeToHighlight(); 178 | if (rangeToRemove) { 179 | let decorationToRemove = this.FindDecoration(rangeToRemove[0].Range); // todo: remove multiple highlights? 180 | if (decorationToRemove) { 181 | decorationToRemove.Range = new Range(0, 0, 0, 0); 182 | } 183 | this.RefreshHighlights(); 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Find an existing decoration 190 | * 191 | * @param {Range} range Range to find 192 | * @return {*} {(TTDecoration | undefined)} 193 | * @memberof TTDecorations 194 | */ 195 | FindDecoration(range: Range, cursorPosition?: Position): TTDecoration | undefined { 196 | let match: TTDecoration | undefined; 197 | 198 | if (range) { 199 | match = this.Decorators.find((d) => d.Range.isEqual(range)); 200 | 201 | if (!match) { 202 | let editor = getActiveEditor(); 203 | match = this.Decorators.find((d) => d.Range.contains(editor!.selection.active)); 204 | } 205 | } 206 | 207 | return match; 208 | } 209 | 210 | /** 211 | * Find word matches 212 | * 213 | * @return {*} 214 | * @memberof TTDecorations 215 | */ 216 | async GetRangeToHighlight(settings?: IRangeToHighlightSettings): Promise { 217 | const editor = getActiveEditor(); 218 | if (!editor) { 219 | return Promise.reject("error"); // fix: undefined? 220 | } 221 | 222 | let rangeToHighlight: Range[] = []; 223 | 224 | if (!settings?.regex) { 225 | if (editor.selection.isEmpty) { 226 | // https://code.visualstudio.com/api/references/vscode-api#TextDocument 227 | // By default words are defined by common separators, like space, -, _, etc. In addition, per language custom [word definitions] can be defined 228 | // note: if the cursor is at the end of line after a word separator (e.g. a comma), getWordRangeAtPosition() returns null therefore Highlight() or RemoveHighlight() will not work. This is by design 229 | let range = editor.document.getWordRangeAtPosition(editor.selection.active); 230 | if (range) { 231 | rangeToHighlight.push(range); 232 | } 233 | } else { 234 | rangeToHighlight.push(new Range(editor.selection.start, editor.selection.end)); 235 | } 236 | } 237 | 238 | let word: any; 239 | if (settings?.regex) { 240 | // search by regex 241 | word = await this.AskForRegEx(); 242 | } else { 243 | // search by selection or cursor position 244 | if (rangeToHighlight.length > 0) { 245 | word = getTextFromSelection(editor, new Selection(rangeToHighlight[0].start, rangeToHighlight[0].end)); 246 | } 247 | } 248 | 249 | if (word === undefined) { 250 | return Promise.reject(); // fix: undefined? 251 | } 252 | 253 | let matches: TTDecorationRange[] = []; 254 | let lines = getLinesFromDocumentOrSelection(editor); 255 | if (settings?.regex) { 256 | lines?.forEach((line) => { 257 | let regExpMatches = line.text.match(word); 258 | // there might be multiple matches in a line 259 | let index = 0; 260 | regExpMatches?.forEach((regExpMatch) => { 261 | index = line.text.indexOf(regExpMatch, index); 262 | if (index > -1) { 263 | let rangeMatch = new Range(line.lineNumber, index, line.lineNumber, index + regExpMatch!.length); 264 | matches.push(new TTDecorationRange(rangeMatch, editor.document.uri.fsPath)); 265 | } 266 | }); 267 | }); 268 | } else if (settings?.allMatches) { 269 | lines?.forEach((line) => { 270 | let index = 0; 271 | settings?.matchCase ? (index = line.text.indexOf(word!)) : (index = line.text.toLowerCase().indexOf(word!.toLowerCase())); 272 | if (index > -1) { 273 | let rangeMatch = new Range(line.lineNumber, index, line.lineNumber, index + word!.length); 274 | matches.push(new TTDecorationRange(rangeMatch, editor.document.uri.fsPath)); 275 | } 276 | }); 277 | } 278 | 279 | rangeToHighlight.forEach((range) => { 280 | matches.push(new TTDecorationRange(range, editor.document.uri.fsPath)); 281 | }); 282 | 283 | return Promise.resolve(matches); 284 | } 285 | 286 | private async AskForRegEx(): Promise { 287 | const regex = await window.showInputBox({ ignoreFocusOut: true, prompt: "Regular Expression to search the document" }); 288 | if (!regex) { 289 | return Promise.reject(); 290 | } 291 | 292 | let regExp = getRegExpObject(regex); 293 | return Promise.resolve(new RegExp(regExp)); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/modules/delimiters.ts: -------------------------------------------------------------------------------- 1 | import { Position, Selection, TextEditor } from "vscode"; 2 | import { addSelection, getActiveEditor, getTextFromSelection } from "./helpers"; 3 | // import { delimiterType } from './QuotesAndParentheses'; 4 | 5 | /** 6 | * Delimiter types to use for selections 7 | * 8 | * @export 9 | * @enum {number} 10 | */ 11 | export enum delimiterTypes { 12 | bracket = "bracket", 13 | quote = "quote", 14 | } 15 | 16 | /** 17 | * Delimiter type, can be open or close 18 | * 19 | * @enum {number} 20 | */ 21 | enum delimiterTypeDirection { 22 | open = "open", 23 | close = "close", 24 | } 25 | 26 | /** 27 | * Delimiter type 28 | * 29 | * @typedef {delimiter} 30 | */ 31 | type delimiter = { 32 | name: string; 33 | char: string; 34 | pairedChar: string; 35 | offset: number | undefined; 36 | pairedOffset: number | undefined; 37 | type: delimiterTypes; 38 | direction: delimiterTypeDirection; 39 | position: number; 40 | }; 41 | 42 | /** 43 | * Delimiters to use for selections 44 | * 45 | * @type {{ 46 | name: string; 47 | char: string; 48 | pairedChar: string; 49 | type: delimiterTypes; 50 | direction: delimiterTypeDirection; 51 | }[]} 52 | */ 53 | const delimiters: { 54 | name: string; 55 | char: string; 56 | pairedChar: string; 57 | type: delimiterTypes; 58 | direction: delimiterTypeDirection; 59 | }[] = [ 60 | { 61 | name: "openRound", 62 | char: "(", 63 | pairedChar: ")", 64 | type: delimiterTypes.bracket, 65 | direction: delimiterTypeDirection.open, 66 | }, 67 | { 68 | name: "openSquare", 69 | char: "[", 70 | pairedChar: "]", 71 | type: delimiterTypes.bracket, 72 | direction: delimiterTypeDirection.open, 73 | }, 74 | { 75 | name: "openCurly", 76 | char: "{", 77 | pairedChar: "}", 78 | type: delimiterTypes.bracket, 79 | direction: delimiterTypeDirection.open, 80 | }, 81 | { 82 | name: "openAngle", 83 | char: "<", 84 | pairedChar: ">", 85 | type: delimiterTypes.bracket, 86 | direction: delimiterTypeDirection.open, 87 | }, 88 | { 89 | name: "openSingleQuote", 90 | char: "'", 91 | pairedChar: "'", 92 | type: delimiterTypes.quote, 93 | direction: delimiterTypeDirection.open, 94 | }, 95 | { 96 | name: "openDoubleQuote", 97 | char: '"', 98 | pairedChar: '"', 99 | type: delimiterTypes.quote, 100 | direction: delimiterTypeDirection.open, 101 | }, 102 | { 103 | name: "openBacktick", 104 | char: "`", 105 | pairedChar: "`", 106 | type: delimiterTypes.quote, 107 | direction: delimiterTypeDirection.open, 108 | }, 109 | { 110 | name: "closeRound", 111 | char: ")", 112 | pairedChar: "(", 113 | type: delimiterTypes.bracket, 114 | direction: delimiterTypeDirection.close, 115 | }, 116 | { 117 | name: "closeSquare", 118 | char: "]", 119 | pairedChar: "[", 120 | type: delimiterTypes.bracket, 121 | direction: delimiterTypeDirection.close, 122 | }, 123 | { 124 | name: "closeCurly", 125 | char: "}", 126 | pairedChar: "{", 127 | type: delimiterTypes.bracket, 128 | direction: delimiterTypeDirection.close, 129 | }, 130 | { 131 | name: "closeAngle", 132 | char: ">", 133 | pairedChar: "<", 134 | type: delimiterTypes.bracket, 135 | direction: delimiterTypeDirection.close, 136 | }, 137 | { 138 | name: "closeSingleQuote", 139 | char: "'", 140 | pairedChar: "'", 141 | type: delimiterTypes.quote, 142 | direction: delimiterTypeDirection.close, 143 | }, 144 | { 145 | name: "closeDoubleQuote", 146 | char: '"', 147 | pairedChar: '"', 148 | type: delimiterTypes.quote, 149 | direction: delimiterTypeDirection.close, 150 | }, 151 | { 152 | name: "closeBacktick", 153 | char: "`", 154 | pairedChar: "`", 155 | type: delimiterTypes.quote, 156 | direction: delimiterTypeDirection.close, 157 | }, 158 | ]; 159 | 160 | /** 161 | * Selection offset object, used to get the start and end offsets of the selection 162 | * 163 | * @typedef {selectionOffset} 164 | */ 165 | type selectionOffset = { 166 | start: number; 167 | end: number; 168 | }; 169 | 170 | /** 171 | * Text split at selection. 172 | * NOTE: The object returned does not include the selection text (if any). 173 | * 174 | * @typedef {textSpitAtSelection} 175 | */ 176 | type textSpitAtSelection = { 177 | textBeforeSelectionStart: string; 178 | textAfterSelectionStart: string; 179 | }; 180 | 181 | /** 182 | * List of open delimiters 183 | * 184 | * @type {*} 185 | */ 186 | const openDelimiters = delimiters.filter((delimiter) => delimiter.direction === "open"); //.map((delimiter) => delimiter.char); 187 | 188 | /** 189 | * List of close delimiters 190 | * 191 | * @type {*} 192 | */ 193 | const closeDelimiters = delimiters.filter((delimiter) => delimiter.direction === "close"); //.map((delimiter) => delimiter.char); 194 | 195 | /** 196 | * Returns the current selection offset 197 | * 198 | * @returns {(selectionOffset | undefined)} 199 | */ 200 | function getSelectionOffset(editor: TextEditor): selectionOffset | undefined { 201 | return { 202 | start: editor.document.offsetAt(editor.selection.start), 203 | end: editor.document.offsetAt(editor.selection.end), 204 | }; 205 | } 206 | 207 | /** 208 | * Returns the text before and after the selection, but not the selected text 209 | * 210 | * @returns {(textSpitAtSelection | undefined)} 211 | */ 212 | function getTextSplitAtSelection(): textSpitAtSelection | undefined { 213 | let editor = getActiveEditor(); 214 | if (!editor) { 215 | return; 216 | } 217 | 218 | let selectionOffset = getSelectionOffset(editor); 219 | if (!selectionOffset) { 220 | return; 221 | } 222 | let documentStartPosition = new Position(0, 0); 223 | let documentEndPosition = new Position(editor.document.lineCount - 1, editor.document.lineAt(editor.document.lineCount - 1).range.end.character); 224 | let textBeforeSelectionStart = getTextFromSelection(editor, new Selection(documentStartPosition, editor.document.positionAt(selectionOffset.start)))!; 225 | let textAfterSelectionStart = getTextFromSelection(editor, new Selection(editor.document.positionAt(selectionOffset.end), documentEndPosition))!; 226 | 227 | return { 228 | textBeforeSelectionStart, 229 | textAfterSelectionStart, 230 | }; 231 | } 232 | 233 | /** 234 | * Determines if the current selection already includes delimiters 235 | * 236 | * @param {string} text 237 | * @param {delimiterTypes} delimiterType 238 | * @returns {boolean} 239 | */ 240 | function selectionIncludesDelimiters(text: string, delimiterType: delimiterTypes): boolean { 241 | if (!text) { 242 | return false; 243 | } 244 | 245 | let includesOpeningDelimiter: boolean; 246 | let includesClosingDelimiter: boolean; 247 | 248 | openDelimiters.filter((delimiter) => delimiter.type === delimiterType).find((delimiter) => delimiter.char === text[0]) 249 | ? (includesOpeningDelimiter = true) 250 | : (includesOpeningDelimiter = false); 251 | closeDelimiters.filter((delimiter) => delimiter.type === delimiterType).find((delimiter) => delimiter.char === text[text.length - 1]) 252 | ? (includesClosingDelimiter = true) 253 | : (includesClosingDelimiter = false); 254 | 255 | if (includesClosingDelimiter && includesOpeningDelimiter) { 256 | return true; 257 | } 258 | 259 | return false; 260 | } 261 | 262 | /** 263 | * Find the opening bracket starting from the cursor position or active selection 264 | * 265 | * @param {string} text The text to search 266 | * @param {delimiterTypes} delimiterType The delimiter type to search for 267 | * @param {number} startOffset The offset to start searching from 268 | * @returns {(delimiter | undefined)} 269 | */ 270 | function findOpeningBracket(text: string, delimiterType: delimiterTypes, startOffset: number): delimiter | undefined { 271 | if (!text) { 272 | return undefined; 273 | } 274 | 275 | let position = text.length - 1; 276 | let closedDelimiters = { 277 | closeRound: 0, 278 | closeSquare: 0, 279 | closeCurly: 0, 280 | closeAngle: 0, 281 | }; 282 | type closeDelimitersKey = keyof typeof closedDelimiters; 283 | 284 | let openingDelimiters = delimiters.filter((delimiter) => delimiter.direction === "open" && delimiter.type === delimiterType); 285 | 286 | while (position >= 0) { 287 | let closingDelimiter = 288 | Object.values(delimiters) 289 | .filter((delimiter) => delimiter.type === delimiterType) 290 | .filter((delimiter) => delimiter.direction === delimiterTypeDirection.close) 291 | .find((delimiter) => delimiter.char === text.at(position)) ?? undefined; 292 | 293 | if (closingDelimiter) { 294 | // do not count arrow functions as delimiters 295 | if (text.at(position - 1) !== "=") { 296 | closedDelimiters[closingDelimiter!.name as closeDelimitersKey]++; 297 | } 298 | } 299 | 300 | let openingDelimiter = Object.values(openingDelimiters).find((delimiter) => delimiter.char === text.at(position)) ?? undefined; 301 | if (openingDelimiter) { 302 | // found opening delimiter, let's check if it is paired with a closing delimiter we already found 303 | let closingDelimiter = Object.values(delimiters).find((d) => d.char === openingDelimiter!.pairedChar); 304 | if (closedDelimiters[closingDelimiter?.name as closeDelimitersKey] > 0) { 305 | closedDelimiters[closingDelimiter?.name as closeDelimitersKey]--; 306 | } else { 307 | if (Object.values(closedDelimiters).every((value) => value <= 0)) { 308 | return { 309 | name: openingDelimiter.name, 310 | char: text.at(position)!, 311 | pairedChar: openingDelimiter.pairedChar, 312 | position: position, 313 | pairedOffset: undefined, // update 314 | type: openingDelimiter.type, 315 | direction: openingDelimiter.direction, 316 | offset: startOffset, 317 | } as delimiter; 318 | } 319 | } 320 | } 321 | 322 | position--; 323 | } 324 | 325 | return; 326 | } 327 | 328 | function findClosingBracket(text: string, openingDelimiter: delimiter, startOffset: number, position: number = 0): delimiter | undefined { 329 | if (!text) { 330 | return undefined; 331 | } 332 | 333 | // keep track of the number of opening delimiters found 334 | let openingDelimiterCount = 0; 335 | 336 | while (position < text.length) { 337 | if (text.at(position) === openingDelimiter.char) { 338 | openingDelimiterCount++; 339 | } 340 | 341 | if (text.at(position) === openingDelimiter.pairedChar) { 342 | if (openingDelimiterCount === 0) { 343 | return { 344 | name: delimiters.filter((delimiter) => delimiter.char === text.at(position))[0].name, 345 | char: text.at(position)!, 346 | pairedChar: openingDelimiter.char, 347 | position: startOffset + position + 1, 348 | pairedOffset: undefined, // update 349 | type: openingDelimiter.type, 350 | direction: delimiterTypeDirection.close, 351 | offset: startOffset, 352 | } as delimiter; 353 | } else { 354 | openingDelimiterCount--; 355 | } 356 | } 357 | 358 | position++; 359 | } 360 | 361 | return; 362 | } 363 | 364 | /** 365 | * Select text between delimiters, based on the cursor position or the existing selection 366 | * 367 | * @export 368 | * @param {delimiterTypes} delimiterType 369 | */ 370 | export function selectTextBetweenDelimiters(delimiterType: delimiterTypes) { 371 | const editor = getActiveEditor(); 372 | if (!editor) { 373 | return; 374 | } 375 | 376 | const activeDocument = editor.document; 377 | if (!activeDocument) { 378 | return; 379 | } 380 | 381 | let selectionOffset = getSelectionOffset(editor); 382 | if (!selectionOffset) { 383 | return; 384 | } 385 | 386 | let textSplitAtSelectionStart = getTextSplitAtSelection(); 387 | if (!textSplitAtSelectionStart) { 388 | return; 389 | } 390 | 391 | let openingDelimiter = 392 | delimiterType === delimiterTypes.bracket 393 | ? findOpeningBracket(textSplitAtSelectionStart.textBeforeSelectionStart, delimiterType, selectionOffset.start) 394 | : findOpeningQuote(textSplitAtSelectionStart.textBeforeSelectionStart, delimiterType, selectionOffset.start); 395 | if (!openingDelimiter) { 396 | return; 397 | } 398 | 399 | let closingDelimiter = 400 | delimiterType === delimiterTypes.bracket 401 | ? findClosingBracket(textSplitAtSelectionStart.textAfterSelectionStart, openingDelimiter, selectionOffset.end) 402 | : findClosingQuote(textSplitAtSelectionStart.textAfterSelectionStart, openingDelimiter, selectionOffset.end); 403 | if (!closingDelimiter) { 404 | return; 405 | } 406 | 407 | let newSelectionOffsetStart = openingDelimiter.position; 408 | let newSelectionOffsetEnd = closingDelimiter.position; 409 | 410 | let currentSelection = getTextFromSelection(editor, editor.selection); 411 | if (selectionIncludesDelimiters(currentSelection!, delimiterType) || !currentSelection) { 412 | // the current selection already includes the delimiters, so the new selection should not, unless: 413 | // - the current selection is empty 414 | // - the new selection needs to include consecutive delimiters 415 | if ((selectionOffset.start !== newSelectionOffsetStart + 1 && selectionOffset.end !== newSelectionOffsetEnd - 1) || currentSelection!.length === 0) { 416 | newSelectionOffsetStart++; 417 | newSelectionOffsetEnd--; 418 | } 419 | if (selectionOffset.start === newSelectionOffsetStart + 1 && selectionOffset.end !== newSelectionOffsetEnd - 1) { 420 | newSelectionOffsetStart++; 421 | newSelectionOffsetEnd--; 422 | } 423 | } 424 | 425 | addSelection(activeDocument.positionAt(newSelectionOffsetStart), activeDocument.positionAt(newSelectionOffsetEnd)); 426 | } 427 | 428 | /** 429 | * Remove delimiters, based on the cursor position or the existing selection 430 | * 431 | * @export 432 | * @param {delimiterTypes} delimiterType 433 | */ 434 | export function removeDelimiters(delimiterType: delimiterTypes) { 435 | const editor = getActiveEditor(); 436 | if (!editor) { 437 | return; 438 | } 439 | 440 | const activeDocument = editor.document; 441 | if (!activeDocument) { 442 | return; 443 | } 444 | 445 | let [newSelectionOffsetStart, newSelectionOffsetEnd] = getDelimitersOffset(delimiterType); 446 | if (!newSelectionOffsetStart || !newSelectionOffsetEnd) { 447 | return; 448 | } 449 | let correctOffset = 1; 450 | 451 | editor.edit((editBuilder) => { 452 | // remove end delimiter 453 | editBuilder.replace( 454 | new Selection(activeDocument.positionAt(newSelectionOffsetEnd!), activeDocument.positionAt(newSelectionOffsetEnd! - correctOffset)), 455 | "" 456 | ); 457 | 458 | // remove start delimiter 459 | editBuilder.replace( 460 | new Selection(activeDocument.positionAt(newSelectionOffsetStart!), activeDocument.positionAt(newSelectionOffsetStart! + correctOffset)), 461 | "" 462 | ); 463 | }); 464 | } 465 | 466 | /** 467 | * Find the offset of the delimiter 468 | * 469 | * @param {delimiterTypes} delimiterType The type of delimiter to find 470 | * @returns {([number | undefined, number | undefined])} 471 | */ 472 | function getDelimitersOffset(delimiterType: delimiterTypes): [number | undefined, number | undefined] { 473 | const editor = getActiveEditor(); 474 | if (!editor) { 475 | return [undefined, undefined]; 476 | } 477 | 478 | const activeDocument = editor.document; 479 | if (!activeDocument) { 480 | return [undefined, undefined]; 481 | } 482 | 483 | let selectionOffset = getSelectionOffset(editor); 484 | if (!selectionOffset) { 485 | return [undefined, undefined]; 486 | } 487 | 488 | let textSplitAtSelectionStart = getTextSplitAtSelection(); 489 | if (!textSplitAtSelectionStart) { 490 | return [undefined, undefined]; 491 | } 492 | 493 | let openingDelimiter = 494 | delimiterType === delimiterTypes.bracket 495 | ? findOpeningBracket(textSplitAtSelectionStart.textBeforeSelectionStart, delimiterType, selectionOffset.start) 496 | : findOpeningQuote(textSplitAtSelectionStart.textBeforeSelectionStart, delimiterType, selectionOffset.start); 497 | if (!openingDelimiter) { 498 | return [undefined, undefined]; 499 | } 500 | 501 | let closingDelimiter = 502 | delimiterType === delimiterTypes.bracket 503 | ? findClosingBracket(textSplitAtSelectionStart.textAfterSelectionStart, openingDelimiter, selectionOffset.end) 504 | : findClosingQuote(textSplitAtSelectionStart.textAfterSelectionStart, openingDelimiter, selectionOffset.end); 505 | 506 | return [openingDelimiter?.position, closingDelimiter?.position]; 507 | } 508 | 509 | /** 510 | * Find the opening quote starting from the cursor position or active selection 511 | * 512 | * @param {string} text 513 | * @param {delimiterTypes} delimiterType 514 | * @param {number} startOffset 515 | * @returns {(delimiter | undefined)} 516 | */ 517 | function findOpeningQuote(text: string, delimiterType: delimiterTypes, startOffset: number): delimiter | undefined { 518 | if (!text) { 519 | return undefined; 520 | } 521 | 522 | let position = text.length - 1; 523 | 524 | let openingDelimiters = delimiters.filter((delimiter) => delimiter.direction === "open" && delimiter.type === delimiterType); 525 | 526 | while (position >= 0) { 527 | let openingDelimiter = Object.values(openingDelimiters).find((delimiter) => delimiter.char === text.at(position)) ?? undefined; 528 | 529 | if (openingDelimiter) { 530 | return { 531 | name: openingDelimiter.name, 532 | char: text.at(position)!, 533 | pairedChar: openingDelimiter.pairedChar, 534 | position: position, 535 | pairedOffset: undefined, // update 536 | type: openingDelimiter.type, 537 | direction: openingDelimiter.direction, 538 | offset: startOffset, 539 | } as delimiter; 540 | } 541 | 542 | position--; 543 | } 544 | 545 | return; 546 | } 547 | 548 | /** 549 | * Find the closing quote starting from the cursor position or active selection 550 | * 551 | * @param {string} text The text to search in 552 | * @param {delimiter} openingDelimiter The opening delimiter 553 | * @param {number} startOffset The offset to start the search from 554 | * @param {number} [position=0] The position to start the search from 555 | * @returns {(delimiter | undefined)} 556 | */ 557 | function findClosingQuote(text: string, openingDelimiter: delimiter, startOffset: number, position: number = 0): delimiter | undefined { 558 | if (!text) { 559 | return undefined; 560 | } 561 | 562 | while (position < text.length) { 563 | if (text.at(position) === openingDelimiter.pairedChar) { 564 | return { 565 | name: delimiters.filter((delimiter) => delimiter.char === text.at(position))[0].name, 566 | char: text.at(position)!, 567 | pairedChar: openingDelimiter.char, 568 | position: startOffset + position + 1, 569 | pairedOffset: undefined, // update 570 | type: openingDelimiter.type, 571 | direction: delimiterTypeDirection.close, 572 | offset: startOffset, 573 | } as delimiter; 574 | } 575 | 576 | position++; 577 | } 578 | 579 | return; 580 | } 581 | 582 | /** 583 | * Cycle between delimiter types 584 | * 585 | * @export 586 | * @param {delimiterTypes} delimiterType 587 | */ 588 | export function cycleDelimiters(delimiterType: delimiterTypes) { 589 | const editor = getActiveEditor(); 590 | if (!editor) { 591 | return; 592 | } 593 | 594 | const activeDocument = editor.document; 595 | if (!activeDocument) { 596 | return; 597 | } 598 | 599 | let [newSelectionOffsetStart, newSelectionOffsetEnd] = getDelimitersOffset(delimiterType); 600 | if (!newSelectionOffsetStart || !newSelectionOffsetEnd) { 601 | return; 602 | } 603 | const delimiters = getDelimiters(delimiterType, delimiterTypeDirection.open); 604 | const currentDelimiter = delimiters.find((delimiter) => delimiter.char === activeDocument.getText()[newSelectionOffsetStart!]); 605 | const currentDelimiterIndex = delimiters.indexOf(currentDelimiter!); 606 | const nextDelimiter = delimiters[(currentDelimiterIndex + 1) % delimiters.length]; 607 | 608 | const correctOffset = 1; 609 | 610 | editor.edit((editBuilder) => { 611 | // replace start delimiter 612 | editBuilder.replace( 613 | new Selection(activeDocument.positionAt(newSelectionOffsetStart!), activeDocument.positionAt(newSelectionOffsetStart! + correctOffset)), 614 | nextDelimiter.char 615 | ); 616 | 617 | // replace end delimiter 618 | editBuilder.replace( 619 | new Selection(activeDocument.positionAt(newSelectionOffsetEnd!), activeDocument.positionAt(newSelectionOffsetEnd! - correctOffset)), 620 | nextDelimiter.pairedChar 621 | ); 622 | }); 623 | } 624 | 625 | /** 626 | * Returns the delimiters for the given type and direction 627 | * 628 | * @param {delimiterTypes} delimiterType The type of delimiter to return 629 | * @param {delimiterTypeDirection} delimiterDirection The direction of the delimiter to return 630 | * @returns {delimiter[]} 631 | */ 632 | function getDelimiters(delimiterType: delimiterTypes, delimiterDirection: delimiterTypeDirection): delimiter[] { 633 | return Object.values(delimiters).filter((delimiter) => delimiter.type === delimiterType && delimiter.direction === delimiterDirection) as delimiter[]; 634 | } 635 | -------------------------------------------------------------------------------- /src/modules/filterText.ts: -------------------------------------------------------------------------------- 1 | import { getActiveEditor, getDocumentTextOrSelection, createNewEditor, getSelection, linesToLine, getLinesFromString, getTextFromSelection, getDocumentEOL } from "./helpers"; 2 | import * as os from "os"; 3 | import { window, workspace, TextEditor, Selection, Uri } from "vscode"; 4 | 5 | export const REGEX_TEXT_BETWEEN_SPACES = /([^\s"'`{}()[\]])+/; 6 | export const REGEX_VALIDATE_EMAIL = /[\w-]+@[\w-]+\.\w+/; 7 | 8 | /** 9 | * Removes empty lines from the active document or selection; optionally removes only duplicate empty lines. 10 | * This function is called by the TextEditorCommands `RemoveAllEmptyLines` and `RemoveRedundantEmptyLines` and is a wrapper for `removeEmptyLinesInternal` 11 | * @param {boolean} redundantOnly Remove only duplicate empty lines 12 | * @async 13 | */ 14 | export async function removeEmptyLines(redundantOnly: boolean) { 15 | let text = getDocumentTextOrSelection(); 16 | if (!text) { 17 | return; 18 | } 19 | 20 | const newText = await removeEmptyLinesInternal(text, redundantOnly); 21 | 22 | let editor = getActiveEditor(); 23 | if (!editor) { 24 | return; 25 | } 26 | 27 | let selection = getSelection(editor); 28 | if (!selection) { 29 | return; 30 | } 31 | editor.edit((editBuilder) => { 32 | editBuilder.replace(selection, newText); 33 | }); 34 | } 35 | 36 | /** 37 | * Removes empty lines from the active document or selection; optionally removes only duplicate empty lines. 38 | * This function is called by `removeEmptyLines` by Mocha tests 39 | * @param {string} text The text to remove empty lines from 40 | * @param {boolean} redundantOnly Removes only duplicate empty lines, or all 41 | * @async 42 | * @returns {Promise} 43 | */ 44 | export async function removeEmptyLinesInternal(text: string, redundantOnly: boolean): Promise { 45 | const eol = getDocumentEOL(getActiveEditor()); 46 | let r; 47 | let rr: string; 48 | // /^\n{2,}/gm ==> two or more empty lines 49 | // /^\n+/gm ==> any empty line 50 | redundantOnly ? (r = /^\s*(\n{2,}|^\s*(\r\n){2,})/gm) : (r = /^\s*(\n+|\r\n+)/gm); 51 | // replace multiple empty lines with a single one, or with nothing 52 | redundantOnly ? (rr = eol) : (rr = ""); 53 | 54 | return Promise.resolve(text.replace(r, rr!)); 55 | } 56 | 57 | /** 58 | * Removes duplicate lines from the active selection or document and optionally open the result in a new editor 59 | * @param {boolean} openInNewTextEditor Open the resulting text in a new editor 60 | * @async 61 | */ 62 | export async function removeDuplicateLines(openInNewTextEditor: boolean) { 63 | let text = getDocumentTextOrSelection(); 64 | const eol = getDocumentEOL(getActiveEditor()); 65 | 66 | const o = os; 67 | let lines = text?.split(eol); 68 | if (!lines) { 69 | return; 70 | } 71 | 72 | const ignoreWhitespaces = workspace.getConfiguration().get("TextToolbox.ignoreWhitespaceInLineFilters"); 73 | if (ignoreWhitespaces) { 74 | for (let i = 0; i < lines.length - 1; i++) { 75 | lines[i] = lines[i].trim(); 76 | } 77 | } 78 | 79 | for (const line of lines) { 80 | while (lines.indexOf(line) !== lines.lastIndexOf(line)) { 81 | lines.splice(lines.lastIndexOf(line), 1); 82 | } 83 | } 84 | let newText = (await linesToLine(lines)).trim(); 85 | newText = await removeEmptyLinesInternal(newText, false); 86 | 87 | if (openInNewTextEditor) { 88 | createNewEditor(newText); 89 | return; 90 | } 91 | 92 | const editor = getActiveEditor(); 93 | let selection = getSelection(editor!); 94 | 95 | editor!.edit((editBuilder) => { 96 | editBuilder.replace(selection!, newText); 97 | }); 98 | } 99 | 100 | /** 101 | * Filter the active Selection or Document based on user's input, either regexp (default) or simple string. 102 | * - `regexp` behaves like a normal regular expression, returns the result of the RegExp match. 103 | * - `string` returns all lines containing the search string, exactly how is typed. 104 | * The default search string type (regexp or string) can be configured using `TextToolbox.filtersUseRegularExpressions` 105 | * Default: regexp 106 | * @param {boolean} openInNewTextEditor 107 | * @async 108 | */ 109 | export async function filterLinesUsingRegExpOrString(openInNewTextEditor?: boolean) { 110 | let searchString = await window.showInputBox({ ignoreFocusOut: true, placeHolder: "Regular Expression or String to match" }); 111 | if (!searchString) { 112 | return; 113 | } 114 | 115 | let text; 116 | if (workspace.getConfiguration().get("TextToolbox.filtersUseRegularExpressions")) { 117 | text = findLinesMatchingRegEx(searchString)!; 118 | } else { 119 | text = await findLinesMatchingString(searchString); 120 | } 121 | 122 | text!.length > 0 ? createNewEditor(await linesToLine(text!)) : window.showInformationMessage("No match found"); 123 | } 124 | 125 | /** 126 | * Searches the current Selection and returns all RegExp matches 127 | * @param {string | undefined} searchString 128 | * @returns {string[] | undefined} 129 | */ 130 | export function findLinesMatchingRegEx(searchString: string | undefined): string[] | undefined { 131 | if (!searchString) { 132 | return; 133 | } 134 | 135 | let text: string | any = []; 136 | 137 | const regExpFlags = searchString.match("(?!.*/).*")![0] || undefined; 138 | const regExpString = searchString.match("(?<=/)(.*?)(?=/)")![0]; 139 | const regExp = new RegExp(regExpString, regExpFlags); 140 | 141 | let match; 142 | if (!regExpFlags || regExpFlags?.indexOf("g") < 0) { 143 | match = regExp.exec(getDocumentTextOrSelection()!); 144 | if (match) { 145 | text.push(match[0]); 146 | } 147 | } else if (regExpFlags || regExpFlags.indexOf("g") >= 0) { 148 | while ((match = regExp.exec(getDocumentTextOrSelection()!))) { 149 | text.push(match[0]); 150 | } 151 | } 152 | 153 | return text; 154 | } 155 | 156 | /** 157 | * Searches the current Selection and returns all lines containing [searchString] 158 | * @param {string} searchString The string to search for and match 159 | * @returns {Promise} 160 | * @async 161 | */ 162 | export async function findLinesMatchingString(searchString: string): Promise { 163 | if (!searchString) { 164 | return; 165 | } 166 | let text: string[] | undefined = []; 167 | 168 | text = await getLinesFromString(getDocumentTextOrSelection()!); 169 | if (!text) { 170 | return; 171 | } 172 | text = text.filter((line) => line.indexOf(searchString) >= 0); 173 | 174 | return Promise.resolve(text); 175 | } 176 | 177 | /** 178 | * Opens the current selection(s) in a new Editor 179 | * @return {Promise} 180 | * @async 181 | */ 182 | export async function openSelectionInNewEditor(): Promise { 183 | const editor = getActiveEditor(); 184 | if (!editor) { 185 | return Promise.reject("No active editor found"); 186 | } 187 | 188 | if (editor.selection.isEmpty) { 189 | return false; 190 | } 191 | 192 | let selections = editor.selections; 193 | let text: string[] = []; 194 | const eol = getDocumentEOL(getActiveEditor()); 195 | 196 | selections.forEach((s) => { 197 | text.push(getTextFromSelection(editor, s)! + eol); 198 | }); 199 | 200 | await createNewEditor(text!.join(eol)); 201 | return Promise.resolve(true); 202 | } 203 | 204 | /** 205 | * Returns the string between two spaces, starting at the current cursor position 206 | * 207 | * @export 208 | * @param {TextEditor} editor The current editor 209 | * @param {?string} [text] The text to search in 210 | * @returns {(string | undefined)} 211 | */ 212 | export function getTextBetweenSpaces(editor: TextEditor, text?: string): string | undefined { 213 | const document = editor.document; 214 | if (!document) { 215 | return; 216 | } 217 | 218 | // prettier-ignore 219 | let range = document.getWordRangeAtPosition( 220 | editor.selection.active, 221 | REGEX_TEXT_BETWEEN_SPACES 222 | ); 223 | if (!range) { 224 | return; 225 | } 226 | 227 | text = getTextFromSelection(editor, new Selection(range!.start, range!.end)); 228 | if (text) { 229 | return text; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/modules/helpers.ts: -------------------------------------------------------------------------------- 1 | import { commands, Range, Selection, TextEditor, window, workspace, TextLine, DocumentHighlight, Position, EndOfLine, TextDocument } from "vscode"; 2 | import * as os from "os"; 3 | 4 | /** 5 | * Returns the active text editor 6 | * @returns {TextEditor | undefined} 7 | */ 8 | export function getActiveEditor(): TextEditor | undefined { 9 | return window.activeTextEditor; 10 | } 11 | 12 | /** 13 | * Returns the active document 14 | * 15 | * @export 16 | * @returns {(TextDocument | undefined)} 17 | */ 18 | export function getActiveDocument(): TextDocument | undefined { 19 | return getActiveEditor()?.document; 20 | } 21 | 22 | /** 23 | * Validates that there is an active text editor 24 | */ 25 | function validateEditor() { 26 | const editor = getActiveEditor(); 27 | if (!editor) { 28 | return; 29 | } 30 | } 31 | 32 | /** 33 | * Validate that there is an active selection 34 | * @returns {boolean} 35 | */ 36 | export function validateSelection(): boolean { 37 | validateEditor(); 38 | 39 | const selections = window.activeTextEditor?.selections; 40 | if (!selections) { 41 | return false; 42 | } 43 | 44 | if (selections?.length < 1) { 45 | window.showWarningMessage("You must select some text first"); 46 | return false; 47 | } 48 | 49 | return true; 50 | } 51 | 52 | /** 53 | * Pauses execution of the given number of milliseconds 54 | * @param milliseconds the number of milliseconds to wait 55 | */ 56 | export function sleep(milliseconds: number): Promise { 57 | return new Promise((resolve) => { 58 | setTimeout(resolve, milliseconds); 59 | }); 60 | } 61 | 62 | /** 63 | * Selects all text in the active text editor 64 | */ 65 | export function selectAllText(): Thenable { 66 | return commands.executeCommand("editor.action.selectAll"); 67 | } 68 | 69 | /** 70 | * Returns text from the Selection, or the entire document if there is no selection 71 | * Does not support multiple selections 72 | * 73 | * @returns {string | undefined} 74 | */ 75 | export function getDocumentTextOrSelection(fullLineOnly?: boolean): string | undefined { 76 | const editor = getActiveEditor()!; 77 | const selection = editor!.selection; 78 | 79 | if (selection.isEmpty) { 80 | return editor.document.getText(); 81 | } else { 82 | if (fullLineOnly) { 83 | } else { 84 | return getTextFromSelection(editor, selection); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Returns the current selection or the entire document as new selection, but it does not actually select the text in the editor 91 | * @param editor The editor containing the selection to return or create 92 | * @returns {Selection} 93 | */ 94 | export function getSelection(editor: TextEditor): Selection { 95 | if (editor.selection.isEmpty) { 96 | let selection: Selection; 97 | 98 | const lineCount = editor.document.lineCount; 99 | selection = new Selection(0, 0, lineCount, editor.document.lineAt(lineCount - 1).text.length); 100 | return selection; 101 | } 102 | 103 | return editor.selection; 104 | } 105 | 106 | /** 107 | * Returns text from the passed in Selection 108 | * @param editor The Editor with the selection 109 | * @param selection The Selection object to convert into text 110 | * @type {string | undefined} 111 | */ 112 | export function getTextFromSelection(editor: TextEditor, selection: Selection): string | undefined { 113 | return editor.document.getText(new Range(selection.start, selection.end)); 114 | } 115 | 116 | /** 117 | * Returns an object with line information for each line in the selection 118 | * @param editor The Editor with the selection 119 | * @param selection The Selection object to split into lines 120 | * @return {(TextLine[] | undefined)} 121 | */ 122 | export function getLinesFromSelection(editor: TextEditor, selection?: Selection): TextLine[] | undefined { 123 | let lines: TextLine[] = []; 124 | let selections: Selection[] = []; 125 | 126 | if (selection) { 127 | selections.push(selection); 128 | } else { 129 | // The type 'readonly Selection[]' is 'readonly' and cannot be assigned to the mutable type 'Selection[]'. 130 | // https://stackoverflow.com/a/53416703 131 | // selection = editor?.selections; 132 | selections = editor.selections.map((s) => s); 133 | if (!selections) { 134 | return; 135 | } 136 | } 137 | 138 | selections.forEach((s) => { 139 | let selectionStartLine = s.start.line; 140 | let selectionEndLine = s.end.line; 141 | 142 | for (let i = selectionStartLine; i <= selectionEndLine; i++) { 143 | if (i === selectionStartLine) { 144 | if (s.start.character < editor.document.lineAt(selectionStartLine).text.length) { 145 | lines.push(editor?.document.lineAt(i)); 146 | } 147 | } 148 | else if (i > selectionStartLine && i < selectionEndLine) { 149 | lines.push(editor?.document.lineAt(i)); 150 | } 151 | else if (i === selectionEndLine) { 152 | if (s.end.character > 0) { 153 | lines.push(editor?.document.lineAt(i)); 154 | } 155 | } 156 | } 157 | }); 158 | 159 | return lines!; 160 | } 161 | 162 | /** 163 | * Returns an array of lines from the document or selection 164 | * 165 | * @export 166 | * @param {TextEditor} editor The editor containing the selection 167 | * @return {*} {(TextLine[] | undefined)} 168 | */ 169 | export function getLinesFromDocumentOrSelection(editor: TextEditor): TextLine[] | undefined; 170 | /** 171 | * Returns an array of lines from the document or selection 172 | * 173 | * @export 174 | * @param {TextEditor} editor The editor containing the selection 175 | * @param {Range} range The range to split into lines 176 | * @return {*} {(TextLine[] | undefined)} 177 | */ 178 | export function getLinesFromDocumentOrSelection(editor: TextEditor, range: Range): TextLine[] | undefined; 179 | /** 180 | * Returns an array of lines from the document or selection 181 | * 182 | * @export 183 | * @param {TextEditor} editor The editor containing the selection 184 | * @param {Selection} selection The selection to split into lines 185 | * @return {*} {(TextLine[] | undefined)} 186 | */ 187 | export function getLinesFromDocumentOrSelection(editor: TextEditor, selection: Selection): TextLine[] | undefined; 188 | /** 189 | * Returns an array of lines from the document or selection 190 | * 191 | * @export 192 | * @param {TextEditor} editor The editor containing the selection 193 | * @param {Range} [range] The range to get lines from 194 | * @param {Selection} [selection] The selection to get lines from 195 | * @return {*} {(TextLine[] | undefined)} 196 | */ 197 | export function getLinesFromDocumentOrSelection(editor: TextEditor, range?: Range, selection?: Selection): TextLine[] | undefined { 198 | const lineCount = editor.document.lineCount; 199 | if (lineCount < 1) { 200 | return; 201 | } 202 | 203 | let textLines: TextLine[] = []; 204 | 205 | if (selection) { 206 | return getLinesFromSelection(editor, selection); 207 | } else if (range) { 208 | return getLinesFromSelection(editor, new Selection(range.start, range.end)); 209 | } else { 210 | for (let i = 0; i < lineCount; i++) { 211 | textLines.push(editor.document.lineAt(i)); 212 | } 213 | } 214 | 215 | return textLines; 216 | } 217 | 218 | /** 219 | * Creates a new TextEditor containing the passed in text 220 | * @param {string} text 221 | * @returns {TextEditor} 222 | */ 223 | export function createNewEditor(text?: string): Promise { 224 | return new Promise(async (resolve, reject) => { 225 | await workspace.openTextDocument({ content: text, language: "plaintext", preview: false } as any).then( 226 | (doc) => { 227 | resolve(window.showTextDocument(doc)); 228 | }, 229 | (err) => reject(err) 230 | ); 231 | }); 232 | } 233 | 234 | /** 235 | * Close the active editor or all active editors in the current window 236 | * @param {boolean} closeAll Optional: if `true`, closes all editors in the current window; if `false` or missing closes the active editor only 237 | * @returns {Promise} 238 | */ 239 | export function closeTextEditor(closeAll?: boolean): Promise { 240 | if (closeAll) { 241 | commands.executeCommand("workbench.action.closeAllEditors"); 242 | } else { 243 | commands.executeCommand("workbench.action.closeActiveEditor"); 244 | } 245 | 246 | return Promise.resolve(); 247 | } 248 | 249 | /** 250 | * Join an array of lines using the document EOL and returns the resulting string 251 | * @param {string[]} lines The array of lines (text) to convert into a single line 252 | * @returns {Promise} 253 | * @async 254 | */ 255 | export async function linesToLine(lines: string[]): Promise { 256 | const eol = getDocumentEOL(getActiveEditor()); 257 | 258 | return Promise.resolve(lines.join(eol)); 259 | } 260 | 261 | /** 262 | * Split a string based on the document EOL and returns the resulting array of strings (lines) 263 | * @param {string} line The line to convert into an array of strings 264 | * @returns {*} {Promise} 265 | * @async 266 | */ 267 | export async function getLinesFromString(line: string): Promise { 268 | const eol = getDocumentEOL(getActiveEditor()); 269 | 270 | return Promise.resolve(line.split(eol)); 271 | } 272 | 273 | /** 274 | * Returns an array of selections if any is available in the active document, otherwise returns the entire document as a single selection 275 | * @param {TextEditor} editor The active text editor to get the selections from 276 | * @return {*} {Promise} 277 | * @async 278 | */ 279 | export async function getSelections(editor: TextEditor): Promise { 280 | if (editor.selections.length >= 1 && !editor.selection.isEmpty) { 281 | return Promise.resolve(editor.selections); 282 | } else { 283 | const lastLine = editor.document.lineAt(editor.document.lineCount - 1); 284 | let selection = new Selection(0, 0, lastLine.lineNumber, lastLine.text.length); 285 | let selections: Selection[] = []; 286 | selections.push(selection); 287 | return Promise.resolve(selections); 288 | } 289 | } 290 | 291 | /** 292 | * Returns a RegExp object based on a RegExp string and flags 293 | * 294 | * @export 295 | * @param {string} regex The RegExp string to convert into a RegExp object 296 | * @return {*} {RegExp} 297 | */ 298 | export function getRegExpObject(regex: string): RegExp { 299 | const regExpFlags = regex.match("(?!.*/).*")![0] || undefined; 300 | const regExpString = regex.match("(?<=/)(.*?)(?=/)")![0]; 301 | const regExpObject = new RegExp(regExpString, regExpFlags); 302 | 303 | return new RegExp(regExpObject); 304 | } 305 | 306 | /** 307 | * Adds a new selection to the active editor; replaces any existing selections 308 | * 309 | * @export 310 | * @param {Position} positionStart The start position of the selection 311 | * @param {Position} positionEnd The end position of the selection 312 | * @return {*} 313 | */ 314 | export function addSelection(positionStart: Position, positionEnd: Position) { 315 | let editor = getActiveEditor(); 316 | if (!editor) { 317 | return; 318 | } 319 | 320 | let document = editor.document; 321 | if (!document) { 322 | return; 323 | } 324 | 325 | let newSelection = new Selection(positionStart, positionEnd); 326 | editor.selections = [newSelection]; 327 | } 328 | 329 | /** 330 | * Returns the Position of the cursor in the editor. Supports multicursor 331 | * @export 332 | * @param {TextEditor} editor The editor to get the cursor position from 333 | * @return {*} {Position[]} 334 | */ 335 | export function getCursorPosition(editor: TextEditor): Position[] { 336 | let position: Position[] = []; 337 | editor.selections.forEach((selection) => { 338 | position.push(selection.active); 339 | }); 340 | 341 | return position; 342 | } 343 | 344 | /** 345 | * Returns the end of line sequence that is predominately used in this document. 346 | * If the document contains mixed line endings, it returns the OS default. 347 | * 348 | * @export 349 | * @param {?TextEditor} [editor] The editor to get the EOL from 350 | * @returns {string} 351 | */ 352 | export function getDocumentEOL(editor?: TextEditor): string { 353 | if (!editor) { 354 | editor = getActiveEditor(); 355 | } 356 | if (!editor) { 357 | return os.EOL; 358 | } 359 | 360 | if (editor.document.eol === EndOfLine.CRLF) { 361 | return "\r\n"; 362 | } else if (editor.document.eol === EndOfLine.LF) { 363 | return "\n"; 364 | } 365 | 366 | return os.EOL; 367 | } 368 | 369 | export function isNumber(str: string): boolean { 370 | // credit: https://bobbyhadz.com/blog/typescript-check-if-string-is-valid-number 371 | if (typeof str !== "string") { 372 | return false; 373 | } 374 | 375 | if (str.trim() === "") { 376 | return false; 377 | } 378 | 379 | return !Number.isNaN(Number(str)); 380 | } 381 | 382 | export function incrementString(value: string) { 383 | // credit: https://stackoverflow.com/a/55952917/9335336 384 | let carry = 1; 385 | let res = ""; 386 | 387 | for (let i = value.length - 1; i >= 0; i--) { 388 | let char = value.charCodeAt(i); 389 | 390 | char += carry; 391 | 392 | if (char > 90 && char < 97) { 393 | char = 65; 394 | carry = 1; 395 | } else if (char > 122) { 396 | char = 97; 397 | carry = 1; 398 | } else { 399 | carry = 0; 400 | } 401 | 402 | res = String.fromCharCode(char) + res; 403 | 404 | if (!carry) { 405 | res = value.substring(0, i) + res; 406 | break; 407 | } 408 | } 409 | 410 | if (carry) { 411 | res = value[0] + res; 412 | } 413 | 414 | return res; 415 | } 416 | 417 | export enum caseOptions { 418 | upper = "upper", 419 | lower = "lower", 420 | } 421 | 422 | export function toRoman(num: number, caseOption?: caseOptions ): string { 423 | let roman = ""; 424 | const decimalToRoman: { [key: number]: string } = { 425 | 1: "I", 426 | 4: "IV", 427 | 5: "V", 428 | 9: "IX", 429 | 10: "X", 430 | 40: "XL", 431 | 50: "L", 432 | 90: "XC", 433 | 100: "C", 434 | 400: "CD", 435 | 500: "D", 436 | 900: "CM", 437 | 1000: "M", 438 | }; 439 | let keys = Object.keys(decimalToRoman) 440 | .map((key) => parseInt(key)) 441 | .sort((a, b) => b - a); 442 | for (let i = 0; i < keys.length; i++) { 443 | while (num >= keys[i]) { 444 | roman += decimalToRoman[keys[i]]; 445 | num -= keys[i]; 446 | } 447 | } 448 | if (caseOption === caseOptions.upper) { 449 | return roman.toUpperCase(); 450 | } else if (caseOption === caseOptions.lower) { 451 | return roman.toLowerCase(); 452 | } else { 453 | return roman; 454 | } 455 | } 456 | 457 | export function fromRoman(roman: string): number { 458 | const romanToDecimal: { [key: string]: number } = { 459 | I: 1, 460 | V: 5, 461 | X: 10, 462 | L: 50, 463 | C: 100, 464 | D: 500, 465 | M: 1000, 466 | i: 1, 467 | v: 5, 468 | x: 10, 469 | l: 50, 470 | c: 100, 471 | d: 500, 472 | m: 1000, 473 | }; 474 | let decimal = 0; 475 | let previous = 0; 476 | for (let i = roman.length - 1; i >= 0; i--) { 477 | const current = romanToDecimal[roman[i]]; 478 | if (current < previous) { 479 | decimal -= current; 480 | } else { 481 | decimal += current; 482 | } 483 | previous = current; 484 | } 485 | return decimal; 486 | } 487 | -------------------------------------------------------------------------------- /src/modules/indentation.ts: -------------------------------------------------------------------------------- 1 | import { commands, Selection, TextEditor } from "vscode"; 2 | 3 | export enum IndentationType { 4 | Spaces = "spaces", 5 | Tabs = "tabs", 6 | } 7 | 8 | export function updateIndentation(editor: TextEditor, indentationType: IndentationType, indentationSize: number) { 9 | if (indentationSize === editor.options.tabSize) { 10 | return; 11 | } 12 | 13 | editor 14 | .edit((editBuilder) => { 15 | for (let lineNumber = 0; lineNumber < editor.document.lineCount; lineNumber++) { 16 | const line = editor.document.lineAt(lineNumber); 17 | 18 | if (indentationType === IndentationType.Spaces) { 19 | editBuilder.replace(line.range, indentSpaces(line.text, indentationSize)); 20 | } 21 | } 22 | }) 23 | .then(() => { 24 | // remove the selection added by the replace call: https://github.com/microsoft/vscode/issues/124154 25 | editor.selection = new Selection(editor.selection.active, editor.selection.active); 26 | }); 27 | 28 | editor.options.tabSize = indentationSize; 29 | editor.options.insertSpaces = indentationType === IndentationType.Spaces ? true : false; 30 | } 31 | 32 | function indentSpaces(line: string, indentSize: number): string { 33 | const leadingSpaces = new RegExp(/^\s*/).exec(line)?.at(0); 34 | const replaceRegExp = indentSize === 2 ? /[ ]{4}|\t/g : /[ ]{2}|\t/g; 35 | if (leadingSpaces) { 36 | const newLeadingSpaces = leadingSpaces.replace(replaceRegExp, "".padEnd(indentSize, " ")); 37 | 38 | return newLeadingSpaces + line.replace(/^\s+/, ""); 39 | } 40 | 41 | return line; 42 | } 43 | 44 | export function setEditorOptionsContext(editor: TextEditor) { 45 | commands.executeCommand("setContext", "tt.tabSize", editor!.options.tabSize); 46 | commands.executeCommand("setContext", "tt.insertSpaces", editor!.options.insertSpaces); 47 | // @update: https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.indentSize.d.ts 48 | // commands.executeCommand("setContext", "tt.indentSize", editor!.options.indentSize); 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/json.ts: -------------------------------------------------------------------------------- 1 | import { getActiveEditor, getTextFromSelection } from "./helpers"; 2 | import * as jsonic from "jsonic"; 3 | import { Selection } from "vscode"; 4 | 5 | /** 6 | * Transforms a text string into proper JSON format, optionally can fix syntax errors. 7 | * @param {boolean} fixJson Fix syntax errors 8 | * @return {*} {(Promise)} 9 | */ 10 | export async function stringifyJson(fixJson: boolean): Promise { 11 | const editor = getActiveEditor(); 12 | if (!editor) { 13 | return Promise.reject(); 14 | } 15 | 16 | editor.edit((editBuilder) => { 17 | let selections: Selection[] = []; 18 | 19 | if (editor.selection.isEmpty) { 20 | selections.push(new Selection(0, 0, editor.document.lineCount, editor.document.lineAt(editor.document.lineCount - 1).text.length)); 21 | } else { 22 | selections = editor.selections.map((s) => s); 23 | } 24 | 25 | selections.forEach((s) => { 26 | let newJson: string = ""; 27 | let selectionText = getTextFromSelection(editor, s); 28 | if (fixJson) { 29 | newJson = JSON.stringify(jsonic(selectionText!), null, 4); 30 | } else { 31 | newJson = JSON.stringify(selectionText, null, 4); 32 | } 33 | 34 | editBuilder.replace(s, newJson); 35 | }); 36 | }); 37 | 38 | return Promise.resolve(); 39 | } 40 | 41 | /** 42 | * Minifies a JSON string or object 43 | * @return {*} {(Promise)} 44 | */ 45 | export async function minifyJson(): Promise { 46 | const editor = getActiveEditor(); 47 | if (!editor) { 48 | return Promise.reject(); 49 | } 50 | 51 | editor.edit((editBuilder) => { 52 | let selections: Selection[] = []; 53 | 54 | if (editor.selection.isEmpty) { 55 | selections.push(new Selection(0, 0, editor.document.lineCount, editor.document.lineAt(editor.document.lineCount - 1).text.length)); 56 | } else { 57 | selections = editor.selections.map((s) => s); 58 | } 59 | 60 | selections.forEach((s) => { 61 | let selectionText = getTextFromSelection(editor, s); 62 | let newJson = JSON.stringify(jsonic(selectionText!)); 63 | 64 | editBuilder.replace(s, newJson); 65 | }); 66 | }); 67 | 68 | return Promise.resolve(); 69 | } 70 | 71 | export function escapeWin32PathInJson() { 72 | const editor = getActiveEditor(); 73 | if (!editor) { 74 | return; 75 | } 76 | 77 | if (!editor.selection.isEmpty) { 78 | editor.edit((editBuilder) => { 79 | editor.selections.forEach((selection) => { 80 | const text = getTextFromSelection(editor, selection); 81 | const newText = text!.replace(/\\/g, "\\\\"); 82 | editBuilder.replace(selection, newText); 83 | }); 84 | }); 85 | } 86 | 87 | return; 88 | } 89 | -------------------------------------------------------------------------------- /src/modules/sortText.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createNewEditor, 3 | getDocumentTextOrSelection, 4 | getSelection, 5 | getActiveEditor, 6 | getDocumentEOL, 7 | getLinesFromSelection, 8 | } from "./helpers"; 9 | import { Selection, TextLine, window } from "vscode"; 10 | 11 | /** 12 | * Sorting direction: ascending | descending | reverse 13 | */ 14 | export enum sortDirection { 15 | ascending = "ascending", 16 | descending = "descending", 17 | reverse = "reverse", 18 | } 19 | 20 | /** 21 | * Ask the user the sort direction: ascending (default), descending and reverse 22 | * @param openInNewTextEditor Optionally open the sorted lines in a new editor 23 | * @async 24 | */ 25 | export async function askForSortDirection(openInNewTextEditor?: boolean): Promise { 26 | const direction = await window.showQuickPick(Object.values(sortDirection), { 27 | ignoreFocusOut: true, 28 | canPickMany: false, 29 | }); 30 | if (!direction) { 31 | return Promise.reject(); 32 | } 33 | 34 | return Promise.resolve(sortDirection[direction as keyof typeof sortDirection]); 35 | } 36 | 37 | /** 38 | * Sort lines in the active selection or active editor. 39 | * Optionally open the sorted lines in a new editor. 40 | * @param {string} direction The direction to sort the selection: ascending | descending | reverse 41 | * @param {boolean} openInNewTextEditor Optionally open the sorted lines in a new editor 42 | * @returns {Promise} 43 | * @async 44 | */ 45 | export async function sortLines(direction: string, openInNewTextEditor?: boolean): Promise { 46 | const eol = getDocumentEOL(getActiveEditor()); 47 | 48 | let editor = getActiveEditor(); 49 | if (!editor) { 50 | return Promise.reject("No active editor"); 51 | } 52 | let selectedLines = getLinesFromSelection(editor); 53 | 54 | if (!selectedLines) { 55 | return Promise.reject("No lines to sort, all lines are null or empty"); 56 | } 57 | 58 | let sortedLines: TextLine[]; 59 | let selectionStartLineNumber = selectedLines[0].lineNumber; 60 | let selectionEndLineNumber = selectedLines.at(-1)!.lineNumber; 61 | switch (direction) { 62 | case "ascending": 63 | sortedLines = selectedLines.sort((a, b) => 0 - (a.text > b.text ? -1 : 1)); 64 | break; 65 | case "descending": 66 | sortedLines = selectedLines.sort((a, b) => 0 - (a.text > b.text ? 1 : -1)); 67 | break; 68 | case "reverse": 69 | sortedLines = selectedLines.reverse(); 70 | break; 71 | default: 72 | return Promise.reject("Sort direction is invalid"); 73 | } 74 | 75 | let newText = sortedLines.map((line) => line.text).join(eol); 76 | if (openInNewTextEditor) { 77 | createNewEditor(newText); 78 | return Promise.resolve(true); 79 | } else { 80 | const editor = window.activeTextEditor; 81 | // prettier-ignore 82 | const selection = new Selection( 83 | selectionStartLineNumber, 84 | 0, 85 | selectionEndLineNumber, 86 | editor!.document.lineAt(selectionEndLineNumber).text.length 87 | ); 88 | 89 | editor?.edit((editBuilder) => { 90 | editBuilder.replace(selection, newText); 91 | }); 92 | } 93 | 94 | return Promise.resolve(true); 95 | } 96 | 97 | /** 98 | * Sort by line length the selected lines or all the lines in the document, if there is no selection. 99 | * Optionally opens the sorted lines in a new editor. 100 | * 101 | * @export 102 | * @async 103 | * @param {string} direction Sort direction: ascending, descending or reverse 104 | * @param {?boolean} [openInNewTextEditor] Optionally open the sorted lines in a new editor 105 | * @returns {Promise} 106 | */ 107 | export async function sortLinesByLength(direction: string, openInNewTextEditor?: boolean): Promise { 108 | const eol = getDocumentEOL(getActiveEditor()); 109 | 110 | let newLines = getDocumentTextOrSelection() 111 | ?.split(eol) 112 | .filter((el) => { 113 | return el !== null && el !== ""; 114 | }); 115 | if (!newLines) { 116 | return Promise.reject("No lines to sort, all lines are null or empty"); 117 | } 118 | 119 | let sortedLines: string[]; 120 | switch (direction) { 121 | case "ascending": 122 | sortedLines = newLines.sort((a, b) => a.length - b.length); 123 | break; 124 | case "descending": 125 | sortedLines = newLines.sort((a, b) => b.length - a.length); 126 | break; 127 | case "reverse": 128 | sortedLines = newLines.reverse(); 129 | break; 130 | default: 131 | return Promise.reject("Sort direction is invalid"); 132 | } 133 | 134 | if (openInNewTextEditor) { 135 | createNewEditor(sortedLines?.join(eol)); 136 | return Promise.resolve(true); 137 | } else { 138 | const editor = window.activeTextEditor; 139 | const selection = getSelection(editor!); 140 | editor?.edit((editBuilder) => { 141 | editBuilder.replace(selection!, sortedLines!.join(eol)); 142 | }); 143 | } 144 | 145 | return Promise.resolve(true); 146 | } 147 | -------------------------------------------------------------------------------- /src/modules/statusBarSelection.ts: -------------------------------------------------------------------------------- 1 | import { window, ExtensionContext, workspace, StatusBarAlignment, Selection, StatusBarItem } from "vscode"; 2 | import { getActiveEditor, getCursorPosition, getTextFromSelection } from "./helpers"; 3 | 4 | let statusBarItem: StatusBarItem; 5 | 6 | /** 7 | * Registers relevant StatusBar events to the ExtensionContext 8 | * @param {ExtensionContext} context An ExtensionContext used to subscribe to relevant StatusBar events 9 | */ 10 | export function createStatusBarItem(context: ExtensionContext) { 11 | updateStatusBarConfiguration(); 12 | statusBarItem.command = undefined; 13 | context.subscriptions.push(statusBarItem); 14 | 15 | context.subscriptions.push(window.onDidChangeTextEditorSelection(updateStatusBar)); 16 | context.subscriptions.push(window.onDidChangeActiveTextEditor(updateStatusBar)); 17 | context.subscriptions.push(window.onDidChangeActiveTextEditor(countWords)); 18 | context.subscriptions.push( 19 | workspace.onDidChangeConfiguration((e) => { 20 | if (e.affectsConfiguration("TextToolbox.enableStatusBarWordLineCount") || e.affectsConfiguration("TextToolbox.statusBarPriority")) { 21 | window.showInformationMessage("Please reload the window for the change to take effect"); 22 | } 23 | 24 | if (e.affectsConfiguration("TextToolbox.tabOut.enabled")) { 25 | updateStatusBar(); 26 | } 27 | }) 28 | ); 29 | } 30 | 31 | /** 32 | * Updates the StatusBar configuration for this extension 33 | */ 34 | function updateStatusBarConfiguration() { 35 | if (!workspace.getConfiguration().get("TextToolbox.enableStatusBarWordLineCount")) { 36 | disposeStatusBarItem(); 37 | return; 38 | } 39 | 40 | let statusBarAlignment; 41 | switch (workspace.getConfiguration().get("TextToolbox.statusBarAlignment")) { 42 | case "Right": 43 | statusBarAlignment = StatusBarAlignment.Right; 44 | break; 45 | case "Left": 46 | statusBarAlignment = StatusBarAlignment.Left; 47 | break; 48 | default: 49 | break; 50 | } 51 | const statusBarPriority: number | undefined = workspace.getConfiguration().get("TextToolbox.statusBarPriority"); 52 | 53 | if (!statusBarItem) { 54 | statusBarItem = window.createStatusBarItem(statusBarAlignment, statusBarPriority); 55 | statusBarItem.command = "vscode-texttoolbox.createStatusBarItem"; 56 | } 57 | } 58 | 59 | /** 60 | * Returns the number of lines in a selection 61 | * @param {Selection} selection The selection containing the lines to be counted 62 | * @returns {number} 63 | */ 64 | function countSelectedLines(selection: Selection): number { 65 | let n = 0; 66 | if (selection.start.line === selection.end.line) { 67 | if (selection.start.character !== selection.end.character) { 68 | // only one line 69 | n += 1; 70 | } 71 | } else { 72 | n = selection.end.line - selection.start.line + 1; 73 | } 74 | 75 | return n; 76 | } 77 | 78 | /** 79 | * Returns the number of works in the active document 80 | * @returns {number} 81 | */ 82 | function countWords(): number { 83 | let text: string | undefined; 84 | let selection = window.activeTextEditor?.selection; 85 | if (selection) { 86 | if (selection.isEmpty) { 87 | text = window.activeTextEditor?.document.getText(); 88 | } else { 89 | text = getTextFromSelection(window.activeTextEditor!, selection); 90 | } 91 | } 92 | if (!text) { 93 | return 0; 94 | } 95 | 96 | // remove unnecessary whitespaces 97 | text = text.replace(/(< ([^>]+)<)/g, "").replace(/\s+/g, " "); 98 | text = text.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); 99 | let n = 0; 100 | if (text !== "") { 101 | n = text.split(" ").length; 102 | } 103 | 104 | return n; 105 | } 106 | 107 | /** 108 | * Updates the StatusBar item with the number of lines and words in the active editor or selections. 109 | * Also add the cursor position (offset) 110 | */ 111 | function updateStatusBar() { 112 | const selections = window.activeTextEditor?.selections; 113 | if (!selections) { 114 | return; 115 | } 116 | let lineCount = selections.reduce((previous, current) => previous + countSelectedLines(current), 0); 117 | 118 | let wordCount = countWords(); 119 | 120 | let editor = getActiveEditor(); 121 | let cursorPosition = getCursorPosition(editor!)[0]; 122 | let offset = editor!.document.offsetAt(cursorPosition); 123 | // investigate: support multicursor offsets? 124 | 125 | let tabOutText = 126 | workspace.getConfiguration().get("TextToolbox.tabOut.enabled") && 127 | workspace.getConfiguration().get("TextToolbox.tabOut.showInStatusBar") 128 | ? ", TabOut" 129 | : ""; 130 | let lineCloudText = lineCount > 0 ? `Lns: ${lineCount}, ` : ""; 131 | 132 | if (wordCount > 0) { 133 | statusBarItem.text = `${lineCloudText}Wds: ${wordCount}, Pos: ${offset}${tabOutText}`; 134 | statusBarItem.show(); 135 | } else { 136 | statusBarItem.hide(); 137 | } 138 | } 139 | 140 | /** 141 | * Cleanup the StatusBar item 142 | */ 143 | export function disposeStatusBarItem() { 144 | statusBarItem.dispose(); 145 | } 146 | -------------------------------------------------------------------------------- /src/modules/tabOut.ts: -------------------------------------------------------------------------------- 1 | import { commands, Selection, window, workspace } from "vscode"; 2 | import { getActiveEditor, getCursorPosition } from "./helpers"; 3 | 4 | /** 5 | * Check if tabOut is enabled; this includes if tabOut is allowed for the active document's language type 6 | * 7 | * @returns {boolean} 8 | */ 9 | function isTabOutEnabled(): boolean { 10 | // is tabOut enabled? 11 | if (!workspace.getConfiguration().get("TextToolbox.tabOut.enabled")) { 12 | return false; 13 | } 14 | 15 | // is the current language type excluded? 16 | const excludedLanguages = workspace.getConfiguration().get("TextToolbox.tabOut.disableLanguages"); 17 | const currentLanguage = getActiveEditor()?.document.languageId; 18 | if (excludedLanguages?.includes(currentLanguage!)) { 19 | return false; 20 | } 21 | 22 | // is the current language type included? 23 | const includedLanguages = workspace.getConfiguration().get("TextToolbox.tabOut.enabledLanguages"); 24 | if (!includedLanguages?.includes(currentLanguage!) && !includedLanguages?.includes("*")) { 25 | return false; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | /** 32 | * Check if we should tabOut 33 | * 34 | * @returns {boolean} 35 | */ 36 | function shouldTabOut(): boolean { 37 | if (!isTabOutEnabled()) { 38 | return false; 39 | } 40 | 41 | const editor = getActiveEditor(); 42 | if (!editor) { 43 | return false; 44 | } 45 | 46 | let cursorPosition = getCursorPosition(editor); 47 | if (cursorPosition[0].character === 0) { 48 | // do not tabOut if the cursor is at the beginning of the line, this is used for indentation 49 | return false; 50 | } 51 | 52 | // is the next character in the tabOut list? 53 | const tabOutList = workspace.getConfiguration().get("TextToolbox.tabOut.characters"); 54 | const nextCharacter = editor.document.lineAt(cursorPosition[0].line).text[cursorPosition[0].character]; 55 | if (!tabOutList?.includes(nextCharacter)) { 56 | return false; 57 | } 58 | 59 | return true; 60 | } 61 | 62 | /** 63 | * Tab Out 64 | * 65 | * @export 66 | * @returns {*} 67 | */ 68 | export function tabOut() { 69 | if (!isTabOutEnabled()) { 70 | commands.executeCommand("tab"); 71 | return; 72 | } 73 | 74 | if (!shouldTabOut()) { 75 | commands.executeCommand("tab"); 76 | return; 77 | } 78 | 79 | // new cursor position 80 | const editor = getActiveEditor(); 81 | if (!editor) { 82 | return; 83 | } 84 | 85 | let cursorPosition = getCursorPosition(editor); 86 | let newPosition = cursorPosition[0].with(cursorPosition[0].line, cursorPosition[0].character + 1); 87 | let newSelection = new Selection(newPosition, newPosition); 88 | return (window.activeTextEditor!.selection = newSelection); 89 | } 90 | 91 | /** 92 | * Toggle tabOut enabled 93 | * 94 | * @export 95 | */ 96 | export function toggleTabOut() { 97 | const tabOutEnabled = workspace.getConfiguration("TextToolbox.tabOut"); 98 | tabOutEnabled.update("enabled", !tabOutEnabled.get("enabled"), true); 99 | } -------------------------------------------------------------------------------- /src/modules/textManipulation.ts: -------------------------------------------------------------------------------- 1 | import { Selection, TextEditor, window, TextLine } from "vscode"; 2 | import { 3 | getActiveEditor, 4 | getLinesFromDocumentOrSelection, 5 | getTextFromSelection, 6 | createNewEditor, 7 | getDocumentEOL, 8 | isNumber, 9 | incrementString, 10 | toRoman, 11 | } from "./helpers"; 12 | import * as path from "path"; 13 | import jwt_decode from "jwt-decode"; 14 | import { caseOptions } from './helpers'; 15 | 16 | /** 17 | * Trim whitespaces from the active selection(s) or from the entire document 18 | * @return {*} {(Promise)} 19 | */ 20 | export async function trimLineOrSelection(): Promise { 21 | const editor = getActiveEditor(); 22 | if (!editor) { 23 | return; 24 | } 25 | 26 | const textLines = getLinesFromDocumentOrSelection(editor); 27 | 28 | editor.edit((eb) => { 29 | textLines?.forEach((textLine) => { 30 | eb.replace(textLine.range, textLine.text.trim()); 31 | }); 32 | }); 33 | 34 | return Promise.resolve(true); 35 | } 36 | 37 | /** 38 | * Split the selection using the passed in delimiter 39 | * @return {*} 40 | */ 41 | export async function splitSelection(openInNewEditor: boolean) { 42 | const delimiter = await window.showInputBox({ prompt: "delimiter" }); 43 | if (!delimiter) { 44 | return; 45 | } 46 | 47 | splitSelectionInternal(delimiter, openInNewEditor); 48 | } 49 | 50 | /** 51 | * Split the selection using the passed in delimiter 52 | * @param {string} delimiter Delimiter to use to split the selection 53 | * @return {*} {Promise} 54 | */ 55 | export async function splitSelectionInternal(delimiter: string, openInNewEditor: boolean): Promise { 56 | const editor = getActiveEditor(); 57 | if (!editor) { 58 | return Promise.resolve(false); 59 | } 60 | 61 | if (editor.selection.isEmpty) { 62 | return Promise.resolve(false); 63 | } 64 | 65 | const eol = getDocumentEOL(getActiveEditor()); 66 | 67 | if (openInNewEditor) { 68 | let newEditorText: string = ""; 69 | 70 | editor.selections.forEach((s) => { 71 | newEditorText += getTextFromSelection(editor, s)?.split(delimiter).join(eol) + eol; 72 | }); 73 | 74 | await createNewEditor(newEditorText); 75 | } else { 76 | editor.edit((editBuilder) => { 77 | editor.selections.forEach((s) => { 78 | editBuilder.replace(s, getTextFromSelection(editor, s)?.split(delimiter).join(eol)!); 79 | }); 80 | }); 81 | } 82 | 83 | return Promise.resolve(true); 84 | } 85 | 86 | /** 87 | * Enumerates Platform path types 88 | * @enum {number} 89 | */ 90 | export enum pathTransformationType { 91 | "posix" = "posix", 92 | "win32" = "win32", 93 | "darwin" = "darwin", 94 | } 95 | 96 | /** 97 | * Transforms the selected path string to the chosen platform type target 98 | * @param {pathTransformationType} type Enum the Platform types to transform the path to 99 | * @return {*} {(Promise)} 100 | */ 101 | export async function transformPath(type: pathTransformationType): Promise { 102 | const editor = getActiveEditor(); 103 | if (!editor) { 104 | return Promise.reject(); 105 | } 106 | 107 | const selection = editor.selection; 108 | if (!selection) { 109 | return Promise.reject(); 110 | } 111 | 112 | let pathString = getTextFromSelection(editor, selection); 113 | 114 | switch (type) { 115 | case pathTransformationType.posix: 116 | pathString = path.posix.normalize(pathString!).replace(/\\+/g, "/"); 117 | break; 118 | 119 | case pathTransformationType.darwin: 120 | pathString = path.posix.normalize(pathString!).replace(/\\+/g, "/"); 121 | break; 122 | 123 | case pathTransformationType.win32: 124 | // if this is a json document, use double backslashes 125 | if (editor.document.languageId === "json" || editor.document.languageId === "jsonc") { 126 | pathString = path.posix.normalize(pathString!).replace(/\/+/g, "\\\\"); 127 | } else { 128 | pathString = path.posix.normalize(pathString!).replace(/\/+/g, "\\"); 129 | } 130 | 131 | break; 132 | 133 | default: 134 | break; 135 | } 136 | 137 | editor.edit((editBuilder) => { 138 | editBuilder.replace(selection, pathString!); 139 | }); 140 | } 141 | 142 | /** 143 | * Convert an integer to its hexadecimal representation 144 | * 145 | * @export 146 | * @param {number} n The integer to convert 147 | * @returns {(string | undefined)} 148 | */ 149 | export function convertDecimalToHexadecimal(n: number): string | undefined { 150 | if (Number.isInteger(n)) { 151 | return n.toString(16); 152 | } 153 | } 154 | 155 | /** 156 | * Convert an hexadecimal number to its decimal representation 157 | * 158 | * @export 159 | * @param {string} hex The hexadecimal number to convert 160 | * @returns {(number | undefined)} 161 | */ 162 | export function convertHexadecimalToDecimal(hex: string): number | undefined { 163 | return parseInt(hex, 16); 164 | } 165 | 166 | /** 167 | * Conversion type 168 | * 169 | * @export 170 | * @enum {number} 171 | */ 172 | export enum conversionType { 173 | "toBase64" = "toBase64", 174 | "fromBase64" = "fromBase64", 175 | "toHTML" = "toHTML", 176 | "fromHTML" = "fromHTML", 177 | "decToHex" = "decToHex", 178 | "hexToDec" = "hexToDec", 179 | "encodeUri" = "encodeUri", 180 | "decodeUri" = "decodeUri", 181 | "JWTDecode" = "JWTDecode", 182 | } 183 | 184 | /** 185 | * Convert the selected text to Base64 186 | * 187 | * @export 188 | * @async 189 | * @param {conversionType} conversion Conversion Type: toBase64 or fromBase64 190 | * @returns {*} 191 | */ 192 | export async function convertSelection(conversion: conversionType) { 193 | const editor = getActiveEditor(); 194 | if (!editor) { 195 | return; 196 | } 197 | 198 | const selections: any = editor.selections; 199 | if (!selections) { 200 | return; 201 | } 202 | 203 | await convertSelectionInternal(editor, selections, conversion); 204 | } 205 | 206 | /** 207 | * Convert the selected text to Base64 ro ASCII 208 | * 209 | * @export 210 | * @async 211 | * @param {TextEditor} editor The editor to convert the selection in 212 | * @param {Selection} selection The Selection to convert 213 | * @param {conversionType} conversion Conversion Type: toBase64 or fromBase64 214 | * @returns {(Promise)} 215 | */ 216 | export async function convertSelectionInternal(editor: TextEditor, selection: Selection[], conversion: conversionType): Promise { 217 | if (!editor) { 218 | return Promise.reject(); 219 | } 220 | 221 | editor.edit((editBuilder) => { 222 | selection.forEach((s) => { 223 | let textSelection = getTextFromSelection(editor, s); 224 | if (!textSelection) { 225 | return; 226 | } 227 | let convertedText: string | number | undefined; 228 | 229 | switch (conversion) { 230 | case conversionType.toBase64: 231 | convertedText = convertToBase64(textSelection); 232 | break; 233 | case conversionType.fromBase64: 234 | convertedText = convertFromBase64(textSelection); 235 | break; 236 | case conversionType.toHTML: 237 | convertedText = convertToHTML(textSelection); 238 | break; 239 | case conversionType.fromHTML: 240 | convertedText = convertFromHTML(textSelection); 241 | break; 242 | case conversionType.decToHex: 243 | convertedText = convertDecimalToHexadecimal(+textSelection); 244 | break; 245 | case conversionType.hexToDec: 246 | convertedText = convertHexadecimalToDecimal(textSelection); 247 | break; 248 | case conversionType.encodeUri: 249 | convertedText = encodeUri(textSelection); 250 | break; 251 | case conversionType.decodeUri: 252 | convertedText = decodeUri(textSelection); 253 | break; 254 | case conversionType.JWTDecode: 255 | convertedText = decodeJWTToken(textSelection); 256 | break; 257 | 258 | default: 259 | break; 260 | } 261 | 262 | if (convertedText) { 263 | editBuilder.replace(s, convertedText!.toString()); 264 | } 265 | }); 266 | }); 267 | } 268 | 269 | export function convertToBase64(text: string): string { 270 | return Buffer.from(text, "binary").toString("base64"); 271 | } 272 | export function convertFromBase64(text: string): string { 273 | return Buffer.from(text, "base64").toString("binary"); 274 | } 275 | 276 | export function convertToHTML(text: string): string { 277 | return text.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(//g, ">"); 278 | } 279 | 280 | export function convertFromHTML(text: string): string { 281 | return text 282 | .replace(/"/g, '"') 283 | .replace(/'/g, "'") 284 | .replace(/</g, "<") 285 | .replace(/>/g, ">") 286 | .replace(/&/g, "&"); 287 | } 288 | 289 | export function encodeUri(text: string): string { 290 | return encodeURIComponent(text); 291 | } 292 | 293 | export function decodeUri(text: string): string { 294 | return decodeURIComponent(text); 295 | } 296 | 297 | export function decodeJWTToken(token: string): string { 298 | let decodedToken: jwtToken = { 299 | token: token, 300 | header: { 301 | ...jwt_decode(token, { header: true }), 302 | }, 303 | payload: { 304 | ...jwt_decode(token), 305 | }, 306 | }; 307 | 308 | return JSON.stringify(decodedToken, null, 4); 309 | } 310 | 311 | type jwtToken = { 312 | token: string; 313 | header: { 314 | alg: string; 315 | typ: string; 316 | }; 317 | payload: { 318 | [key: string]: string; 319 | }; 320 | }; 321 | 322 | export enum orderedListTypes { 323 | "1. " = "Number.", 324 | "1) " = "Number)", 325 | "a. " = "lowercase.", 326 | "a) " = "lowercase)", 327 | "A. " = "Uppercase.", 328 | "A) " = "Uppercase)", 329 | "i. " = "Roman lowercase.", 330 | "i) " = "Roman lowercase)", 331 | "I. " = "Roman UPPERCASE.", 332 | "I) " = "Roman UPPERCASE)", 333 | } 334 | 335 | export async function transformToOrderedList() { 336 | const editor = getActiveEditor(); 337 | if (!editor) { 338 | return; 339 | } 340 | 341 | let pick: string | undefined; 342 | await new Promise((resolve) => { 343 | let quickPick = window.createQuickPick(); 344 | quickPick.onDidHide(() => quickPick.dispose()); 345 | quickPick.title = "Select the list type"; 346 | quickPick.canSelectMany = false; 347 | quickPick.matchOnDescription = true; 348 | 349 | const enumAsKeyValue = Object.entries(orderedListTypes).reduce((acc, [key, value]) => { 350 | acc[key] = value; 351 | return acc; 352 | }, {} as { [key: string]: string }); 353 | quickPick.items = Object.keys(enumAsKeyValue).map((listItem) => { 354 | return { label: listItem, description: enumAsKeyValue[listItem] }; 355 | }); 356 | quickPick.title = "Select the list type"; 357 | quickPick.show(); 358 | 359 | quickPick.onDidChangeSelection(async (selection) => { 360 | pick = selection[0].label; 361 | }); 362 | 363 | quickPick.onDidAccept(async () => { 364 | if (pick) { 365 | quickPick.hide(); 366 | resolve(pick); 367 | } 368 | }); 369 | }); 370 | if (!pick) { 371 | return; 372 | } 373 | 374 | const selections = editor.selections; 375 | let lines: TextLine[] = []; 376 | if (selections.every((selection) => selection.isEmpty)) { 377 | // multi-cursor, no text selected 378 | selections.forEach((selection) => { 379 | lines!.push(editor.document.lineAt(selection.start.line)); 380 | }); 381 | } else { 382 | lines = selections.map((selection) => getLinesFromDocumentOrSelection(editor, selection)!).flat(); 383 | } 384 | 385 | let [index, separator] = [pick[0], `${pick[1]}${pick[2]}`]; 386 | 387 | // handles 1. and 1) 388 | if (isNumber(index)) { 389 | editor.edit((editBuilder) => { 390 | let i = parseInt(index); 391 | lines!.forEach((line) => { 392 | editBuilder.insert(line.range.start, `${i}${separator}`); 393 | i++; 394 | }); 395 | }); 396 | } 397 | 398 | // handles a. and a) and A. and A) 399 | // 97 = a 400 | // 122 = z 401 | // 65 = A 402 | // 90 = Z 403 | if (index === "a" || index === "A") { 404 | editor.edit((editBuilder) => { 405 | lines!.forEach((line) => { 406 | editBuilder.insert(line.range.start, `${index}${separator}`); 407 | index = incrementString(index); 408 | }); 409 | }); 410 | } 411 | 412 | // handles I. and I) and i. and i) 413 | if (index === "I" || index === "i") { 414 | editor.edit((editBuilder) => { 415 | let outputCase = index === "I" ? caseOptions.upper : caseOptions.lower; 416 | // let outputCase: "upper" | "lower" = index === "I" ? "upper" : "lower"; 417 | let i = 1; 418 | lines!.forEach((line) => { 419 | editBuilder.insert(line.range.start, `${index}${separator}`); 420 | index = toRoman(i, outputCase); 421 | i++; 422 | }); 423 | }); 424 | } 425 | 426 | Promise.resolve(); 427 | } 428 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to the extension test runner script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error(err); 19 | console.error("Failed to run tests"); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | main(); 25 | -------------------------------------------------------------------------------- /src/test/suite/alignText.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { after, before, describe } from "mocha"; 3 | import { EOL } from "os"; 4 | import { alignText } from '../../modules/alignText'; 5 | import { closeTextEditor, sleep, createNewEditor, selectAllText, getDocumentTextOrSelection } from '../../modules/helpers'; 6 | 7 | suite("alignText", () => { 8 | before(() => { 9 | console.log("Starting alignText tests"); 10 | }); 11 | after(async () => { 12 | await sleep(500); 13 | await closeTextEditor(true); 14 | console.log("All alignText tests done"); 15 | }); 16 | 17 | describe("Align text to separator", () => { 18 | let newText = `asdfasdfasd,fadfasdfasdfasdf,adsfadf,asdf,asdfa,df${EOL}asdasdfa,sdfasdfa,sdf,adsf,asd,fasdfasdfadsf,${EOL}asd,sdfasdfa,dsf,asdfa,sdf,as,df,a,df,adfadfa`; 19 | let expectedComma = `asdfasdfasd, fadfasdfasdfasdf, adsfadf, asdf, asdfa, df, ,,,,${EOL}asdasdfa, sdfasdfa, sdf, adsf, asd, fasdfasdfadsf, ,,,,${EOL}asd, sdfasdfa, dsf, asdfa, sdf, as, df, a, df, adfadfa ${EOL}`; 20 | 21 | let tests = [{ newText: newText, separator: ",", expected: expectedComma }]; 22 | 23 | tests.forEach((t) => { 24 | test(`Align text to separator '${t.separator}'`, async () => { 25 | await createNewEditor(t.newText); 26 | await selectAllText(); 27 | await alignText(t.separator); 28 | await sleep(500); 29 | 30 | const text = getDocumentTextOrSelection(); 31 | assert.deepStrictEqual(text, t.expected); 32 | }); 33 | }); 34 | }); 35 | 36 | describe("Align text as table", () => { 37 | let newText = `asdfasdfasd,fadfasdfasdfasdf,adsfadf,asdf,asdfa,df${EOL}asdasdfa,sdfasdfa,sdf,adsf,asd,fasdfasdfadsf,${EOL}asd,sdfasdfa,dsf,asdfa,sdf,as,df,a,df,adfadfa`; 38 | let expectedTable = `| asdfasdfasd | fadfasdfasdfasdf | adsfadf | asdf | asdfa | df | | | | |${EOL}| asdasdfa | sdfasdfa | sdf | adsf | asd | fasdfasdfadsf | | | | |${EOL}| asd | sdfasdfa | dsf | asdfa | sdf | as | df | a | df | adfadfa |${EOL}`; 39 | let expectedTableWithHeaders = `| asdfasdfasd | fadfasdfasdfasdf | adsfadf | asdf | asdfa | df | | | | |${EOL}|-------------|------------------|---------|-------|-------|---------------|----|---|----|---------|${EOL}| asdasdfa | sdfasdfa | sdf | adsf | asd | fasdfasdfadsf | | | | |${EOL}| asd | sdfasdfa | dsf | asdfa | sdf | as | df | a | df | adfadfa |${EOL}`; 40 | 41 | let tests = [ 42 | { newText: newText, separator: ",", withHeaders: false, expected: expectedTable }, 43 | { newText: newText, separator: ",", withHeaders: true, expected: expectedTableWithHeaders }, 44 | ]; 45 | 46 | tests.forEach((t) => { 47 | test(`Align text as table ${t.withHeaders ? "with headers" : ""}`, async () => { 48 | await createNewEditor(t.newText); 49 | await selectAllText(); 50 | await alignText(t.separator, true, t.withHeaders); 51 | await sleep(500); 52 | 53 | const text = getDocumentTextOrSelection(); 54 | assert.deepStrictEqual(text, t.expected); 55 | }); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/test/suite/caseConversion.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { after, before, describe } from "mocha"; 3 | import { sleep, createNewEditor, selectAllText, closeTextEditor, getDocumentTextOrSelection, getActiveEditor } from "../../modules/helpers"; 4 | import { convertSelection, caseConversions } from "../../modules/caseConversion"; 5 | import { EOL } from "os"; 6 | import { Selection } from "vscode"; 7 | import * as os from "os"; 8 | 9 | suite("caseConversion", () => { 10 | before(() => { 11 | console.log("Starting caseConversion tests"); 12 | }); 13 | after(async () => { 14 | await sleep(500); 15 | await closeTextEditor(true); 16 | console.log("All insertText tests done"); 17 | }); 18 | 19 | describe("Case Conversion Single Line", () => { 20 | const testsSingleLine = [ 21 | { conversionType: caseConversions.camelCase, text: "test document", expectedText: "testDocument" }, 22 | { conversionType: caseConversions.constantCase, text: "test document", expectedText: "TEST_DOCUMENT" }, 23 | { conversionType: caseConversions.dotCase, text: "test document", expectedText: "test.document" }, 24 | { conversionType: caseConversions.headerCase, text: "test document", expectedText: "TEST-DOCUMENT" }, 25 | { conversionType: caseConversions.invertCase, text: "TeSt dOcuUMent", expectedText: "tEsT DoCUumENT" }, 26 | { conversionType: caseConversions.kebabCase, text: "test document", expectedText: "test-document" }, 27 | { conversionType: caseConversions.pascalCase, text: "test document", expectedText: "TestDocument" }, 28 | { conversionType: caseConversions.pathCase, text: "test document", expectedText: `test${os.EOL}document` }, 29 | { conversionType: caseConversions.sentenceCase, text: "test document", expectedText: "Test document" }, 30 | ]; 31 | 32 | testsSingleLine.forEach((t) => { 33 | test(`Convert a single line of text to ${t.conversionType}`, async () => { 34 | await createNewEditor(t.text); 35 | await selectAllText(); 36 | convertSelection(t.conversionType); 37 | await sleep(500); 38 | const expectedText = t.expectedText; 39 | const actualText = getDocumentTextOrSelection(); 40 | assert.strictEqual(actualText, expectedText); 41 | }); 42 | }); 43 | }); 44 | 45 | describe.skip("Case Conversion Multi Line", () => { 46 | // todo: implement 47 | // test("Convert multicursor", async () => { 48 | // await createNewEditor(`asd${EOL}${EOL}asd`); 49 | // const editor = getActiveEditor(); 50 | // let selections: Selection[] = []; 51 | // selections.push(new Selection(0, 0, 0, 2)); 52 | // selections.push(new Selection(2, 0, 2, 2)); 53 | // editor!.selections = selections; 54 | // convertSelection(caseConversions.snakeCase); 55 | // await sleep(500); 56 | // await selectAllText(); 57 | // assert.deepStrictEqual(getDocumentTextOrSelection(), `asd_asd${EOL}${EOL}asd_asd`); 58 | // }); 59 | }); 60 | 61 | describe.skip("Case Conversion Multi Cursor", () => { 62 | // todo: implement 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/test/suite/controlCharacters.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { after, before, describe, afterEach } from "mocha"; 3 | import { EOL } from "os"; 4 | import { replaceControlCharacters } from "../../modules/controlCharacters"; 5 | import { closeTextEditor, createNewEditor, getActiveEditor, getDocumentTextOrSelection, selectAllText, sleep } from "../../modules/helpers"; 6 | import { ConfigurationTarget, Selection, window, workspace, commands, env } from "vscode"; 7 | 8 | suite("controlCharacters", () => { 9 | before(async () => { 10 | console.log("Starting controlCharacters tests"); 11 | let config = workspace.getConfiguration("TextToolbox", window.activeTextEditor?.document); 12 | await config.update("replaceControlCharactersWith", undefined, ConfigurationTarget.Global); 13 | }); 14 | after(async () => { 15 | await sleep(500); 16 | await closeTextEditor(true); 17 | console.log("All controlCharacters tests done"); 18 | }); 19 | 20 | afterEach(async () => { 21 | let config = workspace.getConfiguration("TextToolbox", window.activeTextEditor?.document); 22 | await config.update("replaceControlCharactersWith", undefined, ConfigurationTarget.Global); 23 | }); 24 | 25 | describe("Control Characters", () => { 26 | const newTextEditor = `Lorem​Ipsum­Dolor​Sit­Amet​Consectetur​Adipiscing​Elit​Sed‌Do​Eiusmod​Tempor​Incididunt​Ut​Labore​Et​Dolore​Magna​Aliqua​Ut​Enim​Ad​Minim​Veniam​Quis​Nostrud​Exercitation­Ullamco​Laboris‌Nisi​Ut​Aliquip­Ex​Ea​Commodo​Consequat​Duis​Aute‌Irure​Dolor​In​Reprehenderit​In​Voluptate​Velit​Esse​Cillum​Dolore­Eu​Fugiat​Nulla‌Pariatur​Excepteur­Sint​Occaecat‌Cupidatat​Non‌Proident​Sunt​In​Culpa­Qui​Officia​Deserunt​Mollit​Anim​Id​Est​Laborum${EOL}Lorem Ipsum Dolor Sit Amet Consectetur Adipiscing Elit Sed Do Eiusmod Tempor Incididunt Ut Labore Et Dolore Magna Aliqua Ut Enim Ad Minim Veniam Quis Nostrud Exercitation Ullamco Laboris Nisi Ut Aliquip Ex Ea Commodo Consequat Duis Aute Irure Dolor In Reprehenderit In Voluptate Velit Esse Cillum Dolore Eu Fugiat Nulla Pariatur Excepteur Sint Occaecat Cupidatat Non Proident Sunt In Culpa Qui Officia Deserunt Mollit Anim Id Est Laborum${EOL}${EOL}There‎ is one ‎before${EOL}– vs -${EOL}`; 27 | 28 | describe("Remove control characters from full document", () => { 29 | const expectedFullDocumentWithEmptyString = `LoremIpsumDolorSitAmetConsecteturAdipiscingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliquaUtEnimAdMinimVeniamQuisNostrudExercitationUllamcoLaborisNisiUtAliquipExEaCommodoConsequatDuisAuteIrureDolorInReprehenderitInVoluptateVelitEsseCillumDoloreEuFugiatNullaPariaturExcepteurSintOccaecatCupidatatNonProidentSuntInCulpaQuiOfficiaDeseruntMollitAnimIdEstLaborum${EOL}LoremIpsumDolorSitAmetConsecteturAdipiscingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliquaUtEnimAdMinimVeniamQuisNostrudExercitationUllamcoLaborisNisiUtAliquipExEaCommodoConsequatDuisAuteIrureDolorInReprehenderitInVoluptateVelitEsseCillumDoloreEuFugiatNullaPariaturExcepteurSintOccaecatCupidatatNonProidentSuntInCulpaQuiOfficiaDeseruntMollitAnimIdEstLaborum${EOL}${EOL}There is one before${EOL} vs -${EOL}`; 30 | 31 | const expectedFullDocumentWithSpace = `Lorem Ipsum Dolor Sit Amet Consectetur Adipiscing Elit Sed Do Eiusmod Tempor Incididunt Ut Labore Et Dolore Magna Aliqua Ut Enim Ad Minim Veniam Quis Nostrud Exercitation Ullamco Laboris Nisi Ut Aliquip Ex Ea Commodo Consequat Duis Aute Irure Dolor In Reprehenderit In Voluptate Velit Esse Cillum Dolore Eu Fugiat Nulla Pariatur Excepteur Sint Occaecat Cupidatat Non Proident Sunt In Culpa Qui Officia Deserunt Mollit Anim Id Est Laborum${EOL}Lorem Ipsum Dolor Sit Amet Consectetur Adipiscing Elit Sed Do Eiusmod Tempor Incididunt Ut Labore Et Dolore Magna Aliqua Ut Enim Ad Minim Veniam Quis Nostrud Exercitation Ullamco Laboris Nisi Ut Aliquip Ex Ea Commodo Consequat Duis Aute Irure Dolor In Reprehenderit In Voluptate Velit Esse Cillum Dolore Eu Fugiat Nulla Pariatur Excepteur Sint Occaecat Cupidatat Non Proident Sunt In Culpa Qui Officia Deserunt Mollit Anim Id Est Laborum${EOL}${EOL}There is one before${EOL} vs -`; 32 | 33 | const expectedFullDocumentWithCustomStringX = `LoremxIpsumxDolorxSitxAmetxConsecteturxAdipiscingxElitxSedxDoxEiusmodxTemporxIncididuntxUtxLaborexEtxDolorexMagnaxAliquaxUtxEnimxAdxMinimxVeniamxQuisxNostrudxExercitationxUllamcoxLaborisxNisixUtxAliquipxExxEaxCommodoxConsequatxDuisxAutexIrurexDolorxInxReprehenderitxInxVoluptatexVelitxEssexCillumxDolorexEuxFugiatxNullaxPariaturxExcepteurxSintxOccaecatxCupidatatxNonxProidentxSuntxInxCulpaxQuixOfficiaxDeseruntxMollitxAnimxIdxEstxLaborum${EOL}LoremxIpsumxDolorxSitxAmetxConsecteturxAdipiscingxElitxSedxDoxEiusmodxTemporxIncididuntxUtxLaborexEtxDolorexMagnaxAliquaxUtxEnimxAdxMinimxVeniamxQuisxNostrudxExercitationxUllamcoxLaborisxNisixUtxAliquipxExxEaxCommodoxConsequatxDuisxAutexIrurexDolorxInxReprehenderitxInxVoluptatexVelitxEssexCillumxDolorexEuxFugiatxNullaxPariaturxExcepteurxSintxOccaecatxCupidatatxNonxProidentxSuntxInxCulpaxQuixOfficiaxDeseruntxMollitxAnimxIdxEstxLaborum${EOL}${EOL}Therex is one xbefore${EOL}x vs -`; 34 | 35 | test("Remove control characters from full document, replace with empty string", async () => { 36 | await createNewEditor(newTextEditor); 37 | const editor = getActiveEditor(); 38 | await replaceControlCharacters(editor); 39 | await sleep(500); 40 | 41 | let text = getDocumentTextOrSelection(); 42 | assert.deepStrictEqual(text, expectedFullDocumentWithEmptyString); 43 | }); 44 | 45 | test("Remove control characters from full document, replace with space", async () => { 46 | let config = workspace.getConfiguration("TextToolbox", window.activeTextEditor?.document); 47 | await config.update("replaceControlCharactersWith", " ", ConfigurationTarget.Global); 48 | await createNewEditor(newTextEditor); 49 | const editor = getActiveEditor(); 50 | await replaceControlCharacters(editor); 51 | await sleep(500); 52 | 53 | let text = getDocumentTextOrSelection(); 54 | assert.deepStrictEqual(text, expectedFullDocumentWithSpace); 55 | }); 56 | 57 | test("Remove control characters from full document, replace with custom string x", async () => { 58 | let config = workspace.getConfiguration("TextToolbox", window.activeTextEditor?.document); 59 | await config.update("replaceControlCharactersWith", "x", ConfigurationTarget.Global); 60 | await createNewEditor(newTextEditor); 61 | const editor = getActiveEditor(); 62 | await replaceControlCharacters(editor); 63 | await sleep(500); 64 | 65 | let text = getDocumentTextOrSelection(); 66 | assert.deepStrictEqual(text, expectedFullDocumentWithCustomStringX); 67 | }); 68 | }); 69 | 70 | describe("Remove control characters from selection", () => { 71 | const expectedSelectiontWithEmptyString = `Lorem​Ipsum­Dolor​Sit­Amet​Consectetur​Adipiscing​Elit​Sed‌Do​Eiusmod​Tempor​Incididunt​Ut​Labore​Et​Dolore​Magna​Aliqua​Ut​Enim​Ad​Minim​Veniam​Quis​Nostrud​Exercitation­Ullamco​Laboris‌Nisi​Ut​Aliquip­Ex​Ea​Commodo​Consequat​Duis​Aute‌Irure​Dolor​In​Reprehenderit​In​Voluptate​Velit​Esse​Cillum​Dolore­Eu​Fugiat​Nulla‌Pariatur​Excepteur­Sint​Occaecat‌Cupidatat​Non‌Proident​Sunt​In​Culpa­Qui​Officia​Deserunt​Mollit​Anim​Id​Est​Laborum${EOL}Lorem Ipsum Dolor Sit Amet Consectetur Adipiscing Elit Sed Do Eiusmod Tempor Incididunt Ut Labore Et Dolore Magna Aliqua Ut Enim Ad Minim Veniam Quis Nostrud Exercitation Ullamco Laboris Nisi Ut Aliquip Ex Ea Commodo Consequat Duis Aute Irure Dolor In Reprehenderit In Voluptate Velit Esse Cillum Dolore Eu Fugiat Nulla Pariatur Excepteur Sint Occaecat Cupidatat Non Proident Sunt In Culpa Qui Officia Deserunt Mollit Anim Id Est Laborum${EOL}${EOL}There‎ is one ‎before${EOL} vs -${EOL}`; 72 | const expectedSelectiontWithSpace = `Lorem​Ipsum­Dolor​Sit­Amet​Consectetur​Adipiscing​Elit​Sed‌Do​Eiusmod​Tempor​Incididunt​Ut​Labore​Et​Dolore​Magna​Aliqua​Ut​Enim​Ad​Minim​Veniam​Quis​Nostrud​Exercitation­Ullamco​Laboris‌Nisi​Ut​Aliquip­Ex​Ea​Commodo​Consequat​Duis​Aute‌Irure​Dolor​In​Reprehenderit​In​Voluptate​Velit​Esse​Cillum​Dolore­Eu​Fugiat​Nulla‌Pariatur​Excepteur­Sint​Occaecat‌Cupidatat​Non‌Proident​Sunt​In​Culpa­Qui​Officia​Deserunt​Mollit​Anim​Id​Est​Laborum${EOL}Lorem Ipsum Dolor Sit Amet Consectetur Adipiscing Elit Sed Do Eiusmod Tempor Incididunt Ut Labore Et Dolore Magna Aliqua Ut Enim Ad Minim Veniam Quis Nostrud Exercitation Ullamco Laboris Nisi Ut Aliquip Ex Ea Commodo Consequat Duis Aute Irure Dolor In Reprehenderit In Voluptate Velit Esse Cillum Dolore Eu Fugiat Nulla Pariatur Excepteur Sint Occaecat Cupidatat Non Proident Sunt In Culpa Qui Officia Deserunt Mollit Anim Id Est Laborum${EOL}${EOL}There‎ is one ‎before${EOL} vs -${EOL}`; 73 | const expectedSelectiontWithCustomStringX = `Lorem​Ipsum­Dolor​Sit­Amet​Consectetur​Adipiscing​Elit​Sed‌Do​Eiusmod​Tempor​Incididunt​Ut​Labore​Et​Dolore​Magna​Aliqua​Ut​Enim​Ad​Minim​Veniam​Quis​Nostrud​Exercitation­Ullamco​Laboris‌Nisi​Ut​Aliquip­Ex​Ea​Commodo​Consequat​Duis​Aute‌Irure​Dolor​In​Reprehenderit​In​Voluptate​Velit​Esse​Cillum​Dolore­Eu​Fugiat​Nulla‌Pariatur​Excepteur­Sint​Occaecat‌Cupidatat​Non‌Proident​Sunt​In​Culpa­Qui​Officia​Deserunt​Mollit​Anim​Id​Est​Laborum${EOL}Lorem Ipsum Dolor Sit Amet Consectetur Adipiscing Elit Sed Do Eiusmod Tempor Incididunt Ut Labore Et Dolore Magna Aliqua Ut Enim Ad Minim Veniam Quis Nostrud Exercitation Ullamco Laboris Nisi Ut Aliquip Ex Ea Commodo Consequat Duis Aute Irure Dolor In Reprehenderit In Voluptate Velit Esse Cillum Dolore Eu Fugiat Nulla Pariatur Excepteur Sint Occaecat Cupidatat Non Proident Sunt In Culpa Qui Officia Deserunt Mollit Anim Id Est Laborum${EOL}${EOL}There‎ is one ‎before${EOL}x vs -${EOL}`; 74 | 75 | test("Remove control characters from selection, replace with empty string", async () => { 76 | await createNewEditor(newTextEditor); 77 | const editor = getActiveEditor(); 78 | let selections: Selection[] = []; 79 | selections.push(new Selection(4, 0, 4, 8)); 80 | editor!.selections = selections; 81 | await replaceControlCharacters(editor); 82 | await sleep(500); 83 | 84 | await selectAllText(); 85 | let text = getDocumentTextOrSelection(); 86 | assert.deepStrictEqual(text, expectedSelectiontWithEmptyString); 87 | }); 88 | 89 | test("Remove control characters from selection, replace with space", async () => { 90 | let config = workspace.getConfiguration("TextToolbox", window.activeTextEditor?.document); 91 | await config.update("replaceControlCharactersWith", " ", ConfigurationTarget.Global); 92 | await createNewEditor(newTextEditor); 93 | const editor = getActiveEditor(); 94 | let selections: Selection[] = []; 95 | selections.push(new Selection(4, 0, 4, 7)); 96 | editor!.selections = selections; 97 | await replaceControlCharacters(editor); 98 | await sleep(500); 99 | 100 | await selectAllText(); 101 | let text = getDocumentTextOrSelection(); 102 | assert.deepStrictEqual(text, expectedSelectiontWithSpace); 103 | }); 104 | 105 | test("Remove control characters from selection, replace with custom string x", async () => { 106 | let config = workspace.getConfiguration("TextToolbox", window.activeTextEditor?.document); 107 | await config.update("replaceControlCharactersWith", "x", ConfigurationTarget.Global); 108 | await createNewEditor(newTextEditor); 109 | const editor = getActiveEditor(); 110 | let selections: Selection[] = []; 111 | selections.push(new Selection(4, 0, 4, 8)); 112 | editor!.selections = selections; 113 | await replaceControlCharacters(editor); 114 | await sleep(500); 115 | 116 | await selectAllText(); 117 | let text = getDocumentTextOrSelection(); 118 | assert.deepStrictEqual(text, expectedSelectiontWithCustomStringX); 119 | }); 120 | }); 121 | 122 | describe.skip("Remove control charachters on paste", () => { 123 | test("Remove control characters on paste", async () => { 124 | let config = workspace.getConfiguration("TextToolbox", window.activeTextEditor?.document); 125 | await config.update("removeControlCharactersOnPaste", true, ConfigurationTarget.Global); 126 | 127 | await env.clipboard.writeText(newTextEditor); 128 | 129 | await createNewEditor(); 130 | await commands.executeCommand("editor.action.clipboardPasteAction"); 131 | await sleep(500); 132 | 133 | const newText = getDocumentTextOrSelection(); 134 | const expectedFullDocumentWithEmptyString = `LoremIpsumDolorSitAmetConsecteturAdipiscingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliquaUtEnimAdMinimVeniamQuisNostrudExercitationUllamcoLaborisNisiUtAliquipExEaCommodoConsequatDuisAuteIrureDolorInReprehenderitInVoluptateVelitEsseCillumDoloreEuFugiatNullaPariaturExcepteurSintOccaecatCupidatatNonProidentSuntInCulpaQuiOfficiaDeseruntMollitAnimIdEstLaborum${EOL}LoremIpsumDolorSitAmetConsecteturAdipiscingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliquaUtEnimAdMinimVeniamQuisNostrudExercitationUllamcoLaborisNisiUtAliquipExEaCommodoConsequatDuisAuteIrureDolorInReprehenderitInVoluptateVelitEsseCillumDoloreEuFugiatNullaPariaturExcepteurSintOccaecatCupidatatNonProidentSuntInCulpaQuiOfficiaDeseruntMollitAnimIdEstLaborum${EOL}${EOL}There is one before${EOL} vs -${EOL}`; 135 | 136 | assert.deepStrictEqual(newText, expectedFullDocumentWithEmptyString); 137 | 138 | await config.update("removeControlCharactersOnPaste", undefined, ConfigurationTarget.Global); 139 | }); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/test/suite/delimiters.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { after, before, describe } from "mocha"; 3 | import { EOL } from "os"; 4 | import { alignText } from "../../modules/alignText"; 5 | import { closeTextEditor, sleep, createNewEditor, selectAllText, getDocumentTextOrSelection } from "../../modules/helpers"; 6 | 7 | suite("delimiters", () => { 8 | before(() => { 9 | console.log("Starting delimiters tests"); 10 | }); 11 | after(async () => { 12 | await sleep(500); 13 | await closeTextEditor(true); 14 | console.log("All delimiters tests done"); 15 | }); 16 | 17 | describe.skip("Select text between quotes", () => { 18 | 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/test/suite/filterText.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { before, after, describe } from "mocha"; 3 | import { sleep, createNewEditor, selectAllText, getDocumentTextOrSelection, closeTextEditor, linesToLine, getActiveEditor } from "../../modules/helpers"; 4 | import { findLinesMatchingRegEx, openSelectionInNewEditor, removeDuplicateLines, removeEmptyLines } from "../../modules/filterText"; 5 | import * as os from "os"; 6 | import { ConfigurationTarget, window, workspace, Selection } from "vscode"; 7 | 8 | suite("filterText", () => { 9 | before(() => { 10 | console.log("Starting filterText tests"); 11 | }); 12 | after(async () => { 13 | await sleep(500); 14 | await closeTextEditor(true); 15 | console.log("All insertText tests done"); 16 | }); 17 | 18 | describe("Remove all empty lines", async () => { 19 | const eol = os.EOL; 20 | const testEditorText = `Fehfomda pemup mihjeb${eol}${eol}uvonono nelvojpo wokragsi geligab${eol}${eol}${eol}pokacan repafme racje ut alhacov${eol}${eol}Hireme gahze${eol}${eol}${eol}${eol}${eol}pi zo iro becago vekabo${eol}luihait abe zukuv gof tususho${eol}${eol}${eol}${eol}`; 21 | const testEditorTextRedundantExpected = `Fehfomda pemup mihjeb${eol}${eol}uvonono nelvojpo wokragsi geligab${eol}${eol}pokacan repafme racje ut alhacov${eol}${eol}Hireme gahze${eol}${eol}pi zo iro becago vekabo${eol}luihait abe zukuv gof tususho${eol}${eol}`; 22 | const testEditorTextAllExpected = `Fehfomda pemup mihjeb${eol}uvonono nelvojpo wokragsi geligab${eol}pokacan repafme racje ut alhacov${eol}Hireme gahze${eol}pi zo iro becago vekabo${eol}luihait abe zukuv gof tususho${eol}`; 23 | 24 | let tests = [ 25 | { docText: testEditorText, redundantOnly: true, expected: testEditorTextRedundantExpected }, 26 | { docText: testEditorText, redundantOnly: false, expected: testEditorTextAllExpected }, 27 | ]; 28 | 29 | tests.forEach(function (t) { 30 | let itRedundant; 31 | t.redundantOnly ? (itRedundant = "redundant") : (itRedundant = "all"); 32 | test("Remove " + itRedundant + " empty lines", async () => { 33 | await createNewEditor(testEditorText); 34 | await removeEmptyLines(t.redundantOnly); 35 | await sleep(500); 36 | 37 | let text = getDocumentTextOrSelection(); 38 | assert.deepStrictEqual(text, t.expected); 39 | }); 40 | }); 41 | }); 42 | 43 | describe("Remove duplicate lines", async () => { 44 | const eol = os.EOL; 45 | const testEditorDuplicateLines = `Gowul ibbohu tafeid fokecdif lab adazujob${eol}meaf dekase sij of wehi nefowumu wizabeti${eol}${eol}Gowul ibbohu tafeid fokecdif lab adazujob${eol}Gowul ibbohu tafeid fokecdif lab adazujob${eol}${eol}${eol}palte${eol}${eol}${eol}meaf dekase sij of wehi nefowumu wizabeti${eol}meaf dekase sij of wehi nefowumu wizabeti${eol}meaf dekase sij of wehi nefowumu wizabeti${eol}${eol}${eol}Nefzuh toehe jiubvid tic didukod ehe ji ${eol}ana mur tiofapel sudvivot hub wurgo jifhi jumkehfot ${eol}palte${eol}${eol}ana mur tiofapel sudvivot hub wurgo jifhi jumkehfot ${eol}`; 46 | const testEditorDuplicateLinesRemoved = `Gowul ibbohu tafeid fokecdif lab adazujob${eol}meaf dekase sij of wehi nefowumu wizabeti${eol}palte${eol}Nefzuh toehe jiubvid tic didukod ehe ji${eol}ana mur tiofapel sudvivot hub wurgo jifhi jumkehfot`; 47 | 48 | const tests = [ 49 | { docText: testEditorDuplicateLines, selection: true, openInNewEditor: false, expected: testEditorDuplicateLinesRemoved }, 50 | { docText: testEditorDuplicateLines, selection: false, openInNewEditor: false, expected: testEditorDuplicateLinesRemoved }, 51 | { docText: testEditorDuplicateLines, selection: true, openInNewEditor: true, expected: testEditorDuplicateLinesRemoved }, 52 | { docText: testEditorDuplicateLines, selection: false, openInNewEditor: true, expected: testEditorDuplicateLinesRemoved }, 53 | ]; 54 | 55 | tests.forEach(function (t) { 56 | let testTitle: string; 57 | if (t.selection && !t.openInNewEditor) { 58 | testTitle = "Remove duplicate lines and update selection"; 59 | } else if (!t.selection && !t.openInNewEditor) { 60 | testTitle = "Remove duplicate lines from the document"; 61 | } else if (t.selection && t.openInNewEditor) { 62 | testTitle = "Remove duplciate lines, open result in new editor"; 63 | } else if (!t.selection && t.openInNewEditor) { 64 | testTitle = "Remove duplicate lines from the document, open resul in new editor"; 65 | } 66 | 67 | test(testTitle!, async () => { 68 | await createNewEditor(t.docText); 69 | if (t.selection) { 70 | await selectAllText(); 71 | } 72 | await removeDuplicateLines(t.openInNewEditor); 73 | await sleep(500); 74 | 75 | let text = getDocumentTextOrSelection(); 76 | assert.deepStrictEqual(text, t.expected); 77 | }); 78 | }); 79 | }); 80 | 81 | describe("Filter lines", () => { 82 | const eol = os.EOL; 83 | const textToFilter = `pippo${eol}pippo${eol}${eol}paperino${eol}paperino pippo${eol}${eol}${eol}pippo${eol}paperino${eol}pippo paperino${eol}${eol}paperino${eol}paperino${eol}${eol}paperino${eol}`; 84 | const regExpExpectedResult = `paperino${eol}paperino pippo${eol}paperino${eol}pippo paperino${eol}paperino${eol}paperino${eol}paperino`; 85 | 86 | let tests = [{ textToFilter: textToFilter, regExpString: "/.*paperino.*/gm", expected: regExpExpectedResult }]; 87 | 88 | tests.forEach(function (t) { 89 | test(`Filter text with ${t.regExpString}`, async () => { 90 | await createNewEditor(t.textToFilter); 91 | let result = findLinesMatchingRegEx(t.regExpString); 92 | await createNewEditor(await linesToLine(result!)); 93 | await sleep(500); 94 | 95 | let text = String(getDocumentTextOrSelection()); 96 | assert.deepStrictEqual(text, t.expected); 97 | }); 98 | 99 | tests = [{ textToFilter: textToFilter, regExpString: "paperino", expected: regExpExpectedResult }]; 100 | 101 | test(`Filter text with ${t.regExpString} as string`, async () => { 102 | let config = workspace.getConfiguration("TextToolbox", window.activeTextEditor?.document); 103 | await config.update("filtersUseRegularExpressions", false, ConfigurationTarget.Global); 104 | 105 | await createNewEditor(t.textToFilter); 106 | let result = findLinesMatchingRegEx(t.regExpString); 107 | await createNewEditor(await linesToLine(result!)); 108 | await sleep(500); 109 | 110 | let text = String(getDocumentTextOrSelection()); 111 | assert.deepStrictEqual(text, t.expected); 112 | 113 | await config.update("filtersUseRegularExpressions", undefined, ConfigurationTarget.Global); 114 | }); 115 | }); 116 | }); 117 | 118 | describe("Open selection in new editor", () => { 119 | const eol = os.EOL; 120 | const text = `pippo${eol}pippo${eol}${eol}paperino${eol}paperino pippo${eol}${eol}${eol}pippo${eol}paperino${eol}pippo paperino${eol}${eol}paperino${eol}paperino${eol}${eol}paperino${eol}`; 121 | const expected = `pippo${eol}${eol}paperino${eol}${eol}paperino pippo${eol}${eol}paperino${eol}`; 122 | 123 | test("Open selection in new editor", async () => { 124 | await createNewEditor(text); 125 | const editor = getActiveEditor(); 126 | let selections: Selection[] = []; 127 | selections.push(new Selection(0, 0, 0, 6)); 128 | selections.push(new Selection(3, 0, 3, 8)); 129 | selections.push(new Selection(4, 0, 4, 14)); 130 | selections.push(new Selection(11, 0, 11, 8)); 131 | editor!.selections = selections; 132 | await openSelectionInNewEditor(); 133 | await sleep(500); 134 | 135 | const newEditor = getActiveEditor(); 136 | const newText = newEditor?.document.getText(); 137 | 138 | assert.deepStrictEqual(newText, expected); 139 | }); 140 | 141 | // TODO: add test for empty selection 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/test/suite/insertText.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as guid from "guid"; 3 | import { 4 | sleep, 5 | createNewEditor, 6 | selectAllText, 7 | closeTextEditor, 8 | getDocumentTextOrSelection, 9 | getActiveEditor, 10 | getLinesFromSelection, 11 | } from "../../modules/helpers"; 12 | import { 13 | insertGUID, 14 | insertDateTimeInternal, 15 | padDirection, 16 | padSelectionInternal, 17 | insertLineNumbersInternal, 18 | sequenceType, 19 | insertSequenceInternal as insertSequenceInternal, 20 | insertLoremIpsumInternal, 21 | insertCurrencyInternal, 22 | } from "../../modules/insertText"; 23 | import { before, after, describe } from "mocha"; 24 | import { DateTime } from "luxon"; 25 | import { EOL } from "os"; 26 | import { Selection } from "vscode"; 27 | 28 | suite("insertText", () => { 29 | before(() => { 30 | console.log("Starting insertText tests"); 31 | }); 32 | after(async () => { 33 | await sleep(500); 34 | await closeTextEditor(true); 35 | console.log("All insertText tests done"); 36 | }); 37 | 38 | // TODO: test insert new text with no selection and replace text in existing selection 39 | 40 | describe("Insert Text", () => { 41 | describe("GUID", () => { 42 | test("Insert GUID", async () => { 43 | await createNewEditor(); 44 | insertGUID(); 45 | await sleep(500); 46 | 47 | let text = String(getDocumentTextOrSelection()); 48 | assert.ok(guid.isGuid(text), `Value "${text}" is not a valid GUID`); 49 | }); 50 | 51 | test("Insert GUID multicursor", async () => { 52 | await createNewEditor(`asd${EOL}${EOL}asd`); 53 | const editor = getActiveEditor(); 54 | editor!.selections = [new Selection(0, 0, 0, 3), new Selection(2, 0, 2, 3)]; 55 | insertGUID(); 56 | await sleep(500); 57 | 58 | let lines = getLinesFromSelection(editor!); 59 | lines?.forEach((line) => { 60 | if (line.text.length > 0) { 61 | assert.ok(guid.isGuid(line.text), `Value "${line.text}" is not a valid GUID`); 62 | } 63 | }); 64 | }); 65 | 66 | test("Insert GUID all zeros", async () => { 67 | await createNewEditor(); 68 | insertGUID(true); 69 | await sleep(500); 70 | 71 | let text = String(getDocumentTextOrSelection()); 72 | assert.ok(guid.isGuid(text), `Value "${text}" is not a valid GUID`); 73 | }); 74 | 75 | test("Insert GUID all zeros multicursor", async () => { 76 | await createNewEditor(`asd${EOL}${EOL}asd`); 77 | const editor = getActiveEditor(); 78 | editor!.selections = [new Selection(0, 0, 0, 3), new Selection(2, 0, 2, 3)]; 79 | insertGUID(true); 80 | await sleep(500); 81 | 82 | let lines = getLinesFromSelection(editor!); 83 | lines?.forEach((line) => { 84 | if (line.text.length > 0) { 85 | assert.ok(guid.isGuid(line.text), `Value "${line.text}" is not a valid GUID`); 86 | } 87 | }); 88 | }); 89 | }); 90 | 91 | describe("Padding", () => { 92 | let tests = [ 93 | { padDirection: padDirection.right, padString: "@", lenght: 10, expected: "test@@@@@@" }, 94 | { padDirection: padDirection.right, padString: "3", lenght: 10, expected: "test333333" }, 95 | { padDirection: padDirection.right, padString: "ab", lenght: 10, expected: "testababab" }, 96 | { padDirection: padDirection.right, padString: " ", lenght: 10, expected: "test " }, 97 | { padDirection: padDirection.left, padString: "@", lenght: 10, expected: "@@@@@@test" }, 98 | { padDirection: padDirection.left, padString: "3", lenght: 10, expected: "333333test" }, 99 | { padDirection: padDirection.left, padString: "ab", lenght: 10, expected: "abababtest" }, 100 | { padDirection: padDirection.left, padString: " ", lenght: 10, expected: " test" }, 101 | ]; 102 | tests.forEach(function (t) { 103 | test("Padding " + t.padDirection, async () => { 104 | await createNewEditor("test"); 105 | await selectAllText(); 106 | await padSelectionInternal(t.padDirection, t.padString, t.lenght); 107 | await sleep(500); 108 | 109 | let text = String(getDocumentTextOrSelection()); 110 | assert.deepStrictEqual(text, t.expected); 111 | }); 112 | }); 113 | 114 | tests = [ 115 | { padDirection: padDirection.right, padString: "@", lenght: 10, expected: "@@@@@@@@@@" }, 116 | { padDirection: padDirection.right, padString: "3", lenght: 10, expected: "3333333333" }, 117 | { padDirection: padDirection.right, padString: "ab", lenght: 10, expected: "ababababab" }, 118 | { padDirection: padDirection.right, padString: " ", lenght: 10, expected: " " }, 119 | { padDirection: padDirection.left, padString: "@", lenght: 10, expected: "@@@@@@@@@@" }, 120 | { padDirection: padDirection.left, padString: "3", lenght: 10, expected: "3333333333" }, 121 | { padDirection: padDirection.left, padString: "ab", lenght: 10, expected: "ababababab" }, 122 | { padDirection: padDirection.left, padString: " ", lenght: 10, expected: " " }, 123 | ]; 124 | tests.forEach(function (t) { 125 | test(`Padding ${t.padDirection} with "${t.padString}" on empty selection`, async () => { 126 | await createNewEditor(); 127 | await padSelectionInternal(t.padDirection, t.padString, t.lenght); 128 | await sleep(500); 129 | 130 | let text = String(getDocumentTextOrSelection()); 131 | assert.deepStrictEqual(text, t.expected); 132 | }); 133 | }); 134 | 135 | tests = [ 136 | { padDirection: padDirection.right, padString: "x", lenght: 10, expected: `asdxxxxxxx${EOL}${EOL}asxxxxxxxx` }, 137 | { padDirection: padDirection.left, padString: "x", lenght: 10, expected: `xxxxxxxasd${EOL}${EOL}xxxxxxxxas` }, 138 | ]; 139 | tests.forEach(function (t) { 140 | test("Padding multiline " + t.padDirection, async () => { 141 | await createNewEditor(`asd${EOL}${EOL}as`); 142 | const editor = getActiveEditor(); 143 | editor!.selections = [new Selection(0, 0, 0, 3), new Selection(2, 0, 2, 3)]; 144 | await padSelectionInternal(t.padDirection, t.padString, t.lenght); 145 | await sleep(500); 146 | 147 | await selectAllText(); 148 | let text = String(getDocumentTextOrSelection()); 149 | assert.deepStrictEqual(text, t.expected); 150 | }); 151 | }); 152 | }); 153 | 154 | describe("Test insert DateTime", () => { 155 | let testDate = DateTime.local(2020, 8, 25, 15, 34, 41).setZone("America/New_York"); 156 | 157 | let tests = [ 158 | { dateFormat: "DATE_SHORT", expected: "8/25/2020" }, 159 | { dateFormat: "TIME_SIMPLE", expected: "6:34 PM" }, 160 | { dateFormat: "TIME_WITH_SECONDS", expected: "6:34:41 PM" }, 161 | { dateFormat: "DATETIME_SHORT", expected: "8/25/2020, 6:34 PM" }, 162 | { dateFormat: "DATE_LONG", expected: "Tuesday, August 25, 2020" }, 163 | { dateFormat: "SORTABLE", expected: "2020-08-25T18:34:41" }, 164 | { dateFormat: "DATETIME_HUGE", expected: "Tuesday, August 25, 2020, 6:34 PM EDT" }, 165 | { dateFormat: "ROUNDTRIP", expected: "2020-08-25T22:34:41.000Z" }, 166 | { dateFormat: "UNIVERSAL_SORTABLE", expected: "2020-08-25T22:34:41Z" }, 167 | { dateFormat: "ISO8601", expected: "2020-08-25T18:34:41.000-04:00" }, 168 | { dateFormat: "RFC2822", expected: "Tue, 25 Aug 2020 18:34:41 -0400" }, 169 | { dateFormat: "HTTP", expected: "Tue, 25 Aug 2020 22:34:41 GMT" }, 170 | { dateFormat: "DATETIME_SHORT_WITH_SECONDS", expected: "8/25/2020, 6:34:41 PM" }, 171 | { dateFormat: "DATETIME_FULL_WITH_SECONDS", expected: "August 25, 2020, 6:34 PM EDT" }, 172 | { dateFormat: "UNIX_SECONDS", expected: "1598394881" }, 173 | { dateFormat: "UNIX_MILLISECONDS", expected: "1598394881000" }, 174 | ]; 175 | 176 | tests.forEach(function (t) { 177 | test("Insert Date " + t.dateFormat, async () => { 178 | await createNewEditor(); 179 | await insertDateTimeInternal(t.dateFormat, testDate); 180 | await sleep(500); 181 | 182 | let text = String(getDocumentTextOrSelection()); 183 | assert.deepStrictEqual(text, t.expected); 184 | }); 185 | }); 186 | 187 | test("Insert Date multicursor", async () => { 188 | await createNewEditor(`asd${EOL}${EOL}asd`); 189 | const editor = getActiveEditor(); 190 | editor!.selections = [new Selection(0, 0, 0, 3), new Selection(2, 0, 2, 3)]; 191 | await insertDateTimeInternal("DATE_SHORT", testDate); 192 | await sleep(500); 193 | 194 | await selectAllText(); 195 | let text = String(getDocumentTextOrSelection()); 196 | assert.deepStrictEqual(text, `8/25/2020${EOL}${EOL}8/25/2020`); 197 | }); 198 | }); 199 | 200 | describe("Insert line numbers", async () => { 201 | const tests = [ 202 | { startFrom: "1", text: `asd${EOL}asd${EOL}asd`, expected: `1 asd${EOL}2 asd${EOL}3 asd` }, 203 | { startFrom: "4", text: `asd${EOL}asd${EOL}asd`, expected: `4 asd${EOL}5 asd${EOL}6 asd` }, 204 | ]; 205 | 206 | tests.forEach((t) => { 207 | test(`Insert line numbers, startFrom ${t.startFrom}`, async () => { 208 | await createNewEditor(t.text); 209 | await selectAllText(); 210 | await insertLineNumbersInternal(t.startFrom); 211 | await sleep(500); 212 | 213 | let text = String(getDocumentTextOrSelection()); 214 | assert.deepStrictEqual(text, t.expected); 215 | }); 216 | }); 217 | }); 218 | 219 | describe("Insert sequence", () => { 220 | describe("Insert numbers sequence", async () => { 221 | const tests = [ 222 | { 223 | type: sequenceType.Numbers, 224 | startFrom: "1", 225 | length: 8, 226 | direction: undefined, 227 | expected: `1${EOL}2${EOL}3${EOL}4${EOL}5${EOL}6${EOL}7${EOL}8${EOL}`, 228 | }, 229 | { type: sequenceType.Numbers, startFrom: "14", length: 5, direction: undefined, expected: `14${EOL}15${EOL}16${EOL}17${EOL}18${EOL}` }, 230 | ]; 231 | 232 | tests.forEach((t) => { 233 | test(`Insert a number sequence starting from ${t.startFrom} for ${t.length} lines`, async () => { 234 | await createNewEditor(); 235 | await insertSequenceInternal(t.type, t.startFrom, t.length, t.direction); 236 | await sleep(500); 237 | 238 | let text = String(getDocumentTextOrSelection()); 239 | assert.deepStrictEqual(text, t.expected); 240 | }); 241 | }); 242 | }); 243 | }); 244 | 245 | describe("Insert Lorem Ipsum", () => { 246 | const loremTypes = [{ type: "Paragraphs" }, { type: "Sentences" }, { type: "Words" }]; 247 | 248 | loremTypes.forEach((l) => { 249 | test(`Insert Lorem Ipsum ${l.type}`, async () => { 250 | await createNewEditor(); 251 | await insertLoremIpsumInternal(l.type, 5); 252 | await sleep(500); 253 | 254 | let text = String(getDocumentTextOrSelection()); 255 | switch (l.type) { 256 | case "Paragraphs": 257 | assert.deepStrictEqual(text.split(EOL).length, 5); 258 | break; 259 | case "Sentences": 260 | assert.deepStrictEqual(text.split(". ").length, 5); 261 | break; 262 | case "Words": 263 | assert.deepStrictEqual(text.split(" ").length, 5); 264 | break; 265 | default: 266 | assert.fail("Invalid Lorem Ipsum paragraph type"); 267 | } 268 | }); 269 | }); 270 | }); 271 | 272 | describe("Insert currency", () => { 273 | const currencies = [ 274 | { currency: "US Dollar", symbol: "$" }, 275 | { currency: "Euro", symbol: "€" }, 276 | { currency: "British Pound", symbol: "£" }, 277 | { currency: "Japanese Yen", symbol: "¥" }, 278 | { currency: "Chinese Yuan", symbol: "¥" }, 279 | { currency: "Indian Rupee", symbol: "₹" }, 280 | { currency: "Mexican Peso", symbol: "$" }, 281 | { currency: "Russian Ruble", symbol: "₽" }, 282 | { currency: "Israeli New Shequel", symbol: "₪" }, 283 | { currency: "Bitcoin", symbol: "BTC" }, 284 | { currency: "South Korean Won", symbol: "₩" }, 285 | { currency: "South African Rand", symbol: "R" }, 286 | { currency: "Swiss Franc", symbol: "CHF" }, 287 | ]; 288 | 289 | currencies.forEach((c) => { 290 | test(`Insert ${c.currency}`, async () => { 291 | await createNewEditor(); 292 | await insertCurrencyInternal(c.currency, true); 293 | await sleep(500); 294 | 295 | const newText = getDocumentTextOrSelection(); 296 | 297 | let symbolPosition = newText?.indexOf(c.symbol)!; 298 | assert.ok(symbolPosition >= 0, "Could not find currency symbol in output string"); 299 | 300 | let currencyValue = newText?.replace(c.symbol!, ""); 301 | let isValidAmount = Number.parseFloat(currencyValue!) ? true : false; 302 | assert.ok(isValidAmount === true, "Invalid currency amount"); 303 | }); 304 | }); 305 | }); 306 | }); 307 | }); 308 | -------------------------------------------------------------------------------- /src/test/suite/json.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { after, before, describe } from 'mocha'; 3 | import { closeTextEditor, sleep, createNewEditor, selectAllText, getDocumentTextOrSelection } from '../../modules/helpers'; 4 | import { minifyJson, stringifyJson } from '../../modules/json'; 5 | import { EOL } from 'os'; 6 | 7 | suite('JSON', () => { 8 | before(() => { 9 | console.log('Starting JSON tests'); 10 | }); 11 | after(async () => { 12 | await sleep(500); 13 | await closeTextEditor(true); 14 | console.log('All JSON tests done'); 15 | }); 16 | 17 | describe('Json', async () => { 18 | let tests = [ 19 | { 20 | description: "Stringify", 21 | text: '[\n \t{\n \t\t"Name": "pwsh",\n \t\t"Id": 10788,\n \t\t"Company": "Microsoft Corporation"\n \t},\n \t{\n \t\t"Name": "pwsh",\n \t\t"Id": 24072,\n \t\t"Company": "Microsoft Corporation"\n \t},\n \t{\n \t\t"Name": "pwsh",\n \t\t"Id": 24784,\n \t\t"Company": "Microsoft Corporation"\n \t}\n ]', 22 | expected: '"[\\n \\t{\\n \\t\\t\\"Name\\": \\"pwsh\\",\\n \\t\\t\\"Id\\": 10788,\\n \\t\\t\\"Company\\": \\"Microsoft Corporation\\"\\n \\t},\\n \\t{\\n \\t\\t\\"Name\\": \\"pwsh\\",\\n \\t\\t\\"Id\\": 24072,\\n \\t\\t\\"Company\\": \\"Microsoft Corporation\\"\\n \\t},\\n \\t{\\n \\t\\t\\"Name\\": \\"pwsh\\",\\n \\t\\t\\"Id\\": 24784,\\n \\t\\t\\"Company\\": \\"Microsoft Corporation\\"\\n \\t}\\n ]"', 23 | fixJson: false 24 | }, 25 | { 26 | description: "Stringify simple object", 27 | text: "a:a,b:b", 28 | expected: `{${EOL} \"a\": \"a\",${EOL} \"b\": \"b\"${EOL}}`, 29 | fixJson: true 30 | }, 31 | { 32 | description: "Stringify with duplicate properties", 33 | text: "a:a,b:b,a:a", 34 | expected: `{${EOL} \"a\": \"a\",${EOL} \"b\": \"b\"${EOL}}`, 35 | fixJson: true 36 | } 37 | ]; 38 | 39 | tests.forEach(t => { 40 | test(`${t.description}`, async () => { 41 | await createNewEditor(t.text); 42 | await selectAllText(); 43 | 44 | await stringifyJson(t.fixJson); 45 | await selectAllText(); 46 | 47 | let actual = getDocumentTextOrSelection(); 48 | assert.deepStrictEqual(actual, t.expected); 49 | }); 50 | }); 51 | 52 | test('Minify', async () => { 53 | const text = '[\n \t{\n \t\t"Name": "pwsh",\n \t\t"Id": 10788,\n \t\t"Company": "Microsoft Corporation"\n \t},\n \t{\n \t\t"Name": "pwsh",\n \t\t"Id": 24072,\n \t\t"Company": "Microsoft Corporation"\n \t},\n \t{\n \t\t"Name": "pwsh",\n \t\t"Id": 24784,\n \t\t"Company": "Microsoft Corporation"\n \t}\n ]'; 54 | 55 | const expected = '[{"Name":"pwsh","Id":10788,"Company":"Microsoft Corporation"},{"Name":"pwsh","Id":24072,"Company":"Microsoft Corporation"},{"Name":"pwsh","Id":24784,"Company":"Microsoft Corporation"}]'; 56 | 57 | await createNewEditor(text); 58 | await selectAllText(); 59 | 60 | await minifyJson(); 61 | await selectAllText(); 62 | 63 | let actual = getDocumentTextOrSelection(); 64 | assert.deepStrictEqual(actual, expected); 65 | }); 66 | }); 67 | }); -------------------------------------------------------------------------------- /src/test/suite/sortText.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { before, after, describe } from 'mocha'; 3 | import { sleep, closeTextEditor, createNewEditor, getDocumentTextOrSelection, getLinesFromString, linesToLine } from '../../modules/helpers'; 4 | import * as os from 'os'; 5 | import { sortLines } from '../../modules/sortText'; 6 | import { Selection } from 'vscode'; 7 | 8 | 9 | suite('sortText', () => { 10 | before(() => { 11 | console.log('Starting sortText tests'); 12 | }); 13 | after(async () => { 14 | await sleep(500); 15 | await closeTextEditor(true); 16 | console.log('All sortText tests done'); 17 | }); 18 | 19 | describe("Sort Text", () => { 20 | const eol = os.EOL; 21 | const textUnsorted = `Connecticut${eol}Pennsylvania${eol}Rhode Island${eol}${eol}${eol}Delaware${eol}Alabama${eol}Arkansas${eol}${eol}New Jersey${eol}Washington${eol}New York${eol}Texas${eol}${eol}`; 22 | const expectedAscending = `Alabama${eol}Arkansas${eol}Connecticut${eol}Delaware${eol}New Jersey${eol}New York${eol}Pennsylvania${eol}Rhode Island${eol}Texas${eol}Washington`; 23 | const expectedDescending = `Washington${eol}Texas${eol}Rhode Island${eol}Pennsylvania${eol}New York${eol}New Jersey${eol}Delaware${eol}Connecticut${eol}Arkansas${eol}Alabama`; 24 | const expectedReverse = `Texas${eol}New York${eol}Washington${eol}New Jersey${eol}Arkansas${eol}Alabama${eol}Delaware${eol}Rhode Island${eol}Pennsylvania${eol}Connecticut`; 25 | const selectionExpectedAscending = `New Jersey${eol}New York${eol}Texas${eol}Washington`; 26 | const selectionExpectedDescending = `Washington${eol}Texas${eol}New York${eol}New Jersey`; 27 | const selectionExpectedReverse = `Texas${eol}New York${eol}Washington${eol}New Jersey`; 28 | 29 | let tests = [ 30 | { sortDirection: "ascending", textUnsorted: textUnsorted, expected: expectedAscending, openInNewTextEditor: false }, 31 | { sortDirection: "descending", textUnsorted: textUnsorted, expected: expectedDescending, openInNewTextEditor: false }, 32 | { sortDirection: "reverse", textUnsorted: textUnsorted, expected: expectedReverse, openInNewTextEditor: false }, 33 | { sortDirection: "ascending", textUnsorted: textUnsorted, expected: expectedAscending, openInNewTextEditor: true }, 34 | { sortDirection: "descending", textUnsorted: textUnsorted, expected: expectedDescending, openInNewTextEditor: true }, 35 | { sortDirection: "reverse", textUnsorted: textUnsorted, expected: expectedReverse, openInNewTextEditor: true } 36 | ]; 37 | 38 | tests.forEach(function (t) { 39 | test("Sort lines " + t.sortDirection + (t.openInNewTextEditor ? " in new editor" : ""), async () => { 40 | await createNewEditor(t.textUnsorted); 41 | await sortLines(t.sortDirection, t.openInNewTextEditor); 42 | await sleep(500); 43 | 44 | let lines = await getLinesFromString(String(getDocumentTextOrSelection())); 45 | assert.deepStrictEqual(await linesToLine(lines!), t.expected); 46 | }); 47 | }); 48 | 49 | tests = [ 50 | { sortDirection: "ascending", textUnsorted: textUnsorted, expected: selectionExpectedAscending, openInNewTextEditor: false }, 51 | { sortDirection: "descending", textUnsorted: textUnsorted, expected: selectionExpectedDescending, openInNewTextEditor: false }, 52 | { sortDirection: "reverse", textUnsorted: textUnsorted, expected: selectionExpectedReverse, openInNewTextEditor: false }, 53 | { sortDirection: "ascending", textUnsorted: textUnsorted, expected: selectionExpectedAscending, openInNewTextEditor: true }, 54 | { sortDirection: "descending", textUnsorted: textUnsorted, expected: selectionExpectedDescending, openInNewTextEditor: true }, 55 | { sortDirection: "reverse", textUnsorted: textUnsorted, expected: selectionExpectedReverse, openInNewTextEditor: true } 56 | ]; 57 | 58 | tests.forEach(t => { 59 | test("Sort selection " + t.sortDirection + (t.openInNewTextEditor ? " in new editor" : ""), async () => { 60 | const editor = await createNewEditor(t.textUnsorted); 61 | let selections: Selection[] = []; 62 | selections.push(new Selection(9, 0, 12, 5)); 63 | editor!.selections = selections; 64 | await sortLines(t.sortDirection, t.openInNewTextEditor); 65 | await sleep(500); 66 | 67 | let lines = await getLinesFromString(String(getDocumentTextOrSelection())); 68 | assert.deepStrictEqual(await linesToLine(lines!), t.expected); 69 | }); 70 | }); 71 | }); 72 | }); -------------------------------------------------------------------------------- /src/test/suite/splitText.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { before, after, describe } from "mocha"; 3 | import { sleep, closeTextEditor, createNewEditor, getActiveEditor, selectAllText, getDocumentTextOrSelection } from "../../modules/helpers"; 4 | import { Selection } from "vscode"; 5 | import { EOL } from "os"; 6 | import { splitSelectionInternal } from "../../modules/textManipulation"; 7 | 8 | suite("splitText", () => { 9 | before(() => { 10 | console.log("Starting splitText tests"); 11 | }); 12 | after(async () => { 13 | await sleep(500); 14 | await closeTextEditor(true); 15 | console.log("All splitText tests done"); 16 | }); 17 | 18 | describe("Split Text", () => { 19 | const textToSplit = `Aute ex anim dolore sunt pariatur culpa sit ut ut ex incididunt mollit dolore.${EOL}Irure, enim, culpa, reprehenderit, aliqua, laboris, irure, sint, proident, labore, quis, laboris, quis, quis.${EOL}Aute proident mollit eiusmod fugiat.`; 20 | 21 | const textSplitSecondLine = `Aute ex anim dolore sunt pariatur culpa sit ut ut ex incididunt mollit dolore.${EOL}Irure${EOL} enim${EOL} culpa${EOL} reprehenderit${EOL} aliqua${EOL} laboris${EOL} irure${EOL} sint${EOL} proident${EOL} labore${EOL} quis${EOL} laboris${EOL} quis${EOL} quis.${EOL}Aute proident mollit eiusmod fugiat.`; 22 | 23 | const textSplitFirstLineNewEditor = `Aute${EOL}ex${EOL}anim${EOL}dolore${EOL}sunt${EOL}pariatur${EOL}culpa${EOL}sit${EOL}ut${EOL}ut${EOL}ex${EOL}incididunt${EOL}mollit${EOL}dolore.${EOL}`; 24 | 25 | test("Split the second line in a text of three", async () => { 26 | await createNewEditor(textToSplit); 27 | const editor = getActiveEditor(); 28 | editor!.selections = [new Selection(1, 0, 1, editor?.document.lineAt(1).text.length!)]; 29 | // editor!.selections = [new Selection(1, 0, 1, 269)]; 30 | await splitSelectionInternal(",", false); 31 | await sleep(500); 32 | 33 | await selectAllText(); 34 | let text = getDocumentTextOrSelection(); 35 | assert.deepStrictEqual(text, textSplitSecondLine); 36 | }); 37 | 38 | test("Split the first line in a text of three, open in new editor", async () => { 39 | await createNewEditor(textToSplit); 40 | const editor = getActiveEditor(); 41 | editor!.selections = [new Selection(0, 0, 0, editor?.document.lineAt(1).text.length!)]; 42 | // editor!.selections = [new Selection(0, 0, 0, 99)]; 43 | await splitSelectionInternal(" ", true); 44 | await sleep(500); 45 | 46 | await selectAllText(); 47 | let text = getDocumentTextOrSelection(); 48 | assert.deepStrictEqual(text, textSplitFirstLineNewEditor); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/test/suite/textManipulation.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { before, after, describe } from 'mocha'; 3 | import { sleep, closeTextEditor, createNewEditor, getActiveEditor, selectAllText, getDocumentTextOrSelection } from '../../modules/helpers'; 4 | import * as os from 'os'; 5 | import { pathTransformationType, transformPath, trimLineOrSelection } from '../../modules/textManipulation'; 6 | import { Selection } from 'vscode'; 7 | 8 | suite("textManipulation", () => { 9 | before(() => { 10 | console.log('Starting textManipulation tests'); 11 | }); 12 | after(async () => { 13 | await sleep(500); 14 | await closeTextEditor(true); 15 | console.log('All testManipulation tests done') 16 | }); 17 | 18 | describe("Trim whitespaces from selection or document", () => { 19 | const eol = os.EOL; 20 | 21 | test("Trim whitespaces from document", async () => { 22 | const text = `Hutruaka pouzjan pu ${eol} elnordu ce ${eol} jan gabajo ${eol}genlosif fobavos vucozu jesidjo ${eol} os ijme koige fomej zuce ruv juusuje ${eol}`; 23 | const expected = `Hutruaka pouzjan pu${eol}elnordu ce${eol}jan gabajo${eol}genlosif fobavos vucozu jesidjo${eol}os ijme koige fomej zuce ruv juusuje${eol}`; 24 | 25 | await createNewEditor(text); 26 | 27 | await trimLineOrSelection(); 28 | await sleep(500); 29 | const editor = getActiveEditor(); 30 | const trimmedText = editor?.document.getText(); 31 | 32 | assert.deepStrictEqual(trimmedText, expected); 33 | }); 34 | 35 | test("Trim whitespaces from selection", async () => { 36 | const text = `Hutruaka pouzjan pu ${eol} elnordu ce ${eol} jan gabajo ${eol}genlosif fobavos vucozu jesidjo ${eol} os ijme koige fomej zuce ruv juusuje ${eol}`; 37 | const expected = `Hutruaka pouzjan pu ${eol}elnordu ce${eol} jan gabajo ${eol}genlosif fobavos vucozu jesidjo${eol} os ijme koige fomej zuce ruv juusuje ${eol}`; 38 | 39 | await createNewEditor(text); 40 | const editor = getActiveEditor(); 41 | let selections: Selection[] = []; 42 | selections.push(new Selection(1, 0, 1, 15)); 43 | selections.push(new Selection(3, 0, 3, 34)); 44 | editor!.selections = selections; 45 | 46 | await trimLineOrSelection(); 47 | await sleep(500); 48 | 49 | const trimmedText = editor?.document.getText(); 50 | 51 | assert.deepStrictEqual(trimmedText, expected); 52 | }); 53 | }); 54 | 55 | describe('Transform Path', () => { 56 | const tests = [ 57 | { type: pathTransformationType.posix, pathString: 'C:\\temp\\pippo.txt', expected: 'C:/temp/pippo.txt' }, 58 | { type: pathTransformationType.win32, pathString: 'C://temp//pippo.txt', expected: 'C:\\temp\\pippo.txt' } 59 | ]; 60 | 61 | tests.forEach(function (t) { 62 | test(`Transform path to ${t.type}`, async () => { 63 | await createNewEditor(t.pathString); 64 | await selectAllText(); 65 | await transformPath(t.type); 66 | await sleep(500); 67 | 68 | const actual = getDocumentTextOrSelection(); 69 | 70 | assert.deepStrictEqual(actual, t.expected); 71 | }); 72 | }); 73 | }); 74 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | 4 | const webConfig = /** @type WebpackConfig */ { 5 | context: __dirname, 6 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 7 | target: "webworker", // web extensions run in a webworker context 8 | entry: { 9 | "extension-web": "./src/extension.ts", // source of the web extension main file 10 | // "test/suite/index-web": "./src/test/suite/index-web.ts", // source of the web extension test runner 11 | }, 12 | output: { 13 | filename: "[name].js", 14 | path: path.join(__dirname, "./dist"), 15 | libraryTarget: "commonjs", 16 | }, 17 | resolve: { 18 | mainFields: ["browser", "module", "main"], // look for `browser` entry point in imported node modules 19 | extensions: [".ts", ".js"], // support ts-files and js-files 20 | alias: { 21 | // provides alternate implementation for node module and source files 22 | }, 23 | fallback: { 24 | // Webpack 5 no longer polyfills Node.js core modules automatically. 25 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 26 | // for the list of Node.js core module polyfills. 27 | assert: require.resolve("assert"), 28 | buffer: require.resolve("buffer"), 29 | // console: require.resolve("console-browserify"), 30 | // constants: require.resolve("constants-browserify"), 31 | crypto: require.resolve("crypto-browserify"), 32 | // domain: require.resolve("domain-browser"), 33 | // events: require.resolve("events"), 34 | // http: require.resolve("stream-http"), 35 | // https: require.resolve("https-browserify"), 36 | os: require.resolve("os-browserify/browser"), 37 | path: require.resolve("path-browserify"), 38 | // punycode: require.resolve("punycode"), 39 | process: require.resolve("process/browser"), 40 | // querystring: require.resolve("querystring-es3"), 41 | stream: require.resolve("stream-browserify"), 42 | // string_decoder: require.resolve("string_decoder"), 43 | // sys: require.resolve("util"), 44 | // timers: require.resolve("timers-browserify"), 45 | // tty: require.resolve("tty-browserify"), 46 | // url: require.resolve("url"), 47 | // util: require.resolve("util"), 48 | // vm: require.resolve("vm-browserify"), 49 | // zlib: require.resolve("browserify-zlib") 50 | 'lorem-ipsum': require.resolve("lorem-ipsum"), 51 | }, 52 | }, 53 | module: { 54 | rules: [ 55 | { 56 | test: /\.ts$/, 57 | exclude: /node_modules/, 58 | use: [ 59 | { 60 | loader: "ts-loader", 61 | }, 62 | ], 63 | }, 64 | ], 65 | }, 66 | plugins: [ 67 | new webpack.ProvidePlugin({ 68 | process: "process/browser", // provide a shim for the global `process` variable 69 | }), 70 | ], 71 | externals: { 72 | vscode: "commonjs vscode", // ignored because it doesn't exist 73 | }, 74 | performance: { 75 | hints: false, 76 | }, 77 | devtool: "nosources-source-map", // create a source map that points to the original source file 78 | }; 79 | const nodeConfig = /** @type WebpackConfig */ { 80 | context: __dirname, 81 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 82 | target: "node", // extensions run in a node context 83 | entry: { 84 | "extension-node": "./src/extension.ts", // source of the node extension main file 85 | // "test/suite/index-node": "./src/test/suite/index-node.ts", // source of the node extension test runner 86 | // "test/suite/extension.test": "./src/test/suite/extension.test.ts", // create a separate file for the tests, to be found by glob 87 | "test/runTest": "./src/test/runTest", // used to start the VS Code test runner (@vscode/test-electron) 88 | }, 89 | output: { 90 | filename: "[name].js", 91 | path: path.join(__dirname, "./dist"), 92 | libraryTarget: "commonjs", 93 | }, 94 | resolve: { 95 | mainFields: ["module", "main"], 96 | extensions: [".ts", ".js"], // support ts-files and js-files 97 | }, 98 | module: { 99 | rules: [ 100 | { 101 | test: /\.ts$/, 102 | exclude: /node_modules/, 103 | use: [ 104 | { 105 | loader: "ts-loader", 106 | }, 107 | ], 108 | }, 109 | ], 110 | }, 111 | externals: { 112 | vscode: "commonjs vscode", // ignored because it doesn't exist 113 | mocha: "commonjs mocha", // don't bundle 114 | "@vscode/test-electron": "commonjs @vscode/test-electron", // don't bundle 115 | }, 116 | performance: { 117 | hints: false, 118 | }, 119 | devtool: "nosources-source-map", // create a source map that points to the original source file 120 | }; 121 | 122 | module.exports = [webConfig, nodeConfig]; 123 | --------------------------------------------------------------------------------