diff --git a/.eslintrc.json b/.eslintrc.json index 86c86f3..88ae7a6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,30 +1,27 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "import", + "format": ["camelCase", "PascalCase"] + } ], - "rules": { - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "import", - "format": [ "camelCase", "PascalCase" ] - } - ], - "@typescript-eslint/semi": "warn", - "curly": "warn", - "eqeqeq": "warn", - "no-throw-literal": "warn", - "semi": "off" - }, - "ignorePatterns": [ - "out", - "dist", - "**/*.d.ts" - ] -} \ No newline at end of file + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/semi": "warn", + "curly": "error", + "eqeqeq": "error", + "no-throw-literal": "warn", + "no-console": "warn", + "prefer-const": "error", + "semi": "off" + }, + "ignorePatterns": ["out", "dist", "**/*.d.ts"] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3fa5cc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-build: + name: Lint & Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run compile + + test: + name: Test + runs-on: ubuntu-latest + needs: lint-and-build + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run compile + - name: Run tests (with virtual display) + run: xvfb-run -a npm test diff --git a/.vscode/launch.json b/.vscode/launch.json index aec515b..8ea99a7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,20 +3,26 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ - - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/extension.test" + ], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 5488000..1bb4436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,36 @@ All notable changes to the "format-switcher" extension will be documented in this file. -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +Format follows [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] -- Initial release +## [0.3.0] - 2026-03-08 + +### Added + +- **Two new formats**: `lower words` (space-separated lowercase) and `UPPER WORDS` (space-separated uppercase). +- **Cycle command** (`extension.formatSwitcher.cycleCase`) — automatically detects the current format and advances to the next in the cycle: `camelCase → snake_case → kebab-case → CONSTANT_CASE → Train-Case → lower words → UPPER WORDS → camelCase`. +- **Keyboard shortcut** `Ctrl+Shift+F` mapped to the cycle command (only active when editor has a selection, avoiding conflicts with "Find in Files"). +- **Multi-cursor support** — all active selections are converted simultaneously. +- `LICENSE` file (MIT). +- GitHub Actions CI pipeline (lint → build → test). + +### Changed + +- Replaced `lodash` runtime dependency (~4.7 MB) with native TypeScript implementations, reducing the packaged extension size by over 95%. +- Case conversion logic extracted to a standalone `caseConverters` module for testability. +- Comprehensive unit test suite (40+ assertions) replacing the placeholder boilerplate. + +### Fixed + +- Context menu submenu now only appears when text is actually selected (`when: editorHasSelection`). +- Internal `CaseType` literal `'Constant case'` corrected to `'upperSnakeCase'` for consistency. + +## [0.2.0] - 2024-01-01 + +### Added + +- Initial release with five case conversions: `camelCase`, `snake_case`, `CONSTANT_CASE`, `kebab-case`, `Train-Case`. +- Right-click context menu submenu "Change case". +- `lodash` used internally for text segmentation. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..396be7f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 EfeDeveloper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 631825b..2590c69 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,83 @@ logo

Format Switcher

- -## **Table of contents** +[![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/EdFerVIIIA.format-switcher)](https://marketplace.visualstudio.com/items?itemName=EdFerVIIIA.format-switcher) +[![CI](https://github.com/EfeDeveloper/format-switcher/actions/workflows/ci.yml/badge.svg)](https://github.com/EfeDeveloper/format-switcher/actions/workflows/ci.yml) -1. [Features](#features) + -2. [Usage](#usage) +## Table of contents -3. [Requirements](#requirements) +1. [Features](#features) +2. [Installation](#installation) +3. [Usage](#usage) +4. [Keyboard Shortcut](#keyboard-shortcut) +5. [Contributing](#contributing) +6. [License](#license) ## Features -`Format Switcher` is a simple extension that allows you to change the formatting of selected text in the editor. The extension adds a new menu item to the context menu. +`Format Switcher` is a VS Code extension that transforms selected text between seven naming-convention formats via the right-click context menu or a keyboard shortcut. -The extension supports the following cases: +Supported formats: -```plaintext -CamelCase +``` +camelCase snake_case CONSTANT_CASE kebab-case Train-Case +lower words +UPPER WORDS +``` + +- **Multi-cursor support** — all active selections are converted simultaneously. +- **Context menu** only appears when text is selected (no more silent no-ops). + +## Installation + +**From the Marketplace:** + +1. Open VS Code +2. Press `Ctrl+P` and run: `ext install EdFerVIIIA.format-switcher` +3. Or search **"Format Switcher"** in the Extensions view (`Ctrl+Shift+X`) + +**From a VSIX file:** + +``` +code --install-extension format-switcher-.vsix ``` ## Usage -![Format Switcher](./images/ExtExample.gif) +Select any text, right-click, and choose **Change case** → pick the desired format. -## Requirements +## Keyboard Shortcut -This extension uses the following libraries +Press **`Ctrl+Shift+F`** with text selected to **cycle** through formats in order: -```json -"lodash": "^4.17.21" ``` +camelCase → snake_case → kebab-case → CONSTANT_CASE → Train-Case → lower words → UPPER WORDS → camelCase → … +``` + +The shortcut only activates when the cursor is inside the editor with a selection, so it does not conflict with the default "Find in Files" shortcut. You can customise the keybinding any time via **File → Preferences → Keyboard Shortcuts**. + +## Contributing + +Bug reports and feature requests are welcome — please [open an issue](https://github.com/EfeDeveloper/format-switcher/issues). + +Pull requests are also welcome. To get started: + +```bash +git clone https://github.com/EfeDeveloper/format-switcher.git +cd format-switcher +npm install +npm test +``` + +Press **F5** in VS Code to launch the Extension Development Host, or use the **"Extension Tests"** launch config to debug tests. + +## License -✨**Enjoy!** +[MIT](LICENSE) © EfeDeveloper diff --git a/images/ExtExample.gif b/images/ExtExample.gif deleted file mode 100644 index 07ad797..0000000 Binary files a/images/ExtExample.gif and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 1492265..f4c8499 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,20 @@ { - "name": "text-case-changer", - "version": "1.0.0", + "name": "format-switcher", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "text-case-changer", - "version": "1.0.0", - "dependencies": { - "lodash": "^4.17.21" - }, + "name": "format-switcher", + "version": "0.2.0", + "license": "MIT", "devDependencies": { - "@types/lodash": "^4.14.202", "@types/mocha": "^10.0.6", "@types/node": "18.x", "@types/vscode": "^1.85.0", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", - "@vscode/test-cli": "^0.0.4", + "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.3.9", "eslint": "^8.56.0", "typescript": "^5.3.3" @@ -26,6 +23,13 @@ "vscode": "^1.85.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -203,6 +207,44 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -257,18 +299,19 @@ "node": ">= 6" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/lodash": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz", - "integrity": "sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==", - "dev": true - }, "node_modules/@types/mocha": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", @@ -493,13 +536,16 @@ "dev": true }, "node_modules/@vscode/test-cli": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.4.tgz", - "integrity": "sha512-Tx0tfbxeSb2Xlo+jpd+GJrNLgKQHobhRHrYvOipZRZQYWZ82sKiK02VY09UjU1Czc/YnZnqyAnjUfaVGl3h09w==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", + "integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==", "dev": true, + "license": "MIT", "dependencies": { "@types/mocha": "^10.0.2", + "c8": "^9.1.0", "chokidar": "^3.5.3", + "enhanced-resolve": "^5.15.0", "glob": "^10.3.10", "minimatch": "^9.0.3", "mocha": "^10.2.0", @@ -508,6 +554,9 @@ }, "bin": { "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=18" } }, "node_modules/@vscode/test-electron": { @@ -680,6 +729,42 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/c8": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", + "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/c8/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -828,6 +913,13 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -928,6 +1020,20 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -1351,6 +1457,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1375,6 +1488,13 @@ "he": "bin/he" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -1551,6 +1671,58 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -1657,11 +1829,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1693,6 +1860,22 @@ "node": "14 || >=16.14" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2479,6 +2662,81 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -2567,6 +2825,21 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index bb5a070..eee2680 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,24 @@ "engines": { "vscode": "^1.85.0" }, + "license": "MIT", + "keywords": [ + "case", + "camelCase", + "snake_case", + "kebab-case", + "formatter", + "text transform" + ], + "bugs": { + "url": "https://github.com/EfeDeveloper/format-switcher/issues" + }, + "homepage": "https://github.com/EfeDeveloper/format-switcher#readme", + "galleryBanner": { + "color": "#1e1e2e", + "theme": "dark" + }, "categories": [ - "Other", "Formatters" ], "activationEvents": [], @@ -21,53 +37,86 @@ "contributes": { "commands": [ { - "command": "extension.changeCase.camelCase", - "title": "CamelCase" + "command": "extension.formatSwitcher.lowerWords", + "title": "lower words" + }, + { + "command": "extension.formatSwitcher.upperWords", + "title": "UPPER WORDS" + }, + { + "command": "extension.formatSwitcher.camelCase", + "title": "camelCase" }, { - "command": "extension.changeCase.snakeCase", + "command": "extension.formatSwitcher.snakeCase", "title": "snake_case" }, { - "command": "extension.changeCase.upperSnakeCase", + "command": "extension.formatSwitcher.upperSnakeCase", "title": "CONSTANT_CASE" }, { - "command": "extension.changeCase.kebabCase", + "command": "extension.formatSwitcher.kebabCase", "title": "kebab-case" }, { - "command": "extension.changeCase.trainCase", + "command": "extension.formatSwitcher.trainCase", "title": "Train-Case" + }, + { + "command": "extension.formatSwitcher.cycleCase", + "title": "Cycle Case Format" + } + ], + "keybindings": [ + { + "command": "extension.formatSwitcher.cycleCase", + "key": "ctrl+shift+f", + "when": "editorTextFocus && editorHasSelection" } ], "submenus": [ { - "id": "changeCaseSubmenuId", - "label": "ChangeCase 🔠" + "id": "formatSwitcherSubmenuId", + "label": "Change case" } ], "menus": { "editor/context": [ { - "submenu": "changeCaseSubmenuId" + "submenu": "formatSwitcherSubmenuId", + "when": "editorHasSelection" } ], - "changeCaseSubmenuId": [ + "formatSwitcherSubmenuId": [ + { + "command": "extension.formatSwitcher.camelCase", + "group": "formats@1" + }, + { + "command": "extension.formatSwitcher.snakeCase", + "group": "formats@2" + }, { - "command": "extension.changeCase.camelCase" + "command": "extension.formatSwitcher.kebabCase", + "group": "formats@3" }, { - "command": "extension.changeCase.snakeCase" + "command": "extension.formatSwitcher.upperSnakeCase", + "group": "formats@4" }, { - "command": "extension.changeCase.upperSnakeCase" + "command": "extension.formatSwitcher.trainCase", + "group": "formats@5" }, { - "command": "extension.changeCase.kebabCase" + "command": "extension.formatSwitcher.lowerWords", + "group": "formats@6" }, { - "command": "extension.changeCase.trainCase" + "command": "extension.formatSwitcher.upperWords", + "group": "formats@7" } ] } @@ -81,18 +130,15 @@ "test": "vscode-test" }, "devDependencies": { - "@types/lodash": "^4.14.202", "@types/mocha": "^10.0.6", "@types/node": "18.x", "@types/vscode": "^1.85.0", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", - "@vscode/test-cli": "^0.0.4", + "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.3.9", "eslint": "^8.56.0", "typescript": "^5.3.3" }, - "dependencies": { - "lodash": "^4.17.21" - } + "dependencies": {} } \ No newline at end of file diff --git a/src/caseConverters.ts b/src/caseConverters.ts new file mode 100644 index 0000000..3422aaa --- /dev/null +++ b/src/caseConverters.ts @@ -0,0 +1,161 @@ +export type CaseType = + | 'lowerWords' + | 'upperWords' + | 'camelCase' + | 'snakeCase' + | 'upperSnakeCase' + | 'kebabCase' + | 'trainCase'; + +/** Ordered cycle: each Ctrl+Shift+F advances to the next format */ +export const CYCLE_ORDER: CaseType[] = [ + 'camelCase', + 'snakeCase', + 'kebabCase', + 'upperSnakeCase', + 'trainCase', + 'lowerWords', + 'upperWords', +]; + +/** + * Splits any cased string into an array of lowercase word tokens. + * Handles: camelCase, PascalCase, snake_case, UPPER_SNAKE, kebab-case, + * Train-Case, plain spaces, and digits adjacent to letters (e.g. hello2World). + */ +function segmentWords(text: string): string[] { + // Insert a separator before transitions: lowercase→uppercase, digit→letter, + // letter→digit, and consecutive uppercase followed by lowercase (ABCDef → ABC Def). + const spaced = text + .replace(/([a-z\d])([A-Z])/g, '$1 $2') + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') + .replace(/([a-zA-Z])(\d)/g, '$1 $2') + .replace(/(\d)([a-zA-Z])/g, '$1 $2'); + + return spaced + .split(/[\s\-_]+/) + .map((w) => w.trim()) + .filter((w) => w.length > 0); +} + +// ─── Converters ────────────────────────────────────────────────────────────── + +/** Splits into words and joins with spaces, all lowercase. */ +export function toLowerWords(text: string): string { + const words = segmentWords(text); + if (words.length === 0) { + return text; + } + return words.map((w) => w.toLowerCase()).join(' '); +} + +export function toCamelCase(text: string): string { + const words = segmentWords(text); + if (words.length === 0) { + return text; + } + return words + .map((w, i) => + i === 0 ? w.toLowerCase() : w[0].toUpperCase() + w.slice(1).toLowerCase(), + ) + .join(''); +} + +export function toSnakeCase(text: string): string { + const words = segmentWords(text); + if (words.length === 0) { + return text; + } + return words.map((w) => w.toLowerCase()).join('_'); +} + +export function toUpperSnakeCase(text: string): string { + const words = segmentWords(text); + if (words.length === 0) { + return text; + } + return words.map((w) => w.toUpperCase()).join('_'); +} + +export function toKebabCase(text: string): string { + const words = segmentWords(text); + if (words.length === 0) { + return text; + } + return words.map((w) => w.toLowerCase()).join('-'); +} + +export function toUpperWords(text: string): string { + const words = segmentWords(text); + if (words.length === 0) { + return text; + } + return words.map((w) => w.toUpperCase()).join(' '); +} + +export function toTrainCase(text: string): string { + const words = segmentWords(text); + if (words.length === 0) { + return text; + } + return words.map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase()).join('-'); +} + +// ─── Detection ─────────────────────────────────────────────────────────────── + +/** + * Detects the case format of a single token (no spaces). + * Returns null if the text doesn't clearly match any supported format, + * or if it's a single word with no separators (ambiguous). + */ +export function detectCase(text: string): CaseType | null { + const t = text.trim(); + if (!t) { + return null; + } + + // Space-separated formats (checked before no-space formats) + if (/^[a-z\d]+( [a-z\d]+)+$/.test(t)) { + return 'lowerWords'; + } + if (/^[A-Z\d]+( [A-Z\d]+)+$/.test(t)) { + return 'upperWords'; + } + + if (/\s/.test(t)) { + return null; + } + + if (/^[a-z][a-z\d]*([A-Z][a-z\d]*)+$/.test(t)) { + return 'camelCase'; + } + if (/^[A-Z][a-z\d]*([A-Z][a-z\d]*)+$/.test(t)) { + return 'camelCase'; + } // PascalCase → treat as camelCase for cycling + if (/^[a-z\d]+(_[a-z\d]+)+$/.test(t)) { + return 'snakeCase'; + } + if (/^[A-Z\d]+(_[A-Z\d]+)+$/.test(t)) { + return 'upperSnakeCase'; + } + if (/^[a-z\d]+(-[a-z\d]+)+$/.test(t)) { + return 'kebabCase'; + } + if (/^[A-Z][a-z\d]*(-[A-Z][a-z\d]*)+$/.test(t)) { + return 'trainCase'; + } + + return null; +} + +// ─── Converter map ─────────────────────────────────────────────────────────── + +export const CONVERTERS: Record string> = { + lowerWords: toLowerWords, + upperWords: toUpperWords, + camelCase: toCamelCase, + snakeCase: toSnakeCase, + upperSnakeCase: toUpperSnakeCase, + kebabCase: toKebabCase, + trainCase: toTrainCase, +}; diff --git a/src/extension.ts b/src/extension.ts index b25ba31..abb67b6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,63 +1,85 @@ import * as vscode from 'vscode'; -import lodash from 'lodash'; +import { + CaseType, + CYCLE_ORDER, + CONVERTERS, + detectCase, + toLowerWords, + toUpperWords, + toCamelCase, + toSnakeCase, + toUpperSnakeCase, + toKebabCase, + toTrainCase, +} from './caseConverters'; -type CaseType = 'camelCase' | 'snakeCase' | 'Constant case' | 'kebabCase' | 'trainCase'; - -export function activate(context: vscode.ExtensionContext) { +export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push( - vscode.commands.registerCommand('extension.changeCase.camelCase', () => { - changeCase('camelCase'); - }), - vscode.commands.registerCommand('extension.changeCase.snakeCase', () => { - changeCase('snakeCase'); - }), - vscode.commands.registerCommand('extension.changeCase.upperSnakeCase', () => { - changeCase('Constant case'); - }), - vscode.commands.registerCommand('extension.changeCase.kebabCase', () => { - changeCase('kebabCase'); - }), - vscode.commands.registerCommand('extension.changeCase.trainCase', () => { - changeCase('trainCase'); - }) + vscode.commands.registerCommand('extension.formatSwitcher.lowerWords', () => + applyCase((text) => toLowerWords(text)), + ), + vscode.commands.registerCommand('extension.formatSwitcher.upperWords', () => + applyCase((text) => toUpperWords(text)), + ), + vscode.commands.registerCommand('extension.formatSwitcher.camelCase', () => + applyCase((text) => toCamelCase(text)), + ), + vscode.commands.registerCommand('extension.formatSwitcher.snakeCase', () => + applyCase((text) => toSnakeCase(text)), + ), + vscode.commands.registerCommand('extension.formatSwitcher.upperSnakeCase', () => + applyCase((text) => toUpperSnakeCase(text)), + ), + vscode.commands.registerCommand('extension.formatSwitcher.kebabCase', () => + applyCase((text) => toKebabCase(text)), + ), + vscode.commands.registerCommand('extension.formatSwitcher.trainCase', () => + applyCase((text) => toTrainCase(text)), + ), + vscode.commands.registerCommand('extension.formatSwitcher.cycleCase', () => + applyCase((text) => cycleConvert(text)), + ), ); } -async function changeCase(caseType: CaseType) { +/** + * Applies a conversion function to every active selection in the editor. + * Supports multi-cursor. Skips empty selections with a status bar hint. + */ +async function applyCase(convert: (text: string) => string): Promise { const editor = vscode.window.activeTextEditor; if (!editor) { return; } - const selection = editor.selection; - const text = editor.document.getText(selection); - - let convertedText: string; + const selections = editor.selections; + const allEmpty = selections.every((s) => s.isEmpty); - switch (caseType) { - case 'camelCase': - convertedText = lodash.camelCase(text); - break; - case 'snakeCase': - convertedText = lodash.snakeCase(text); - break; - case 'Constant case': - convertedText = lodash.upperCase(text).replace(/\s/g, '_'); - break; - case 'kebabCase': - convertedText = lodash.kebabCase(text); - break; - case 'trainCase': - convertedText = lodash.startCase(lodash.camelCase(text)).replace(/\s/g, '-'); - break; - default: - convertedText = text; - break; + if (allEmpty) { + vscode.window.setStatusBarMessage('$(warning) Select text first', 3000); + return; } await editor.edit((editBuilder) => { - editBuilder.replace(selection, convertedText); + for (const selection of selections) { + if (selection.isEmpty) { + continue; + } + const text = editor.document.getText(selection); + editBuilder.replace(selection, convert(text)); + } }); } -export function deactivate() {} +/** + * Given some text, detects its current case format and returns the next + * one in the cycle order. Falls back to camelCase when undetected. + */ +function cycleConvert(text: string): string { + const current = detectCase(text); + const currentIndex = current === null ? -1 : CYCLE_ORDER.indexOf(current); + const nextType: CaseType = CYCLE_ORDER[(currentIndex + 1) % CYCLE_ORDER.length]; + return CONVERTERS[nextType](text); +} + +export function deactivate(): void {} diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 4ca0ab4..87e5061 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,15 +1,223 @@ import * as assert from 'assert'; +import { + toCamelCase, + toSnakeCase, + toUpperSnakeCase, + toKebabCase, + toTrainCase, + toLowerWords, + toUpperWords, + detectCase, + CYCLE_ORDER, + CONVERTERS, +} from '../caseConverters'; -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; +// ─── toLowerWords ────────────────────────────────────────────────────────────── +suite('toLowerWords', () => { + test('camelCase input', () => + assert.strictEqual(toLowerWords('helloWorld'), 'hello world')); + test('snake_case input', () => + assert.strictEqual(toLowerWords('hello_world'), 'hello world')); + test('UPPER_SNAKE input', () => + assert.strictEqual(toLowerWords('HELLO_WORLD'), 'hello world')); + test('kebab-case input', () => + assert.strictEqual(toLowerWords('hello-world'), 'hello world')); + test('Train-Case input', () => + assert.strictEqual(toLowerWords('Hello-World'), 'hello world')); + test('already lower words', () => + assert.strictEqual(toLowerWords('hello world'), 'hello world')); + test('single word', () => assert.strictEqual(toLowerWords('hello'), 'hello')); + test('empty string', () => assert.strictEqual(toLowerWords(''), '')); +}); + +// ─── toUpperWords ────────────────────────────────────────────────────────────── +suite('toUpperWords', () => { + test('plain words', () => + assert.strictEqual(toUpperWords('hello world'), 'HELLO WORLD')); + test('camelCase input', () => + assert.strictEqual(toUpperWords('helloWorld'), 'HELLO WORLD')); + test('snake_case input', () => + assert.strictEqual(toUpperWords('hello_world'), 'HELLO WORLD')); + test('kebab-case input', () => + assert.strictEqual(toUpperWords('hello-world'), 'HELLO WORLD')); + test('already UPPER WORDS', () => + assert.strictEqual(toUpperWords('HELLO WORLD'), 'HELLO WORLD')); + test('single word', () => assert.strictEqual(toUpperWords('hello'), 'HELLO')); + test('empty string', () => assert.strictEqual(toUpperWords(''), '')); +}); + +// ─── toCamelCase ────────────────────────────────────────────────────────────── +suite('toCamelCase', () => { + test('plain words', () => assert.strictEqual(toCamelCase('hello world'), 'helloWorld')); + test('snake_case input', () => + assert.strictEqual(toCamelCase('hello_world'), 'helloWorld')); + test('kebab-case input', () => + assert.strictEqual(toCamelCase('hello-world'), 'helloWorld')); + test('UPPER_SNAKE input', () => + assert.strictEqual(toCamelCase('HELLO_WORLD'), 'helloWorld')); + test('already camelCase', () => + assert.strictEqual(toCamelCase('helloWorld'), 'helloWorld')); + test('PascalCase input', () => + assert.strictEqual(toCamelCase('HelloWorld'), 'helloWorld')); + test('Train-Case input', () => + assert.strictEqual(toCamelCase('Hello-World'), 'helloWorld')); + test('single word', () => assert.strictEqual(toCamelCase('hello'), 'hello')); + test('empty string', () => assert.strictEqual(toCamelCase(''), '')); + test('numbers adjacent to letters', () => + assert.strictEqual(toCamelCase('hello2world'), 'hello2World')); +}); + +// ─── toSnakeCase ────────────────────────────────────────────────────────────── +suite('toSnakeCase', () => { + test('plain words', () => + assert.strictEqual(toSnakeCase('hello world'), 'hello_world')); + test('camelCase input', () => + assert.strictEqual(toSnakeCase('helloWorld'), 'hello_world')); + test('kebab-case input', () => + assert.strictEqual(toSnakeCase('hello-world'), 'hello_world')); + test('UPPER_SNAKE input', () => + assert.strictEqual(toSnakeCase('HELLO_WORLD'), 'hello_world')); + test('already snake_case', () => + assert.strictEqual(toSnakeCase('hello_world'), 'hello_world')); + test('single word', () => assert.strictEqual(toSnakeCase('hello'), 'hello')); + test('empty string', () => assert.strictEqual(toSnakeCase(''), '')); + test('numbers adjacent to letters', () => + assert.strictEqual(toSnakeCase('helloWorld2Test'), 'hello_world_2_test')); +}); + +// ─── toUpperSnakeCase ───────────────────────────────────────────────────────── +suite('toUpperSnakeCase', () => { + test('plain words', () => + assert.strictEqual(toUpperSnakeCase('hello world'), 'HELLO_WORLD')); + test('camelCase input', () => + assert.strictEqual(toUpperSnakeCase('helloWorld'), 'HELLO_WORLD')); + test('kebab-case input', () => + assert.strictEqual(toUpperSnakeCase('hello-world'), 'HELLO_WORLD')); + test('snake_case input', () => + assert.strictEqual(toUpperSnakeCase('hello_world'), 'HELLO_WORLD')); + test('already UPPER_SNAKE', () => + assert.strictEqual(toUpperSnakeCase('HELLO_WORLD'), 'HELLO_WORLD')); + test('single word', () => assert.strictEqual(toUpperSnakeCase('hello'), 'HELLO')); + test('empty string', () => assert.strictEqual(toUpperSnakeCase(''), '')); + test('numbers in identifier', () => + assert.strictEqual(toUpperSnakeCase('myVar2'), 'MY_VAR_2')); +}); + +// ─── toKebabCase ────────────────────────────────────────────────────────────── +suite('toKebabCase', () => { + test('plain words', () => + assert.strictEqual(toKebabCase('hello world'), 'hello-world')); + test('camelCase input', () => + assert.strictEqual(toKebabCase('helloWorld'), 'hello-world')); + test('UPPER_SNAKE input', () => + assert.strictEqual(toKebabCase('HELLO_WORLD'), 'hello-world')); + test('snake_case input', () => + assert.strictEqual(toKebabCase('hello_world'), 'hello-world')); + test('already kebab-case', () => + assert.strictEqual(toKebabCase('hello-world'), 'hello-world')); + test('single word', () => assert.strictEqual(toKebabCase('hello'), 'hello')); + test('empty string', () => assert.strictEqual(toKebabCase(''), '')); + test('Train-Case input', () => + assert.strictEqual(toKebabCase('Hello-World'), 'hello-world')); +}); + +// ─── toTrainCase ────────────────────────────────────────────────────────────── +suite('toTrainCase', () => { + test('plain words', () => + assert.strictEqual(toTrainCase('hello world'), 'Hello-World')); + test('camelCase input', () => + assert.strictEqual(toTrainCase('helloWorld'), 'Hello-World')); + test('snake_case input', () => + assert.strictEqual(toTrainCase('hello_world'), 'Hello-World')); + test('UPPER_SNAKE input', () => + assert.strictEqual(toTrainCase('HELLO_WORLD'), 'Hello-World')); + test('kebab-case input', () => + assert.strictEqual(toTrainCase('hello-world'), 'Hello-World')); + test('already Train-Case', () => + assert.strictEqual(toTrainCase('Hello-World'), 'Hello-World')); + test('single word', () => assert.strictEqual(toTrainCase('hello'), 'Hello')); + test('empty string', () => assert.strictEqual(toTrainCase(''), '')); +}); + +// ─── detectCase ─────────────────────────────────────────────────────────────── +suite('detectCase', () => { + test('detects camelCase', () => + assert.strictEqual(detectCase('helloWorld'), 'camelCase')); + test('detects PascalCase as camelCase', () => + assert.strictEqual(detectCase('HelloWorld'), 'camelCase')); + test('detects snake_case', () => + assert.strictEqual(detectCase('hello_world'), 'snakeCase')); + test('detects UPPER_SNAKE', () => + assert.strictEqual(detectCase('HELLO_WORLD'), 'upperSnakeCase')); + test('detects kebab-case', () => + assert.strictEqual(detectCase('hello-world'), 'kebabCase')); + test('detects Train-Case', () => + assert.strictEqual(detectCase('Hello-World'), 'trainCase')); + test('detects lower words', () => + assert.strictEqual(detectCase('hello world'), 'lowerWords')); + test('detects UPPER WORDS', () => + assert.strictEqual(detectCase('HELLO WORLD'), 'upperWords')); + test('single word returns null', () => assert.strictEqual(detectCase('hello'), null)); + test('empty string returns null', () => assert.strictEqual(detectCase(''), null)); +}); + +// ─── Cycle order ────────────────────────────────────────────────────────────── +suite('CYCLE_ORDER and CONVERTERS', () => { + test('CYCLE_ORDER has all 7 formats', () => assert.strictEqual(CYCLE_ORDER.length, 7)); + + test('cycling camelCase → snakeCase', () => { + const current = detectCase('helloWorld'); + assert.strictEqual(current, 'camelCase'); + const idx = CYCLE_ORDER.indexOf(current!); + const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length]; + assert.strictEqual(CONVERTERS[next]('helloWorld'), 'hello_world'); + }); + + test('cycling snakeCase → kebabCase', () => { + const current = detectCase('hello_world'); + assert.strictEqual(current, 'snakeCase'); + const idx = CYCLE_ORDER.indexOf(current!); + const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length]; + assert.strictEqual(CONVERTERS[next]('hello_world'), 'hello-world'); + }); + + test('cycling kebabCase → upperSnakeCase', () => { + const current = detectCase('hello-world'); + assert.strictEqual(current, 'kebabCase'); + const idx = CYCLE_ORDER.indexOf(current!); + const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length]; + assert.strictEqual(CONVERTERS[next]('hello-world'), 'HELLO_WORLD'); + }); + + test('cycling upperSnakeCase → trainCase', () => { + const current = detectCase('HELLO_WORLD'); + assert.strictEqual(current, 'upperSnakeCase'); + const idx = CYCLE_ORDER.indexOf(current!); + const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length]; + assert.strictEqual(CONVERTERS[next]('HELLO_WORLD'), 'Hello-World'); + }); + + test('cycling trainCase → lowerWords', () => { + const current = detectCase('Hello-World'); + assert.strictEqual(current, 'trainCase'); + const idx = CYCLE_ORDER.indexOf(current!); + const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length]; + assert.strictEqual(CONVERTERS[next]('Hello-World'), 'hello world'); + }); -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); + test('cycling lowerWords → upperWords', () => { + const current = detectCase('hello world'); + assert.strictEqual(current, 'lowerWords'); + const idx = CYCLE_ORDER.indexOf(current!); + const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length]; + assert.strictEqual(CONVERTERS[next]('hello world'), 'HELLO WORLD'); + }); - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); + test('cycling upperWords → camelCase (wraps around)', () => { + const current = detectCase('HELLO WORLD'); + assert.strictEqual(current, 'upperWords'); + const idx = CYCLE_ORDER.indexOf(current!); + const next = CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.length]; + assert.strictEqual(CONVERTERS[next]('HELLO WORLD'), 'helloWorld'); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 6954702..076b8b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,16 @@ { - "compilerOptions": { - "module": "Node16", - "target": "ES2022", - "outDir": "out", - "lib": [ - "ES2022" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "outDir": "out", + "lib": ["ES2022"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedParameters": true + }, + "exclude": ["out", "node_modules", ".vscode-test"] }