├── .DS_Store ├── .eslintrc.json ├── .github └── workflows │ ├── ci.yml │ └── publish-extension.yml ├── .gitignore ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json ├── settings.json ├── snipsnap.code-snippets └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── content.txt ├── esbuild.js ├── eslint.config.mjs ├── icon.png ├── images ├── demo.gif └── preview-demo.gif ├── jsconfig.json ├── package-lock.json ├── package.json ├── src ├── commands │ ├── default-functions.ts │ ├── increment-decrement.ts │ ├── index.ts │ ├── preview.ts │ ├── random-case.ts │ ├── sequence.ts │ ├── slugify.ts │ ├── swap_quotes.ts │ ├── title-case.ts │ ├── types.ts │ └── utf8-conversion.ts ├── extension.ts ├── sidebar.ts └── test │ └── extension.test.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marclipovsky/vscode-string-manipulation/f78ed4a38fc7b1da46139aaedad1092233c69dad/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - feature/* 8 | pull_request: 9 | branches: 10 | - master 11 | - feature/* 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | strategy: 17 | matrix: 18 | os: [macos-latest, ubuntu-latest, windows-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | ref: ${{ github.ref }} 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | - name: Install dependencies 28 | run: npm install 29 | - name: Build package 30 | run: npm run build 31 | - name: Run headless test 32 | uses: coactions/setup-xvfb@v1 33 | with: 34 | run: npm test 35 | -------------------------------------------------------------------------------- /.github/workflows/publish-extension.yml: -------------------------------------------------------------------------------- 1 | name: "Publish to Marketplace" 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | types: [completed] 7 | branches: [master] 8 | 9 | jobs: 10 | cd: 11 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | steps: 16 | - name: Checkout to branch 17 | uses: actions/checkout@v4 18 | with: 19 | ref: ${{ github.ref }} 20 | 21 | - name: Setup node.js 22 | uses: actions/setup-node@v4 23 | 24 | - name: "Bump version" 25 | uses: "phips28/gh-action-bump-version@master" 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} 28 | with: 29 | minor-wording: "MINOR" 30 | major-wording: "MAJOR" 31 | patch-wording: "PATCH,FIX" 32 | rc-wording: "RELEASE" 33 | 34 | - name: Install packages 35 | run: npm ci 36 | 37 | - name: Calculate version 38 | id: calculateVersion 39 | run: | 40 | APP_VERSION=`cat package.json | jq ".version" -M | sed 's/\"//g'` 41 | echo "AppVersion=$APP_VERSION" >> $GITHUB_OUTPUT 42 | echo "app version = v$APP_VERSION" 43 | 44 | - name: Compile 45 | run: npm run build 46 | 47 | - name: Build VSIX package 48 | run: npm run package -- -o vscode-string-manipulation.v${{ steps.calculateVersion.outputs.AppVersion }}.vsix 49 | 50 | - name: Publish extension package 51 | env: 52 | VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }} 53 | run: npm run vsce -- publish -p $VSCE_TOKEN 54 | 55 | - uses: actions/upload-artifact@v4 56 | name: Upload artifact 57 | with: 58 | name: vscode-string-manipulation.v${{ steps.calculateVersion.outputs.AppVersion }}.vsix 59 | path: vscode-string-manipulation.v${{ steps.calculateVersion.outputs.AppVersion }}.vsix 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 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": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": [ 9 | "npm: watch:tsc", 10 | "npm: watch:esbuild" 11 | ], 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch:esbuild", 23 | "group": "build", 24 | "problemMatcher": "$esbuild-watch", 25 | "isBackground": true, 26 | "label": "npm: watch:esbuild", 27 | "presentation": { 28 | "group": "watch", 29 | "reveal": "never" 30 | } 31 | }, 32 | { 33 | "type": "npm", 34 | "script": "watch:tsc", 35 | "group": "build", 36 | "problemMatcher": "$tsc-watch", 37 | "isBackground": true, 38 | "label": "npm: watch:tsc", 39 | "presentation": { 40 | "group": "watch", 41 | "reveal": "never" 42 | } 43 | }, 44 | { 45 | "type": "npm", 46 | "script": "watch-tests", 47 | "problemMatcher": "$tsc-watch", 48 | "isBackground": true, 49 | "presentation": { 50 | "reveal": "never", 51 | "group": "watchers" 52 | }, 53 | "group": "build" 54 | }, 55 | { 56 | "label": "tasks: watch-tests", 57 | "dependsOn": [ 58 | "npm: watch", 59 | "npm: watch-tests" 60 | ], 61 | "problemMatcher": [] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | esbuild.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/eslint.config.mjs 12 | **/*.map 13 | **/*.ts 14 | **/.vscode-test.* 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.0 2 | - 1673eb3 Add slugify and fix #6,#7 3 | 4 | # 0.2.0 5 | - e497c8b Add screaming snake case - fixes #6 6 | 7 | # 0.1.0 8 | - 824e06e Add Chicago and AP style titleization. Fixes #2. 9 | - 7bc9db1 Manipulate strings without joining lines. Fixes #1. 10 | - 6e00b93 Adding .vscode dir for running local extension 11 | - c6bb1b8 Using yarn package manager 12 | - 2dab680 Update README.md 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [YEAR] [YOUR NAME] 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 | ![Node.js CI](https://github.com/marclipovsky/vscode-string-manipulation/workflows/Node.js%20CI/badge.svg) 2 | 3 | # String Manipulation for VSCode 4 | 5 | This extension provides string manipulation commands for any selected text as well 6 | as multiple selections. 7 | 8 | Current string functions available: 9 | 10 | 1. camelize - converts hyphenated strings to camelCase 11 | 1. capitalize - capitalizes the first character of each selection 12 | 1. classify - converts underscored text to PascalCase 13 | 1. chop - splits into groups provided n # of characters 14 | 1. clean - collapses multiple spaces into one 15 | 1. clean diacritics - removes diacritic marks from characters 16 | 1. dasherize - converts camelCase to kebab-case 17 | 1. decapitalize - lowercases the first character of each selection 18 | 1. humanize - converts text to human-readable form 19 | 1. reverse - reverses the characters in the selection 20 | 1. screaming snake - converts text to SCREAMING_SNAKE_CASE 21 | 1. sentence - transforms text to sentence case 22 | 1. slugify - converts text to a URL-friendly slug 23 | 1. snake - converts text to snake_case 24 | 1. swap case - inverts the case of each character 25 | 1. titleize - capitalizes the first letter of each word 26 | 1. titleize (AP Style) - capitalizes titles according to AP style 27 | 1. titleize (Chicago Style) - capitalizes titles according to Chicago style 28 | 1. truncate - trims string to n # of characters and appends ellipsis 29 | 1. prune - truncate but keeps ellipsis within character count provided 30 | 1. repeat - repeat selection n # of times 31 | 1. random case - randomly changes the case of characters 32 | 1. swap quotes - swaps between single and double quotes 33 | 1. utf8ToChar - converts Unicode escapes to characters 34 | 1. charToUtf8 - converts characters to Unicode escapes 35 | 36 | Number related functions: 37 | 38 | 1. increment - increases all numbers in the selection by 1 39 | 1. decrement - decreases all numbers in the selection by 1 40 | 1. duplicate and increment - duplicates selection and increments all numbers 41 | 1. duplicate and decrement - duplicates selection and decrements all numbers 42 | 1. sequence - replaces numbers with a sequence starting from the first number 43 | 1. incrementFloat - increases all floating point numbers in the selection by 1 44 | 1. decrementFloat - decreases all floating point numbers in the selection by 1 45 | 46 | Additional utility commands: 47 | 48 | 1. repeat last action - repeats the last string manipulation command that was executed 49 | 50 | ## Use 51 | 52 | To use these commands, press ⌘+p and enter any of the commands above while text is selected in your editor. 53 | 54 | ![String Manipulation Screencast](images/demo.gif) 55 | 56 | ## Preview Transformations 57 | 58 | The extension now includes a powerful preview feature that allows you to see how each transformation will affect your text before applying it. 59 | 60 | ### How to Use the Preview Feature 61 | 62 | 1. Select the text you want to transform 63 | 2. Right-click to open the context menu 64 | 3. Choose "Show Transformations with Preview" 65 | 4. Browse through the available transformations with instant previews 66 | 5. Select a transformation to apply it to your text 67 | 68 | This feature makes it easier to find the right transformation without trial and error. 69 | 70 | ![String Manipulation Preview Feature](images/preview-demo.gif) 71 | 72 | ## 🧪 Introducing Labs Features 73 | 74 | Introducing String Manipulation Labs 75 | 76 | We're excited to announce the launch of String Manipulation Labs—a collection of (really just one at this moment) experimental features designed to enhance and expand the capabilities of the String Manipulation extension. Labs features are disabled by default to ensure a stable experience with the core functionalities. 77 | 78 | ### 🚀 How to Enable Labs Features 79 | 80 | To try out the new Labs features, follow these simple steps: 81 | 82 | 1. Open VSCode Settings: 83 | • Press Ctrl + , (Windows/Linux) or Cmd + , (macOS), or navigate to File > Preferences > Settings. 84 | 2. Search for Labs Settings: 85 | • In the search bar, type stringManipulation.labs. 86 | 3. Enable Labs Features: 87 | • Toggle the String Manipulation Labs setting to On. 88 | 89 | ### 🛠️ We Value Your Feedback 90 | 91 | Since Labs features are experimental, your feedback is invaluable! Let us know your thoughts, report any issues, or suggest improvements to help us refine these tools. 92 | 93 | Thank you for using String Manipulation! 94 | Your support helps us build better tools for the community. 95 | 96 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 97 | -------------------------------------------------------------------------------- /content.txt: -------------------------------------------------------------------------------- 1 | moz-transform 2 | -moz-transform 3 | foo Bar 4 | foo bar 5 | ääkkönen 6 | foo Bar 7 | some_class_name 8 | MozTransform 9 | Foo Bar 10 | capitalize dash-CamelCase_underscore trim 11 | Abc 12 | Un éléphant à l'orée du bois 13 | HELLOworld 14 | This-is_snake case 15 | screaming-snake case 16 | my name is tristan 17 | this is a test 18 | The quick brown fox jumps over the lazy dog. 19 | Underscored-is-like snake-case 20 | aabbccdd 21 | aabbccdd 22 | aabbccddaabbccdd 23 | aabbccdd 24 | a1 b2 c3 4d 5e 6f 12x y23 34z45 25 | a1 b2 c3 4d 5e 26 | 6f 12x y23 34z45 27 | a-4 b-3 c-2 -1d 0e 28 | 6f 12x y23 34z45 29 | a1 b2 c3 4d 5e 6f 12x y23 34z45 30 | a1 b2 c3 4d 31 | 5e 6f 12x y23 34z45 32 | a-3 b-2 c-1 0d 33 | 1e 6f 12x y23 34z45 34 | a1 b2 c3 4d 5e 6f 12x y23 34z45 35 | 36 | a1 b2 c3 4d 5e 6f 12x y23 34z45 37 | 38 | a1 b2 c3 4d 5e 6f 12x y23 34z45 39 | a14 b2 c3 40 | 4d 5e 6f 7x y8 9z12 41 | \u0061\u0062\u0063\u4e2d\u6587\ud83d\udc96 42 | abc中文💖 43 | Hello, World! 44 | 12345!@#$% 45 | Test123! 46 | 'She said, "Hello"' 47 | "My name's Minalike" 48 | "He said, 'It's a trap!'" 49 | 'She exclaimed, \"Wow!\"' 50 | "'Double' and 'single' quotes" 51 | No quotes at all 52 | 'It's' 53 | "My name's %20 Minalike!" 54 | Lorem -1.234 ipsum 5.678 dolor sit amet, consectetur adipiscing. 55 | Sed do 9.876 eiusmod -4.321 tempor incididunt labore. 56 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | 3 | const production = process.argv.includes('--production'); 4 | const watch = process.argv.includes('--watch'); 5 | 6 | /** 7 | * @type {import('esbuild').Plugin} 8 | */ 9 | const esbuildProblemMatcherPlugin = { 10 | name: 'esbuild-problem-matcher', 11 | 12 | setup(build) { 13 | build.onStart(() => { 14 | console.log('[watch] build started'); 15 | }); 16 | build.onEnd((result) => { 17 | result.errors.forEach(({ text, location }) => { 18 | console.error(`✘ [ERROR] ${ text }`); 19 | console.error(` ${ location.file }:${ location.line }:${ location.column }:`); 20 | }); 21 | console.log('[watch] build finished'); 22 | }); 23 | }, 24 | }; 25 | 26 | async function main() { 27 | const ctx = await esbuild.context({ 28 | entryPoints: [ 29 | 'src/extension.ts' 30 | ], 31 | bundle: true, 32 | format: 'cjs', 33 | minify: production, 34 | sourcemap: !production, 35 | sourcesContent: false, 36 | platform: 'node', 37 | outfile: 'dist/extension.js', 38 | external: ['vscode'], 39 | logLevel: 'silent', 40 | plugins: [ 41 | /* add to the end of plugins array */ 42 | esbuildProblemMatcherPlugin, 43 | ], 44 | }); 45 | if (watch) { 46 | await ctx.watch(); 47 | } else { 48 | await ctx.rebuild(); 49 | await ctx.dispose(); 50 | } 51 | } 52 | 53 | main().catch(e => { 54 | console.error(e); 55 | process.exit(1); 56 | }); 57 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | }, { 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | 11 | languageOptions: { 12 | parser: tsParser, 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | }, 16 | 17 | rules: { 18 | "@typescript-eslint/naming-convention": ["warn", { 19 | selector: "import", 20 | format: ["camelCase", "PascalCase"], 21 | }], 22 | 23 | curly: "warn", 24 | eqeqeq: "warn", 25 | "no-throw-literal": "warn", 26 | semi: "warn", 27 | }, 28 | }]; 29 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marclipovsky/vscode-string-manipulation/f78ed4a38fc7b1da46139aaedad1092233c69dad/icon.png -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marclipovsky/vscode-string-manipulation/f78ed4a38fc7b1da46139aaedad1092233c69dad/images/demo.gif -------------------------------------------------------------------------------- /images/preview-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marclipovsky/vscode-string-manipulation/f78ed4a38fc7b1da46139aaedad1092233c69dad/images/preview-demo.gif -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": [ 6 | "es6" 7 | ] 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "string-manipulation", 3 | "displayName": "String Manipulation", 4 | "description": "Format strings to camelize, dasherize, etc.", 5 | "version": "0.7.34", 6 | "publisher": "marclipovsky", 7 | "repository": "https://github.com/marclipovsky/vscode-string-manipulation", 8 | "license": "MIT", 9 | "engines": { 10 | "vscode": "^1.93.0" 11 | }, 12 | "icon": "icon.png", 13 | "keywords": [ 14 | "underscore", 15 | "lodash", 16 | "case", 17 | "camelize", 18 | "dasherize" 19 | ], 20 | "main": "./dist/extension.js", 21 | "activationEvents": [ 22 | "onCommand:string-manipulation.*", 23 | "onView:stringManipulationSidebar" 24 | ], 25 | "contributes": { 26 | "configuration": { 27 | "type": "object", 28 | "title": "String Manipulation Configuration", 29 | "properties": { 30 | "stringManipulation.labs": { 31 | "type": "boolean", 32 | "default": false, 33 | "description": "Enable experimental String Manipulation Labs features." 34 | } 35 | } 36 | }, 37 | "views": { 38 | "explorer": [ 39 | { 40 | "type": "webview", 41 | "id": "stringManipulationSidebar", 42 | "name": "String Manipulation", 43 | "when": "config.stringManipulation.labs" 44 | } 45 | ] 46 | }, 47 | "commands": [ 48 | { 49 | "title": "Show Transformations with Preview", 50 | "category": "String Manipulation", 51 | "command": "string-manipulation.showTransformationsWithPreview" 52 | }, 53 | { 54 | "title": "Titleize", 55 | "category": "String Manipulation", 56 | "command": "string-manipulation.titleize" 57 | }, 58 | { 59 | "title": "Titleize (AP Style)", 60 | "category": "String Manipulation", 61 | "command": "string-manipulation.titleizeApStyle" 62 | }, 63 | { 64 | "title": "Titleize (Chicago Style)", 65 | "category": "String Manipulation", 66 | "command": "string-manipulation.titleizeChicagoStyle" 67 | }, 68 | { 69 | "title": "Camelize", 70 | "category": "String Manipulation", 71 | "command": "string-manipulation.camelize" 72 | }, 73 | { 74 | "title": "Chop", 75 | "category": "String Manipulation", 76 | "command": "string-manipulation.chop" 77 | }, 78 | { 79 | "title": "Clean", 80 | "category": "String Manipulation", 81 | "command": "string-manipulation.clean" 82 | }, 83 | { 84 | "title": "Clean diacritics", 85 | "category": "String Manipulation", 86 | "command": "string-manipulation.cleanDiacritics" 87 | }, 88 | { 89 | "title": "Classify", 90 | "category": "String Manipulation", 91 | "command": "string-manipulation.classify" 92 | }, 93 | { 94 | "title": "Underscored", 95 | "category": "String Manipulation", 96 | "command": "string-manipulation.underscored" 97 | }, 98 | { 99 | "title": "Dasherize", 100 | "category": "String Manipulation", 101 | "command": "string-manipulation.dasherize" 102 | }, 103 | { 104 | "title": "Humanize", 105 | "category": "String Manipulation", 106 | "command": "string-manipulation.humanize" 107 | }, 108 | { 109 | "title": "Slugify", 110 | "category": "String Manipulation", 111 | "command": "string-manipulation.slugify" 112 | }, 113 | { 114 | "title": "Snake", 115 | "category": "String Manipulation", 116 | "command": "string-manipulation.snake" 117 | }, 118 | { 119 | "title": "Screaming Snake", 120 | "category": "String Manipulation", 121 | "command": "string-manipulation.screamingSnake" 122 | }, 123 | { 124 | "title": "Reverse", 125 | "category": "String Manipulation", 126 | "command": "string-manipulation.reverse" 127 | }, 128 | { 129 | "title": "Swap Case", 130 | "category": "String Manipulation", 131 | "command": "string-manipulation.swapCase" 132 | }, 133 | { 134 | "title": "Decapitalize", 135 | "category": "String Manipulation", 136 | "command": "string-manipulation.decapitalize" 137 | }, 138 | { 139 | "title": "Capitalize", 140 | "category": "String Manipulation", 141 | "command": "string-manipulation.capitalize" 142 | }, 143 | { 144 | "title": "Sentence", 145 | "category": "String Manipulation", 146 | "command": "string-manipulation.sentence" 147 | }, 148 | { 149 | "title": "Truncate", 150 | "category": "String Manipulation", 151 | "command": "string-manipulation.truncate" 152 | }, 153 | { 154 | "title": "Prune", 155 | "category": "String Manipulation", 156 | "command": "string-manipulation.prune" 157 | }, 158 | { 159 | "title": "Repeat", 160 | "category": "String Manipulation", 161 | "command": "string-manipulation.repeat" 162 | }, 163 | { 164 | "title": "Increment all numbers", 165 | "category": "String Manipulation", 166 | "command": "string-manipulation.increment" 167 | }, 168 | { 169 | "title": "Decrement all numbers", 170 | "category": "String Manipulation", 171 | "command": "string-manipulation.decrement" 172 | }, 173 | { 174 | "title": "Duplicate and increment all numbers", 175 | "category": "String Manipulation", 176 | "command": "string-manipulation.duplicateAndIncrement" 177 | }, 178 | { 179 | "title": "Duplicate and decrement all numbers", 180 | "category": "String Manipulation", 181 | "command": "string-manipulation.duplicateAndDecrement" 182 | }, 183 | { 184 | "title": "Increment all floats", 185 | "category": "String Manipulation", 186 | "command": "string-manipulation.incrementFloat" 187 | }, 188 | { 189 | "title": "Decrement all floats", 190 | "category": "String Manipulation", 191 | "command": "string-manipulation.decrementFloat" 192 | }, 193 | { 194 | "title": "Sequence all numbers from first number", 195 | "category": "String Manipulation", 196 | "command": "string-manipulation.sequence" 197 | }, 198 | { 199 | "title": "Convert char to UTF8", 200 | "category": "String Manipulation", 201 | "command": "string-manipulation.charToUtf8" 202 | }, 203 | { 204 | "title": "Convert UTF8 to char", 205 | "category": "String Manipulation", 206 | "command": "string-manipulation.utf8ToChar" 207 | }, 208 | { 209 | "title": "Random Case", 210 | "category": "String Manipulation", 211 | "command": "string-manipulation.randomCase" 212 | }, 213 | { 214 | "title": "Swap Quotes", 215 | "category": "String Manipulation", 216 | "command": "string-manipulation.swapQuotes" 217 | } 218 | ], 219 | "submenus": [ 220 | { 221 | "id": "string-manipulation", 222 | "label": "String Manipulation" 223 | } 224 | ], 225 | "menus": { 226 | "editor/context": [ 227 | { 228 | "submenu": "string-manipulation", 229 | "group": "7_modification" 230 | }, 231 | { 232 | "command": "string-manipulation.showTransformationsWithPreview", 233 | "group": "7_modification", 234 | "when": "editorHasSelection" 235 | } 236 | ], 237 | "string-manipulation": [ 238 | { 239 | "command": "string-manipulation.titleize", 240 | "group": "7_modification" 241 | }, 242 | { 243 | "command": "string-manipulation.titleizeApStyle", 244 | "group": "7_modification" 245 | }, 246 | { 247 | "command": "string-manipulation.titleizeChicagoStyle", 248 | "group": "7_modification" 249 | }, 250 | { 251 | "command": "string-manipulation.camelize", 252 | "group": "7_modification" 253 | }, 254 | { 255 | "command": "string-manipulation.chop", 256 | "group": "7_modification" 257 | }, 258 | { 259 | "command": "string-manipulation.clean", 260 | "group": "7_modification" 261 | }, 262 | { 263 | "command": "string-manipulation.cleanDiacritics", 264 | "group": "7_modification" 265 | }, 266 | { 267 | "command": "string-manipulation.classify", 268 | "group": "7_modification" 269 | }, 270 | { 271 | "command": "string-manipulation.underscored", 272 | "group": "7_modification" 273 | }, 274 | { 275 | "command": "string-manipulation.dasherize", 276 | "group": "7_modification" 277 | }, 278 | { 279 | "command": "string-manipulation.humanize", 280 | "group": "7_modification" 281 | }, 282 | { 283 | "command": "string-manipulation.slugify", 284 | "group": "7_modification" 285 | }, 286 | { 287 | "command": "string-manipulation.snake", 288 | "group": "7_modification" 289 | }, 290 | { 291 | "command": "string-manipulation.screamingSnake", 292 | "group": "7_modification" 293 | }, 294 | { 295 | "command": "string-manipulation.reverse", 296 | "group": "7_modification" 297 | }, 298 | { 299 | "command": "string-manipulation.swapCase", 300 | "group": "7_modification" 301 | }, 302 | { 303 | "command": "string-manipulation.decapitalize", 304 | "group": "7_modification" 305 | }, 306 | { 307 | "command": "string-manipulation.capitalize", 308 | "group": "7_modification" 309 | }, 310 | { 311 | "command": "string-manipulation.sentence", 312 | "group": "7_modification" 313 | }, 314 | { 315 | "command": "string-manipulation.truncate", 316 | "group": "7_modification" 317 | }, 318 | { 319 | "command": "string-manipulation.prune", 320 | "group": "7_modification" 321 | }, 322 | { 323 | "command": "string-manipulation.repeat", 324 | "group": "7_modification" 325 | }, 326 | { 327 | "command": "string-manipulation.increment", 328 | "group": "7_modification" 329 | }, 330 | { 331 | "command": "string-manipulation.decrement", 332 | "group": "7_modification" 333 | }, 334 | { 335 | "command": "string-manipulation.duplicateAndIncrement", 336 | "group": "7_modification" 337 | }, 338 | { 339 | "command": "string-manipulation.duplicateAndDecrement", 340 | "group": "7_modification" 341 | }, 342 | { 343 | "command": "string-manipulation.incrementFloat", 344 | "group": "7_modification" 345 | }, 346 | { 347 | "command": "string-manipulation.decrementFloat", 348 | "group": "7_modification" 349 | }, 350 | { 351 | "command": "string-manipulation.sequence", 352 | "group": "7_modification" 353 | }, 354 | { 355 | "command": "string-manipulation.utf8ToChar", 356 | "group": "7_modification" 357 | }, 358 | { 359 | "command": "string-manipulation.charToUtf8", 360 | "group": "7_modification" 361 | }, 362 | { 363 | "command": "string-manipulation.randomCase", 364 | "group": "7_modification" 365 | }, 366 | { 367 | "command": "string-manipulation.swapQuotes", 368 | "group": "7_modification" 369 | } 370 | ] 371 | } 372 | }, 373 | "scripts": { 374 | "compile": "npm run check-types && npm run lint && node esbuild.js", 375 | "watch": "npm-run-all -p watch:*", 376 | "watch:esbuild": "node esbuild.js --watch", 377 | "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", 378 | "watch:test": "nodemon --exec 'npm run test' src/**/* content.txt", 379 | "build": "npm run check-types && npm run lint && node esbuild.js --production", 380 | "compile-tests": "tsc -p . --outDir out", 381 | "watch-tests": "tsc -p . -w --outDir out", 382 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 383 | "check-types": "tsc --noEmit", 384 | "lint": "eslint src", 385 | "test": "vscode-test", 386 | "package": "(rm -rf out || true) && mkdir out && cp package.json out && vsce package", 387 | "vsce": "vsce" 388 | }, 389 | "devDependencies": { 390 | "@types/glob": "^7.1.3", 391 | "@types/mocha": "^10.0.7", 392 | "@types/node": "20.x", 393 | "@types/underscore.string": "^0.0.41", 394 | "@types/vscode": "^1.93.0", 395 | "@typescript-eslint/eslint-plugin": "^8.3.0", 396 | "@typescript-eslint/parser": "^8.3.0", 397 | "@vscode/test-cli": "^0.0.10", 398 | "@vscode/test-electron": "^2.4.1", 399 | "esbuild": "^0.25.0", 400 | "eslint": "^9.9.1", 401 | "glob": "^7.1.5", 402 | "mocha": "^10.8.2", 403 | "npm-run-all": "^4.1.5", 404 | "sinon": "^9.2.4", 405 | "typescript": "^5.5.4", 406 | "vsce": "^2.15.0", 407 | "vscode-test": "^1.6.1" 408 | }, 409 | "dependencies": { 410 | "@sindresorhus/slugify": "^0.3.0", 411 | "ap-style-title-case": "^1.1.2", 412 | "chicago-capitalize": "^0.1.0", 413 | "nodemon": "^3.1.7", 414 | "underscore.string": "^3.3.5" 415 | }, 416 | "__metadata": { 417 | "id": "f458266d-2636-454c-86ba-1df8d80ed929", 418 | "publisherDisplayName": "marclipovsky", 419 | "publisherId": "0bb81b3d-47b5-4792-9d22-906e374145af", 420 | "isPreReleaseVersion": false 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/commands/default-functions.ts: -------------------------------------------------------------------------------- 1 | import * as underscore from "underscore.string"; 2 | import { CommandFunction } from "./types"; 3 | 4 | export const defaultFunction = 5 | (commandName: string, option?: any) => (str: string) => 6 | (underscore as any)[commandName](str, option); 7 | 8 | export const snake = (str: string) => 9 | underscore 10 | .underscored(str) 11 | .replace(/([A-Z])[^A-Z]/g, " $1") 12 | .replace(/[^a-z0-9]+/gi, " ") 13 | .trim() 14 | .replace(/\s/gi, "_"); 15 | 16 | export const screamingSnake: CommandFunction = (str: string) => 17 | snake(str).toUpperCase(); 18 | 19 | export const camelize: CommandFunction = (str: string) => 20 | underscore.camelize(/[a-z]/.test(str) ? str : str.toLowerCase()); 21 | 22 | // Default functions 23 | export const titleize: CommandFunction = defaultFunction("titleize"); 24 | export const classify: CommandFunction = defaultFunction("classify"); 25 | export const clean: CommandFunction = defaultFunction("clean"); 26 | export const cleanDiacritics: CommandFunction = 27 | defaultFunction("cleanDiacritics"); 28 | export const dasherize: CommandFunction = defaultFunction("dasherize"); 29 | export const humanize: CommandFunction = defaultFunction("humanize"); 30 | export const reverse: CommandFunction = defaultFunction("reverse"); 31 | export const decapitalize: CommandFunction = defaultFunction("decapitalize"); 32 | export const capitalize: CommandFunction = defaultFunction("capitalize"); 33 | export const sentence: CommandFunction = defaultFunction("capitalize", true); 34 | export const swapCase: CommandFunction = defaultFunction("swapCase"); 35 | 36 | // Functions with arguments 37 | export const chop: CommandFunction = (n: number) => defaultFunction("chop", n); 38 | export const truncate: CommandFunction = (n: number) => 39 | defaultFunction("truncate", n); 40 | export const prune: CommandFunction = (n: number) => (str: string) => 41 | str.slice(0, n - 3).trim() + "..."; 42 | export const repeat: CommandFunction = (n: number) => 43 | defaultFunction("repeat", n); 44 | -------------------------------------------------------------------------------- /src/commands/increment-decrement.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { CommandFunction } from "./types"; 3 | 4 | export const increment: CommandFunction = (str: string) => 5 | str.replace(/-?\d+/g, (n) => String(Number(n) + 1)); 6 | 7 | export const decrement: CommandFunction = (str: string) => 8 | str.replace(/-?\d+/g, (n) => String(Number(n) - 1)); 9 | 10 | export const incrementFloat: CommandFunction = (str: string) => { 11 | return str.replace(/-?\d+\.\d+/g, (n) => { 12 | let decimalPlaces = (n.split(".")[1] || "").length; 13 | let factor = Math.pow(10, decimalPlaces); 14 | return String((Number(n) * factor + 1) / factor); 15 | }); 16 | }; 17 | 18 | export const decrementFloat: CommandFunction = (str: string) => { 19 | return str.replace(/-?\d+\.\d+/g, (n) => { 20 | let decimalPlaces = (n.split(".")[1] || "").length; 21 | let factor = Math.pow(10, decimalPlaces); 22 | return String((Number(n) * factor - 1) / factor); 23 | }); 24 | }; 25 | 26 | // These functions are placeholders as the actual implementation is in the stringFunction 27 | // They're kept here for type consistency in the command registry 28 | export const duplicateAndIncrement: CommandFunction = () => ""; 29 | export const duplicateAndDecrement: CommandFunction = () => ""; 30 | 31 | // Helper function to handle duplicate and increment/decrement operations 32 | export function handleDuplicateAndIncrementDecrement( 33 | editor: vscode.TextEditor, 34 | selections: readonly vscode.Selection[], 35 | operation: (str: string) => string 36 | ): { 37 | selectionMap: { 38 | [key: number]: { selection: vscode.Selection; replaced: string }; 39 | }; 40 | replacedSelections: string[]; 41 | } { 42 | const selectionMap: { 43 | [key: number]: { selection: vscode.Selection; replaced: string }; 44 | } = {}; 45 | const replacedSelections: string[] = []; 46 | 47 | for (const [index, selection] of selections.entries()) { 48 | const text = editor.document.getText(selection); 49 | const replaced = text + operation(text); 50 | 51 | replacedSelections.push(replaced); 52 | selectionMap[index] = { selection, replaced }; 53 | } 54 | 55 | return { selectionMap, replacedSelections }; 56 | } 57 | 58 | // Helper function to update selections after duplicate operations 59 | export function updateSelectionsAfterDuplicate( 60 | editor: vscode.TextEditor, 61 | selectionMap: { 62 | [key: number]: { selection: vscode.Selection; replaced: string }; 63 | } 64 | ): vscode.Selection[] { 65 | return editor.selections.map((selection, index) => { 66 | const originalSelection = selectionMap[index].selection; 67 | const originalText = editor.document.getText(originalSelection); 68 | 69 | // Calculate the start position of the duplicated text 70 | const startPos = originalSelection.end; 71 | 72 | // Calculate the end position based on the original text length 73 | let endLine = startPos.line; 74 | let endChar = startPos.character + originalText.length; 75 | 76 | // Handle multi-line selections 77 | const lines = originalText.split("\n"); 78 | if (lines.length > 1) { 79 | endLine = startPos.line + lines.length - 1; 80 | // If multi-line, the end character should be the length of the last line 81 | endChar = lines[lines.length - 1].length; 82 | } 83 | 84 | const endPos = new vscode.Position(endLine, endChar); 85 | return new vscode.Selection(startPos, endPos); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { 3 | CommandRegistry, 4 | functionNamesWithArgument, 5 | numberFunctionNames, 6 | CommandFunction, 7 | } from "./types"; 8 | import * as defaultFunctions from "./default-functions"; 9 | import { 10 | increment, 11 | decrement, 12 | duplicateAndIncrement, 13 | duplicateAndDecrement, 14 | handleDuplicateAndIncrementDecrement, 15 | updateSelectionsAfterDuplicate, 16 | incrementFloat, 17 | decrementFloat, 18 | } from "./increment-decrement"; 19 | import { sequence } from "./sequence"; 20 | import { randomCase } from "./random-case"; 21 | import { utf8ToChar, charToUtf8 } from "./utf8-conversion"; 22 | import { titleizeApStyle, titleizeChicagoStyle } from "./title-case"; 23 | import { slugify } from "./slugify"; 24 | import { swapQuotes } from "./swap_quotes"; 25 | 26 | // Combine all commands into a single registry 27 | export const commandNameFunctionMap: CommandRegistry = { 28 | // Default functions 29 | titleize: defaultFunctions.titleize, 30 | chop: defaultFunctions.chop, 31 | classify: defaultFunctions.classify, 32 | clean: defaultFunctions.clean, 33 | cleanDiacritics: defaultFunctions.cleanDiacritics, 34 | underscored: defaultFunctions.snake, 35 | dasherize: defaultFunctions.dasherize, 36 | humanize: defaultFunctions.humanize, 37 | reverse: defaultFunctions.reverse, 38 | decapitalize: defaultFunctions.decapitalize, 39 | capitalize: defaultFunctions.capitalize, 40 | sentence: defaultFunctions.sentence, 41 | camelize: defaultFunctions.camelize, 42 | swapCase: defaultFunctions.swapCase, 43 | snake: defaultFunctions.snake, 44 | screamingSnake: defaultFunctions.screamingSnake, 45 | truncate: defaultFunctions.truncate, 46 | prune: defaultFunctions.prune, 47 | repeat: defaultFunctions.repeat, 48 | 49 | // Specialized functions 50 | increment, 51 | decrement, 52 | duplicateAndIncrement, 53 | duplicateAndDecrement, 54 | incrementFloat, 55 | decrementFloat, 56 | sequence, 57 | utf8ToChar, 58 | charToUtf8, 59 | randomCase, 60 | titleizeApStyle, 61 | titleizeChicagoStyle, 62 | slugify, 63 | swapQuotes, 64 | }; 65 | 66 | // Re-export types and constants 67 | export { 68 | functionNamesWithArgument, 69 | numberFunctionNames, 70 | CommandFunction, 71 | } from "./types"; 72 | 73 | // Main string function that applies the transformations 74 | export const stringFunction = async ( 75 | commandName: string, 76 | context: vscode.ExtensionContext, 77 | shouldApply = true 78 | ): Promise<{ replacedSelections: string[] } | undefined> => { 79 | const editor = vscode.window.activeTextEditor; 80 | if (!editor) { 81 | return; 82 | } 83 | 84 | let selectionMap: { 85 | [key: number]: { selection: vscode.Selection; replaced: string }; 86 | } = {}; 87 | 88 | let multiselectData = {}; 89 | 90 | let stringFunc: (str: string) => string; 91 | 92 | let replacedSelections: string[] = []; 93 | 94 | if (functionNamesWithArgument.includes(commandName)) { 95 | const valueStr = await vscode.window.showInputBox(); 96 | if (valueStr === undefined) { 97 | return; 98 | } 99 | const value = Number(valueStr); 100 | if (isNaN(value)) { 101 | vscode.window.showErrorMessage("Invalid number"); 102 | return; 103 | } 104 | stringFunc = (commandNameFunctionMap[commandName] as Function)(value); 105 | } else if (numberFunctionNames.includes(commandName)) { 106 | stringFunc = (str: string) => 107 | (commandNameFunctionMap[commandName] as Function)(str, multiselectData); 108 | } else { 109 | stringFunc = commandNameFunctionMap[commandName] as (str: string) => string; 110 | } 111 | 112 | if ( 113 | commandName === "duplicateAndIncrement" || 114 | commandName === "duplicateAndDecrement" 115 | ) { 116 | const operation = 117 | commandName === "duplicateAndIncrement" ? increment : decrement; 118 | 119 | const result = handleDuplicateAndIncrementDecrement( 120 | editor, 121 | editor.selections, 122 | operation as (str: string) => string 123 | ); 124 | selectionMap = result.selectionMap; 125 | replacedSelections = result.replacedSelections; 126 | } else { 127 | for (const [index, selection] of editor.selections.entries()) { 128 | const text = editor.document.getText(selection); 129 | const textParts = text.split("\n"); 130 | const replaced = textParts.map((part) => stringFunc(part)).join("\n"); 131 | replacedSelections.push(replaced); 132 | selectionMap[index] = { selection, replaced }; 133 | } 134 | } 135 | 136 | if (shouldApply) { 137 | await editor.edit((builder) => { 138 | Object.values(selectionMap).forEach(({ selection, replaced }) => { 139 | builder.replace(selection, replaced); 140 | }); 141 | }); 142 | 143 | // Set the selection to the duplicated part for duplicateAndIncrement and duplicateAndDecrement 144 | if ( 145 | commandName === "duplicateAndIncrement" || 146 | commandName === "duplicateAndDecrement" 147 | ) { 148 | editor.selections = updateSelectionsAfterDuplicate(editor, selectionMap); 149 | } 150 | 151 | context.globalState.update("lastAction", commandName); 152 | } 153 | 154 | return await Promise.resolve({ replacedSelections }); 155 | }; 156 | 157 | // Activation function 158 | export function activate(context: vscode.ExtensionContext) { 159 | context.globalState.setKeysForSync(["lastAction"]); 160 | 161 | context.subscriptions.push( 162 | vscode.commands.registerCommand( 163 | "string-manipulation.repeatLastAction", 164 | () => { 165 | const lastAction = context.globalState.get("lastAction"); 166 | if (lastAction) { 167 | return stringFunction(lastAction, context); 168 | } 169 | } 170 | ) 171 | ); 172 | 173 | Object.keys(commandNameFunctionMap).forEach((commandName) => { 174 | context.subscriptions.push( 175 | vscode.commands.registerCommand( 176 | `string-manipulation.${commandName}`, 177 | () => stringFunction(commandName, context) 178 | ) 179 | ); 180 | }); 181 | } 182 | -------------------------------------------------------------------------------- /src/commands/preview.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { commandNameFunctionMap } from "./index"; 3 | import { functionNamesWithArgument, numberFunctionNames } from "./types"; 4 | import { stringFunction } from "./index"; 5 | 6 | /** 7 | * Maximum length for preview text in context menu 8 | */ 9 | const MAX_PREVIEW_LENGTH = 30; 10 | 11 | /** 12 | * Truncates a string to a maximum length and adds ellipsis if needed 13 | */ 14 | export function truncateForPreview( 15 | text: string, 16 | maxLength = MAX_PREVIEW_LENGTH 17 | ): string { 18 | if (text.length <= maxLength) { 19 | return text; 20 | } 21 | return text.substring(0, maxLength - 3) + "..."; 22 | } 23 | 24 | /** 25 | * Gets a preview of the transformation for the selected text 26 | */ 27 | export function getTransformationPreview( 28 | commandName: string, 29 | selectedText: string 30 | ): string | undefined { 31 | if (!selectedText || selectedText.trim() === "") { 32 | return undefined; 33 | } 34 | 35 | // Skip preview for functions that require user input 36 | if (functionNamesWithArgument.includes(commandName)) { 37 | return undefined; 38 | } 39 | 40 | // Skip preview for special functions that don't work well with simple previews 41 | if ( 42 | commandName === "duplicateAndIncrement" || 43 | commandName === "duplicateAndDecrement" || 44 | commandName === "sequence" 45 | ) { 46 | return undefined; 47 | } 48 | 49 | try { 50 | let stringFunc: (str: string) => string; 51 | 52 | if (numberFunctionNames.includes(commandName)) { 53 | stringFunc = (str: string) => 54 | (commandNameFunctionMap[commandName] as Function)(str, {}); 55 | } else { 56 | stringFunc = commandNameFunctionMap[commandName] as ( 57 | str: string 58 | ) => string; 59 | } 60 | 61 | // Get the first line of text for preview 62 | const firstLine = selectedText.split("\n")[0]; 63 | const transformed = stringFunc(firstLine); 64 | 65 | return truncateForPreview(transformed); 66 | } catch (error) { 67 | console.error(`Error generating preview for ${commandName}:`, error); 68 | return undefined; 69 | } 70 | } 71 | 72 | // Extended QuickPickItem interface to include the command name 73 | interface TransformationQuickPickItem extends vscode.QuickPickItem { 74 | commandName: string; 75 | } 76 | 77 | /** 78 | * Shows a quick pick menu with previews of all transformations 79 | */ 80 | export async function showTransformationQuickPick( 81 | context: vscode.ExtensionContext 82 | ): Promise { 83 | const editor = vscode.window.activeTextEditor; 84 | if (!editor || editor.selections.length === 0) { 85 | vscode.window.showInformationMessage("No text selected"); 86 | return; 87 | } 88 | 89 | // Get the selected text 90 | const selection = editor.selection; 91 | const selectedText = editor.document.getText(selection); 92 | 93 | if (!selectedText || selectedText.trim() === "") { 94 | vscode.window.showInformationMessage("No text selected"); 95 | return; 96 | } 97 | 98 | // Create quick pick items with previews 99 | const quickPickItems: TransformationQuickPickItem[] = []; 100 | 101 | for (const commandName of Object.keys(commandNameFunctionMap)) { 102 | try { 103 | const preview = getTransformationPreview(commandName, selectedText); 104 | 105 | // Format the command name for display 106 | const displayName = commandName 107 | .replace(/([A-Z])/g, " $1") 108 | .replace(/^./, (str) => str.toUpperCase()); 109 | 110 | // Create the quick pick item 111 | const item: TransformationQuickPickItem = { 112 | label: displayName, 113 | description: preview ? `→ ${preview}` : undefined, 114 | detail: preview ? "Preview of transformation" : "No preview available", 115 | commandName: commandName, // Store the actual command name 116 | }; 117 | 118 | quickPickItems.push(item); 119 | } catch (error) { 120 | console.error( 121 | `Error creating quick pick item for ${commandName}:`, 122 | error 123 | ); 124 | } 125 | } 126 | 127 | // Show the quick pick 128 | const selectedItem = await vscode.window.showQuickPick(quickPickItems, { 129 | placeHolder: "Select a string transformation (with preview)", 130 | matchOnDescription: true, 131 | matchOnDetail: true, 132 | }); 133 | 134 | if (selectedItem) { 135 | try { 136 | // Use the stored command name directly 137 | await stringFunction(selectedItem.commandName, context); 138 | } catch (error: any) { 139 | console.error("Error applying transformation:", error); 140 | vscode.window.showErrorMessage( 141 | `Failed to apply transformation: ${error.message || String(error)}` 142 | ); 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * Registers the command to show the transformation quick pick 149 | */ 150 | export function registerPreviewCommand(context: vscode.ExtensionContext): void { 151 | const command = vscode.commands.registerCommand( 152 | "string-manipulation.showTransformationsWithPreview", 153 | () => showTransformationQuickPick(context) 154 | ); 155 | 156 | context.subscriptions.push(command); 157 | } 158 | -------------------------------------------------------------------------------- /src/commands/random-case.ts: -------------------------------------------------------------------------------- 1 | import { CommandFunction } from "./types"; 2 | 3 | export const randomCase: CommandFunction = (input: string): string => { 4 | let result = ""; 5 | for (const char of input) { 6 | if (Math.random() < 0.5) { 7 | result += char.toLowerCase(); 8 | } else { 9 | result += char.toUpperCase(); 10 | } 11 | } 12 | return result; 13 | }; 14 | -------------------------------------------------------------------------------- /src/commands/sequence.ts: -------------------------------------------------------------------------------- 1 | import { CommandFunction, MultiSelectData } from "./types"; 2 | 3 | export const sequence: CommandFunction = ( 4 | str: string, 5 | multiselectData: MultiSelectData = {} 6 | ) => { 7 | return str.replace(/-?\d+/g, (n) => { 8 | const isFirst = typeof multiselectData.offset !== "number"; 9 | multiselectData.offset = isFirst 10 | ? Number(n) 11 | : (multiselectData.offset || 0) + 1; 12 | return String(multiselectData.offset); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/commands/slugify.ts: -------------------------------------------------------------------------------- 1 | import { CommandFunction } from "./types"; 2 | const slugifyLib = require("@sindresorhus/slugify"); 3 | 4 | export const slugify: CommandFunction = slugifyLib; 5 | -------------------------------------------------------------------------------- /src/commands/swap_quotes.ts: -------------------------------------------------------------------------------- 1 | export const swapQuotes = (str: string): string => { 2 | const singleQuote = "'"; 3 | const doubleQuote = '"'; 4 | 5 | // Check if the string is at least two characters and starts and ends with the same quote 6 | if (str.length < 2) { 7 | return str; // Return as is if not properly quoted 8 | } 9 | 10 | const firstChar = str[0]; 11 | const lastChar = str[str.length - 1]; 12 | 13 | if ( 14 | (firstChar !== singleQuote && firstChar !== doubleQuote) || 15 | firstChar !== lastChar 16 | ) { 17 | // Not properly quoted, return as is 18 | return str; 19 | } 20 | 21 | const originalQuote = firstChar; 22 | const newQuote = originalQuote === singleQuote ? doubleQuote : singleQuote; 23 | let content = str.slice(1, -1); 24 | 25 | // Swap inner quotes 26 | content = content.replace(/['"]/g, (match, offset) => { 27 | // Determine if the quote is part of an apostrophe 28 | const prevChar = content[offset - 1]; 29 | const nextChar = content[offset + 1]; 30 | const isApostrophe = 31 | match === "'" && 32 | /[a-zA-Z]/.test(prevChar || "") && 33 | /[a-zA-Z]/.test(nextChar || ""); 34 | 35 | if (isApostrophe) { 36 | // Handle apostrophe based on the desired output 37 | if (newQuote === singleQuote) { 38 | // Escape apostrophe when outer quote is single quote 39 | return "\\'"; 40 | } else { 41 | // Keep apostrophe as is when outer quote is double quote 42 | return match; 43 | } 44 | } else { 45 | // Swap the quote 46 | return match === originalQuote ? newQuote : originalQuote; 47 | } 48 | }); 49 | 50 | // Return the new string with swapped quotes 51 | return newQuote + content + newQuote; 52 | }; 53 | -------------------------------------------------------------------------------- /src/commands/title-case.ts: -------------------------------------------------------------------------------- 1 | import { CommandFunction } from "./types"; 2 | const apStyleTitleCase = require("ap-style-title-case"); 3 | const chicagoStyleTitleCase = require("chicago-capitalize"); 4 | 5 | export const titleizeApStyle: CommandFunction = apStyleTitleCase; 6 | export const titleizeChicagoStyle: CommandFunction = chicagoStyleTitleCase; 7 | -------------------------------------------------------------------------------- /src/commands/types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export interface MultiSelectData { 4 | offset?: number; 5 | } 6 | 7 | export type StringFunction = ( 8 | str: string, 9 | multiselectData?: MultiSelectData 10 | ) => string; 11 | 12 | export type CommandFunction = 13 | | StringFunction 14 | | ((...args: any[]) => StringFunction); 15 | 16 | export interface CommandRegistry { 17 | [key: string]: CommandFunction; 18 | } 19 | 20 | export const numberFunctionNames = [ 21 | "increment", 22 | "decrement", 23 | "sequence", 24 | "duplicateAndIncrement", 25 | "duplicateAndDecrement", 26 | "incrementFloat", 27 | "decrementFloat", 28 | ]; 29 | 30 | export const functionNamesWithArgument = [ 31 | "chop", 32 | "truncate", 33 | "prune", 34 | "repeat", 35 | ]; 36 | -------------------------------------------------------------------------------- /src/commands/utf8-conversion.ts: -------------------------------------------------------------------------------- 1 | import { CommandFunction } from "./types"; 2 | 3 | export const utf8ToChar: CommandFunction = (str: string) => 4 | str 5 | .match(/\\u[\dA-Fa-f]{4}/g) 6 | ?.map((x) => x.slice(2)) 7 | .map((x) => String.fromCharCode(parseInt(x, 16))) 8 | .join("") || ""; 9 | 10 | export const charToUtf8: CommandFunction = (str: string) => 11 | str 12 | .split("") 13 | .map((x) => `\\u${x.charCodeAt(0).toString(16).padStart(4, "0")}`) 14 | .join(""); 15 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { StringManipulationSidebar } from "./sidebar"; 3 | import { activate as stringManipulationActivate } from "./commands/index"; 4 | import { registerPreviewCommand } from "./commands/preview"; 5 | 6 | export function activate(context: vscode.ExtensionContext) { 7 | stringManipulationActivate(context); 8 | 9 | // Register command to show transformations with previews 10 | registerPreviewCommand(context); 11 | 12 | const sidebarProvider = new StringManipulationSidebar(context); 13 | 14 | context.subscriptions.push( 15 | vscode.window.registerWebviewViewProvider( 16 | StringManipulationSidebar.viewType, 17 | sidebarProvider, 18 | { webviewOptions: { retainContextWhenHidden: true } } 19 | ) 20 | ); 21 | 22 | // Update the sidebar initially 23 | sidebarProvider.updateWebview(); 24 | } 25 | 26 | export function deactivate() {} 27 | -------------------------------------------------------------------------------- /src/sidebar.ts: -------------------------------------------------------------------------------- 1 | // src/StringManipulationSidebar.ts 2 | 3 | import * as vscode from "vscode"; 4 | import { 5 | commandNameFunctionMap, 6 | stringFunction, 7 | functionNamesWithArgument, 8 | } from "./commands"; 9 | 10 | export class StringManipulationSidebar implements vscode.WebviewViewProvider { 11 | public static readonly viewType = "stringManipulationSidebar"; 12 | 13 | private _view?: vscode.WebviewView; 14 | 15 | constructor(private readonly context: vscode.ExtensionContext) { 16 | vscode.window.onDidChangeActiveTextEditor( 17 | () => { 18 | this.updateWebview(); 19 | }, 20 | null, 21 | this.context.subscriptions 22 | ); 23 | vscode.workspace.onDidChangeTextDocument( 24 | () => { 25 | this.updateWebview(); 26 | }, 27 | null, 28 | this.context.subscriptions 29 | ); 30 | } 31 | 32 | resolveWebviewView( 33 | webviewView: vscode.WebviewView, 34 | context: vscode.WebviewViewResolveContext, 35 | _token: vscode.CancellationToken 36 | ) { 37 | this._view = webviewView; 38 | 39 | webviewView.webview.options = { 40 | // Allow scripts in the webview 41 | enableScripts: true, 42 | 43 | // Restrict the webview to only loading content from `out` directory 44 | localResourceRoots: [this.context.extensionUri], 45 | }; 46 | 47 | webviewView.webview.html = this.getHtmlForWebview(webviewView.webview); 48 | 49 | // Handle messages from the webview 50 | webviewView.webview.onDidReceiveMessage(async (message) => { 51 | switch (message.type) { 52 | case "applyCommand": 53 | const commandName = message.command; 54 | await this.applyCommand(commandName); 55 | break; 56 | } 57 | }); 58 | 59 | // Update the webview content when the selection changes 60 | vscode.window.onDidChangeTextEditorSelection( 61 | () => { 62 | this.updateWebview(); 63 | }, 64 | null, 65 | this.context.subscriptions 66 | ); 67 | } 68 | 69 | private getHtmlForWebview(webview: vscode.Webview): string { 70 | // You can use a more sophisticated HTML setup or a front-end framework 71 | return ` 72 | 73 | 74 | 75 | String Manipulation 76 | 108 | 109 | 110 |

Available Transformations

111 |
112 | 113 | 152 | 153 | `; 154 | } 155 | 156 | private async applyCommand(commandName: string) { 157 | const stringFunc = commandNameFunctionMap[commandName]; 158 | if (!stringFunc) { 159 | vscode.window.showErrorMessage(`Command "${commandName}" not found.`); 160 | return; 161 | } 162 | 163 | stringFunction(commandName, this.context, /* shouldApplyChanges */ true); 164 | 165 | vscode.window.showInformationMessage( 166 | `Applied "${commandName}" to selected text.` 167 | ); 168 | } 169 | 170 | public async updateWebview() { 171 | if (!this._view) { 172 | return; 173 | } 174 | 175 | const editor = vscode.window.activeTextEditor; 176 | if (!editor) { 177 | this._view.webview.postMessage({ type: "updateCommands", commands: [] }); 178 | return; 179 | } 180 | 181 | const selections = editor.selections; 182 | if (selections.length === 0) { 183 | this._view.webview.postMessage({ type: "updateCommands", commands: [] }); 184 | return; 185 | } 186 | 187 | // For simplicity, we'll use the first selection 188 | const selectedText = editor.document.getText(selections[0]); 189 | 190 | if (!selectedText) { 191 | this._view.webview.postMessage({ type: "updateCommands", commands: [] }); 192 | return; 193 | } 194 | 195 | // Apply all commands to the selected text 196 | const commands = await Promise.all( 197 | Object.keys(commandNameFunctionMap) 198 | .filter( 199 | (commandName) => !functionNamesWithArgument.includes(commandName) 200 | ) 201 | .map(async (cmdName) => { 202 | const { replacedSelections } = (await stringFunction( 203 | cmdName, 204 | this.context, 205 | /* shouldApplyChanges */ false 206 | )) as { replacedSelections: string[] }; 207 | 208 | let output = replacedSelections.join("
...
"); 209 | 210 | return { 211 | name: cmdName, 212 | output, 213 | originalText: selectedText, 214 | }; 215 | }) 216 | ); 217 | 218 | // Send the commands to the webview 219 | this._view.webview.postMessage({ type: "updateCommands", commands }); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { afterEach, beforeEach, suite, test } from "mocha"; 3 | import * as path from "path"; 4 | import * as vscode from "vscode"; 5 | import { CommandFunction, commandNameFunctionMap } from "../commands/index"; 6 | 7 | type StringTransformationTest = [ 8 | funcName: string, 9 | originalString: string, 10 | expectedString: string, 11 | options?: { 12 | multiselectData?: { 13 | offset: number; 14 | }; 15 | functionArg?: number; 16 | } 17 | ]; 18 | 19 | let editor: vscode.TextEditor; 20 | let document: vscode.TextDocument; 21 | const originalShowInputBox = vscode.window.showInputBox; 22 | const stripNewlines = (str: string) => str.replace(/[\n\r]/g, ""); 23 | 24 | suite("Extension Test Suite", () => { 25 | beforeEach(async () => { 26 | // Arrange: Open a text document before each test 27 | const uri = vscode.Uri.file(path.join(__dirname, "../../content.txt")); 28 | document = await vscode.workspace.openTextDocument(uri); 29 | editor = await vscode.window.showTextDocument(document); 30 | }); 31 | 32 | afterEach(async () => { 33 | await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); 34 | vscode.window.showInputBox = originalShowInputBox; 35 | }); 36 | 37 | let getTextForSelectionsByCommand = async ( 38 | commandName: string, 39 | ranges: { 40 | start: { line: number; character: number }; 41 | end: { line: number; character: number }; 42 | }[] 43 | ): Promise => { 44 | editor.selections = ranges.map((range) => { 45 | const startPos = new vscode.Position( 46 | range.start.line, 47 | range.start.character 48 | ); 49 | const endPos = new vscode.Position(range.end.line, range.end.character); 50 | return new vscode.Selection(startPos, endPos); 51 | }); 52 | await vscode.commands.executeCommand(commandName); 53 | return Promise.resolve( 54 | editor.selections.map((selection) => 55 | stripNewlines(editor.document.getText(selection)) 56 | ) 57 | ); 58 | }; 59 | 60 | suite("activation events", () => { 61 | const extension = vscode.extensions.getExtension( 62 | "marclipovsky.string-manipulation" 63 | )!; 64 | 65 | test("is not active by default", () => { 66 | assert.equal(false, extension.isActive); 67 | }); 68 | 69 | test("activates when running one of the commands", async () => { 70 | await vscode.commands.executeCommand("string-manipulation.titleize"); 71 | assert.equal(true, extension.isActive); 72 | }); 73 | }); 74 | 75 | suite("commands", () => { 76 | test("camelize converts hyphenated strings to camelCase", async () => { 77 | const [output1, output2] = await getTextForSelectionsByCommand( 78 | "string-manipulation.camelize", 79 | [ 80 | { start: { line: 0, character: 0 }, end: { line: 0, character: 14 } }, 81 | { start: { line: 1, character: 0 }, end: { line: 1, character: 15 } }, 82 | ] 83 | ); 84 | 85 | assert.strictEqual(output1 /* moz-transform */, "mozTransform"); 86 | assert.strictEqual(output2 /* -moz-transform */, "MozTransform"); 87 | }); 88 | 89 | test("capitalize capitalizes the first character of each selection", async () => { 90 | const [output1, output2] = await getTextForSelectionsByCommand( 91 | "string-manipulation.capitalize", 92 | [ 93 | { start: { line: 2, character: 0 }, end: { line: 2, character: 3 } }, 94 | { start: { line: 2, character: 4 }, end: { line: 2, character: 8 } }, 95 | ] 96 | ); 97 | 98 | assert.strictEqual(output1 /* foo */, "Foo"); 99 | assert.strictEqual(output2 /* Bar */, "Bar"); 100 | }); 101 | 102 | test("clean collapses multiple spaces into one", async () => { 103 | const [output] = await getTextForSelectionsByCommand( 104 | "string-manipulation.clean", 105 | [{ start: { line: 3, character: 0 }, end: { line: 3, character: 15 } }] 106 | ); 107 | 108 | assert.strictEqual(output /* foo bar */, "foo bar"); 109 | }); 110 | 111 | test("cleanDiacritics removes diacritic marks from characters", async () => { 112 | const [output] = await getTextForSelectionsByCommand( 113 | "string-manipulation.cleanDiacritics", 114 | [{ start: { line: 4, character: 0 }, end: { line: 4, character: 8 } }] 115 | ); 116 | 117 | assert.strictEqual(output /* ääkkönen */, "aakkonen"); 118 | }); 119 | 120 | test("sentence transforms text to sentence case", async () => { 121 | const [output] = await getTextForSelectionsByCommand( 122 | "string-manipulation.sentence", 123 | [{ start: { line: 5, character: 0 }, end: { line: 5, character: 7 } }] 124 | ); 125 | 126 | assert.strictEqual(output /* foo Bar */, "Foo bar"); 127 | }); 128 | 129 | test("classify converts underscored text to PascalCase", async () => { 130 | const [output] = await getTextForSelectionsByCommand( 131 | "string-manipulation.classify", 132 | [{ start: { line: 6, character: 0 }, end: { line: 6, character: 15 } }] 133 | ); 134 | 135 | assert.strictEqual(output /* some_class_name */, "SomeClassName"); 136 | }); 137 | 138 | test("dasherize converts camelCase to kebab-case", async () => { 139 | const [output] = await getTextForSelectionsByCommand( 140 | "string-manipulation.dasherize", 141 | [{ start: { line: 7, character: 0 }, end: { line: 7, character: 12 } }] 142 | ); 143 | 144 | assert.strictEqual(output /* MozTransform */, "-moz-transform"); 145 | }); 146 | 147 | test("decapitalize lowercases the first character of each selection", async () => { 148 | const [output] = await getTextForSelectionsByCommand( 149 | "string-manipulation.decapitalize", 150 | [{ start: { line: 8, character: 0 }, end: { line: 8, character: 7 } }] 151 | ); 152 | 153 | assert.strictEqual(output /* Foo Bar */, "foo Bar"); 154 | }); 155 | 156 | test("humanize converts text to human-readable form", async () => { 157 | const [output] = await getTextForSelectionsByCommand( 158 | "string-manipulation.humanize", 159 | [{ start: { line: 9, character: 0 }, end: { line: 9, character: 45 } }] 160 | ); 161 | 162 | assert.strictEqual( 163 | output /* capitalize dash-CamelCase_underscore trim */, 164 | "Capitalize dash camel case underscore trim" 165 | ); 166 | }); 167 | 168 | test("reverse reverses the characters in the selection", async () => { 169 | const [output] = await getTextForSelectionsByCommand( 170 | "string-manipulation.reverse", 171 | [{ start: { line: 10, character: 0 }, end: { line: 10, character: 3 } }] 172 | ); 173 | 174 | assert.strictEqual(output /* Abc */, "cbA"); 175 | }); 176 | 177 | test("slugify converts text to a URL-friendly slug", async () => { 178 | const [output] = await getTextForSelectionsByCommand( 179 | "string-manipulation.slugify", 180 | [ 181 | { 182 | start: { line: 11, character: 0 }, 183 | end: { line: 11, character: 28 }, 184 | }, 185 | ] 186 | ); 187 | 188 | assert.strictEqual( 189 | output /* Un éléphant à l'orée du bois */, 190 | "un-elephant-a-l-oree-du-bois" 191 | ); 192 | }); 193 | 194 | test("swapCase inverts the case of each character", async () => { 195 | const [output] = await getTextForSelectionsByCommand( 196 | "string-manipulation.swapCase", 197 | [ 198 | { 199 | start: { line: 12, character: 0 }, 200 | end: { line: 12, character: 10 }, 201 | }, 202 | ] 203 | ); 204 | 205 | assert.strictEqual(output /* HELLOworld */, "helloWORLD"); 206 | }); 207 | 208 | test("snake converts text to snake_case", async () => { 209 | const [output] = await getTextForSelectionsByCommand( 210 | "string-manipulation.snake", 211 | [ 212 | { 213 | start: { line: 13, character: 0 }, 214 | end: { line: 13, character: 18 }, 215 | }, 216 | ] 217 | ); 218 | 219 | assert.strictEqual(output /* This-is_snake case */, "this_is_snake_case"); 220 | }); 221 | 222 | test("screamingSnake converts text to SCREAMING_SNAKE_CASE", async () => { 223 | const [output] = await getTextForSelectionsByCommand( 224 | "string-manipulation.screamingSnake", 225 | [ 226 | { 227 | start: { line: 14, character: 0 }, 228 | end: { line: 14, character: 20 }, 229 | }, 230 | ] 231 | ); 232 | 233 | assert.strictEqual( 234 | output /* screaming-snake case */, 235 | "SCREAMING_SNAKE_CASE" 236 | ); 237 | }); 238 | 239 | test("titleize capitalizes the first letter of each word", async () => { 240 | const [output] = await getTextForSelectionsByCommand( 241 | "string-manipulation.titleize", 242 | [ 243 | { 244 | start: { line: 15, character: 0 }, 245 | end: { line: 15, character: 18 }, 246 | }, 247 | ] 248 | ); 249 | 250 | assert.strictEqual(output /* my name is tristan */, "My Name Is Tristan"); 251 | }); 252 | 253 | test("titleizeApStyle capitalizes titles according to AP style", async () => { 254 | const [output] = await getTextForSelectionsByCommand( 255 | "string-manipulation.titleizeApStyle", 256 | [ 257 | { 258 | start: { line: 16, character: 0 }, 259 | end: { line: 16, character: 14 }, 260 | }, 261 | ] 262 | ); 263 | 264 | assert.strictEqual(output /* this is a test */, "This Is a Test"); 265 | }); 266 | 267 | test("titleizeChicagoStyle capitalizes titles according to Chicago style", async () => { 268 | const [output] = await getTextForSelectionsByCommand( 269 | "string-manipulation.titleizeChicagoStyle", 270 | [ 271 | { 272 | start: { line: 17, character: 0 }, 273 | end: { line: 17, character: 44 }, 274 | }, 275 | ] 276 | ); 277 | 278 | assert.strictEqual( 279 | output /* The quick brown fox jumps over the lazy dog. */, 280 | "The Quick Brown Fox Jumps Over the Lazy Dog." 281 | ); 282 | }); 283 | 284 | suite("underscore", () => { 285 | test("converts text to underscore_case", async () => { 286 | const [output] = await getTextForSelectionsByCommand( 287 | "string-manipulation.underscored", 288 | [ 289 | { 290 | start: { line: 18, character: 0 }, 291 | end: { line: 18, character: 31 }, 292 | }, 293 | ] 294 | ); 295 | 296 | assert.strictEqual( 297 | output /* Underscored-is-like snake-case */, 298 | "underscored_is_like_snake_case" 299 | ); 300 | }); 301 | 302 | test("removes special characters and converts text to lowercase with underscores", async () => { 303 | const [output] = await getTextForSelectionsByCommand( 304 | "string-manipulation.underscored", 305 | [ 306 | { 307 | start: { line: 52, character: 0 }, 308 | end: { line: 52, character: 25 }, 309 | }, 310 | ] 311 | ); 312 | 313 | assert.strictEqual( 314 | output /* "My name's %20 Minalike!" */, 315 | "my_name_s_20_minalike" 316 | ); 317 | }); 318 | }); 319 | 320 | test("chop splits the string into chunks of given length", async () => { 321 | vscode.window.showInputBox = async () => { 322 | return "2"; 323 | }; 324 | 325 | const [output] = await getTextForSelectionsByCommand( 326 | "string-manipulation.chop", 327 | [ 328 | { 329 | start: { line: 19, character: 0 }, 330 | end: { line: 19, character: 8 }, 331 | }, 332 | ] 333 | ); 334 | 335 | assert.strictEqual(output /* aabbccdd */, "aa,bb,cc,dd"); 336 | }); 337 | 338 | test("truncate shortens the string to specified length and adds ellipsis", async () => { 339 | vscode.window.showInputBox = async () => { 340 | return "4"; 341 | }; 342 | 343 | const [output] = await getTextForSelectionsByCommand( 344 | "string-manipulation.truncate", 345 | [ 346 | { 347 | start: { line: 20, character: 0 }, 348 | end: { line: 20, character: 8 }, 349 | }, 350 | ] 351 | ); 352 | 353 | assert.strictEqual(output /* aabbccdd */, "aabb..."); 354 | }); 355 | 356 | test("prune truncates the string without breaking words", async () => { 357 | vscode.window.showInputBox = async () => { 358 | return "8"; 359 | }; 360 | 361 | const [output] = await getTextForSelectionsByCommand( 362 | "string-manipulation.prune", 363 | [ 364 | { 365 | start: { line: 21, character: 0 }, 366 | end: { line: 21, character: 16 }, 367 | }, 368 | ] 369 | ); 370 | 371 | assert.strictEqual(output /* aabbccddaabbccdd */, "aabbc..."); 372 | }); 373 | 374 | test("repeat duplicates the string given number of times", async () => { 375 | vscode.window.showInputBox = async () => { 376 | return "2"; 377 | }; 378 | 379 | const [output] = await getTextForSelectionsByCommand( 380 | "string-manipulation.repeat", 381 | [ 382 | { 383 | start: { line: 22, character: 0 }, 384 | end: { line: 22, character: 8 }, 385 | }, 386 | ] 387 | ); 388 | 389 | assert.strictEqual(output /* aabbccdd */, "aabbccddaabbccdd"); 390 | }); 391 | 392 | test("increment increases all numbers in the selection by 1", async () => { 393 | const [output1, output2, output3] = await getTextForSelectionsByCommand( 394 | "string-manipulation.increment", 395 | [ 396 | { 397 | start: { line: 23, character: 0 }, 398 | end: { line: 23, character: 31 }, 399 | }, 400 | { 401 | start: { line: 24, character: 0 }, 402 | end: { line: 25, character: 16 }, 403 | }, 404 | { 405 | start: { line: 26, character: 0 }, 406 | end: { line: 27, character: 16 }, 407 | }, 408 | ] 409 | ); 410 | 411 | assert.strictEqual( 412 | output1 /* a1 b2 c3 4d 5e 6f 12x y23 34z45 */, 413 | "a2 b3 c4 5d 6e 7f 13x y24 35z46" 414 | ); 415 | assert.strictEqual( 416 | output2 /* a1 b2 c3 4d 5e\n6f 12x y23 34z45 */, 417 | "a2 b3 c4 5d 6e7f 13x y24 35z46" 418 | ); 419 | assert.strictEqual( 420 | output3 /* a-4 b-3 c-2 -1d 0e\n6f 12x y23 34z45 */, 421 | "a-3 b-2 c-1 0d 1e7f 13x y24 35z46" 422 | ); 423 | }); 424 | 425 | test("decrement decreases all numbers in the selection by 1", async () => { 426 | const [output1, output2, output3] = await getTextForSelectionsByCommand( 427 | "string-manipulation.decrement", 428 | [ 429 | { 430 | start: { line: 28, character: 0 }, 431 | end: { line: 28, character: 31 }, 432 | }, 433 | { 434 | start: { line: 29, character: 0 }, 435 | end: { line: 30, character: 19 }, 436 | }, 437 | { 438 | start: { line: 31, character: 0 }, 439 | end: { line: 32, character: 19 }, 440 | }, 441 | ] 442 | ); 443 | 444 | assert.strictEqual( 445 | output1 /* a1 b2 c3 4d 5e 6f 12x y23 34z45 */, 446 | "a0 b1 c2 3d 4e 5f 11x y22 33z44" 447 | ); 448 | assert.strictEqual( 449 | output2 /* a1 b2 c3 4d\n5e 6f 12x y23 34z45 */, 450 | "a0 b1 c2 3d4e 5f 11x y22 33z44" 451 | ); 452 | assert.strictEqual( 453 | output3 /* a-3 b-2 c-1 0d\n1e 6f 12x y23 34z45 */, 454 | "a-4 b-3 c-2 -1d0e 5f 11x y22 33z44" 455 | ); 456 | }); 457 | 458 | test("incrementFloat increases all floats in the selection by 1", async () => { 459 | const [output] = await getTextForSelectionsByCommand( 460 | "string-manipulation.incrementFloat", 461 | [ 462 | { 463 | start: { line: 53, character: 0 }, 464 | end: { line: 53, character: 64 }, 465 | }, 466 | ] 467 | ); 468 | 469 | assert.strictEqual( 470 | output /* Lorem -1.234 ipsum 5.678 dolor sit amet, consectetur adipiscing. */, 471 | "Lorem -1.233 ipsum 5.679 dolor sit amet, consectetur adipiscing." 472 | ); 473 | }); 474 | 475 | test("decrementFloat decreases all floats in the selection by 1", async () => { 476 | const [output] = await getTextForSelectionsByCommand( 477 | "string-manipulation.decrementFloat", 478 | [ 479 | { 480 | start: { line: 54, character: 0 }, 481 | end: { line: 54, character: 53 }, 482 | }, 483 | ] 484 | ); 485 | 486 | assert.strictEqual( 487 | output /* Sed do 9.876 eiusmod -4.321 tempor incididunt labore. */, 488 | "Sed do 9.875 eiusmod -4.322 tempor incididunt labore." 489 | ); 490 | }); 491 | 492 | test("duplicateAndIncrement duplicates selection and increments numbers in duplicate", async () => { 493 | const [output] = await getTextForSelectionsByCommand( 494 | "string-manipulation.duplicateAndIncrement", 495 | [ 496 | { 497 | start: { line: 33, character: 0 }, 498 | end: { line: 34, character: 0 }, 499 | }, 500 | ] 501 | ); 502 | 503 | assert.strictEqual( 504 | output, 505 | /* a1 b2 c3 4d 5e 6f 12x y23 34z45\n */ "a2 b3 c4 5d 6e 7f 13x y24 35z46" 506 | ); 507 | }); 508 | 509 | test("duplicateAndDecrement duplicates selection and decrements numbers in duplicate", async () => { 510 | const [output] = await getTextForSelectionsByCommand( 511 | "string-manipulation.duplicateAndDecrement", 512 | [ 513 | { 514 | start: { line: 35, character: 0 }, 515 | end: { line: 36, character: 0 }, 516 | }, 517 | ] 518 | ); 519 | 520 | assert.strictEqual( 521 | output, 522 | /* a1 b2 c3 4d 5e 6f 12x y23 34z45\n */ "a0 b1 c2 3d 4e 5f 11x y22 33z44" 523 | ); 524 | }); 525 | 526 | test("sequence replaces numbers with a sequence starting from 1", async () => { 527 | const [output1, output2] = await getTextForSelectionsByCommand( 528 | "string-manipulation.sequence", 529 | [ 530 | { 531 | start: { line: 37, character: 0 }, 532 | end: { line: 37, character: 31 }, 533 | }, 534 | { 535 | start: { line: 38, character: 0 }, 536 | end: { line: 39, character: 20 }, 537 | }, 538 | ] 539 | ); 540 | 541 | assert.strictEqual( 542 | output1 /* a1 b2 c3 4d 5e 6f 12x y23 34z45 */, 543 | "a1 b2 c3 4d 5e 6f 7x y8 9z10" 544 | ); 545 | assert.strictEqual( 546 | output2 /* a14 b2 c3\n4d 5e 6f 7x y8 9z12 */, 547 | "a11 b12 c1314d 15e 16f 17x y18 19z20" 548 | ); 549 | }); 550 | 551 | test("utf8ToChar converts Unicode escapes to characters", async () => { 552 | const [output] = await getTextForSelectionsByCommand( 553 | "string-manipulation.utf8ToChar", 554 | [ 555 | { 556 | start: { line: 40, character: 0 }, 557 | end: { line: 40, character: 49 }, 558 | }, 559 | ] 560 | ); 561 | 562 | assert.strictEqual( 563 | output /* \u0061\u0062\u0063\u4e2d\u6587\ud83d\udc96 */, 564 | "abc中文💖" 565 | ); 566 | }); 567 | 568 | test("charToUtf8 converts characters to Unicode escapes", async () => { 569 | const [output] = await getTextForSelectionsByCommand( 570 | "string-manipulation.charToUtf8", 571 | [ 572 | { 573 | start: { line: 41, character: 0 }, 574 | end: { line: 41, character: 49 }, 575 | }, 576 | ] 577 | ); 578 | 579 | assert.strictEqual( 580 | output /* abc中文💖 */, 581 | "\\u0061\\u0062\\u0063\\u4e2d\\u6587\\ud83d\\udc96" 582 | ); 583 | }); 584 | 585 | suite("randomCase", () => { 586 | const input = "Hello, World!"; 587 | 588 | test("maintains string length and lowercased content", async () => { 589 | const [output] = await getTextForSelectionsByCommand( 590 | "string-manipulation.randomCase", 591 | [ 592 | { 593 | start: { line: 42, character: 0 }, 594 | end: { line: 42, character: 13 }, 595 | }, 596 | ] 597 | ); 598 | assert.equal(input.length, 13); 599 | assert.equal(input.toLowerCase() /* Hello, World! */, "hello, world!"); 600 | }); 601 | 602 | test("changes the case of at least one character (statistically)", async () => { 603 | const [output] = await getTextForSelectionsByCommand( 604 | "string-manipulation.randomCase", 605 | [ 606 | { 607 | start: { line: 42, character: 0 }, 608 | end: { line: 42, character: 13 }, 609 | }, 610 | ] 611 | ); 612 | let changed = false; 613 | for (let i = 0; i < 10; i++) { 614 | if ( 615 | output !== input && 616 | output.toLowerCase() === input.toLowerCase() 617 | ) { 618 | changed = true; 619 | break; 620 | } 621 | } 622 | assert.equal(changed, true); 623 | }); 624 | 625 | test("handles empty strings", async () => { 626 | const [output] = await getTextForSelectionsByCommand( 627 | "string-manipulation.randomCase", 628 | [ 629 | { 630 | start: { line: 42, character: 0 }, 631 | end: { line: 42, character: 0 }, 632 | }, 633 | ] 634 | ); 635 | assert.equal(output, ""); 636 | }); 637 | 638 | test("preserves non-alphabetic characters", async () => { 639 | const specialChars = "12345!@#$%"; 640 | const [output] = await getTextForSelectionsByCommand( 641 | "string-manipulation.randomCase", 642 | [ 643 | { 644 | start: { line: 43, character: 0 }, 645 | end: { line: 43, character: 10 }, 646 | }, 647 | ] 648 | ); 649 | assert.equal(output /* 12345!@#$% */, specialChars); 650 | }); 651 | 652 | test("handles strings with mixed content", async () => { 653 | const [output] = await getTextForSelectionsByCommand( 654 | "string-manipulation.randomCase", 655 | [ 656 | { 657 | start: { line: 44, character: 0 }, 658 | end: { line: 44, character: 8 }, 659 | }, 660 | ] 661 | ); 662 | assert.equal(output.length, 8); 663 | assert.notEqual(output.replace(/[^a-zA-Z]/g, ""), ""); 664 | }); 665 | }); 666 | 667 | suite("swapQuotes", () => { 668 | test("swaps outer single quotes to double quotes and inner double quotes to single quotes", async () => { 669 | const [output] = await getTextForSelectionsByCommand( 670 | "string-manipulation.swapQuotes", 671 | [ 672 | { 673 | start: { line: 45, character: 0 }, 674 | end: { line: 45, character: 19 }, 675 | }, 676 | ] 677 | ); 678 | assert.strictEqual( 679 | output /* 'She said, "Hello"' */, 680 | `"She said, 'Hello'"` 681 | ); 682 | }); 683 | 684 | test("swaps outer double quotes to single quotes and escapes inner apostrophe", async () => { 685 | const [output] = await getTextForSelectionsByCommand( 686 | "string-manipulation.swapQuotes", 687 | [ 688 | { 689 | start: { line: 46, character: 0 }, 690 | end: { line: 46, character: 20 }, 691 | }, 692 | ] 693 | ); 694 | assert.strictEqual( 695 | output /* "My name's Minalike" */, 696 | `'My name\\'s Minalike'` 697 | ); 698 | }); 699 | 700 | test("swaps outer double quotes to single quotes, inner single quotes to double quotes, and escapes apostrophe in contraction", async () => { 701 | const [output] = await getTextForSelectionsByCommand( 702 | "string-manipulation.swapQuotes", 703 | [ 704 | { 705 | start: { line: 47, character: 0 }, 706 | end: { line: 47, character: 25 }, 707 | }, 708 | ] 709 | ); 710 | assert.strictEqual( 711 | output /* "He said, 'It's a trap!'" */, 712 | `'He said, "It\\'s a trap!"'` 713 | ); 714 | }); 715 | 716 | test("swaps outer single quotes to double quotes and inner escaped double quotes to escaped single quotes", async () => { 717 | const [output] = await getTextForSelectionsByCommand( 718 | "string-manipulation.swapQuotes", 719 | [ 720 | { 721 | start: { line: 48, character: 0 }, 722 | end: { line: 48, character: 27 }, 723 | }, 724 | ] 725 | ); 726 | assert.strictEqual( 727 | output /* 'She exclaimed, \\"Wow!\\"' */, 728 | `"She exclaimed, \\'Wow!\\'"` 729 | ); 730 | }); 731 | 732 | test("swaps outer double quotes to single quotes and inner single quotes to double quotes", async () => { 733 | const [output] = await getTextForSelectionsByCommand( 734 | "string-manipulation.swapQuotes", 735 | [ 736 | { 737 | start: { line: 49, character: 0 }, 738 | end: { line: 49, character: 30 }, 739 | }, 740 | ] 741 | ); 742 | assert.strictEqual( 743 | output /* "'Double' and 'single' quotes" */, 744 | `'"Double" and "single" quotes'` 745 | ); 746 | }); 747 | 748 | test("returns input unchanged when string is not properly quoted", async () => { 749 | const [output] = await getTextForSelectionsByCommand( 750 | "string-manipulation.swapQuotes", 751 | [ 752 | { 753 | start: { line: 50, character: 0 }, 754 | end: { line: 50, character: 16 }, 755 | }, 756 | ] 757 | ); 758 | assert.strictEqual(output /* No quotes at all */, `No quotes at all`); 759 | }); 760 | 761 | test("swaps outer single quotes to double quotes, preserving inner apostrophe", async () => { 762 | const [output] = await getTextForSelectionsByCommand( 763 | "string-manipulation.swapQuotes", 764 | [ 765 | { 766 | start: { line: 51, character: 0 }, 767 | end: { line: 51, character: 6 }, 768 | }, 769 | ] 770 | ); 771 | assert.strictEqual(output /* 'It's' */, `"It's"`); 772 | }); 773 | }); 774 | }); 775 | }); 776 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "lib": [ 6 | "ES2022" 7 | ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | } 16 | } 17 | --------------------------------------------------------------------------------