diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a430d6f..489161e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: node-version: 18.13.0 - name: Install dependencies run: npm install + - name: Build package + run: npm run build - name: Run headless test uses: GabrielBB/xvfb-action@v1.0 with: diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml index a796a31..0473c3d 100644 --- a/.github/workflows/publish-extension.yml +++ b/.github/workflows/publish-extension.yml @@ -13,10 +13,12 @@ jobs: steps: - name: Checkout to branch uses: actions/checkout@v4 + - name: Setup node.js uses: actions/setup-node@v4 with: node-version: 20 + - name: "Bump version" uses: 'phips28/gh-action-bump-version@master' env: @@ -36,8 +38,9 @@ jobs: APP_VERSION=`cat package.json | jq ".version" -M | sed 's/\"//g'` echo "AppVersion=$APP_VERSION" >> $GITHUB_OUTPUT echo "app version = v$APP_VERSION" + - name: Build VSIX package - run: npm run build -- -o vscode-string-manipulation.v${{ steps.calculateVersion.outputs.AppVersion }}.vsix + run: npm run package -- -o vscode-string-manipulation.v${{ steps.calculateVersion.outputs.AppVersion }}.vsix - name: Publish extension package env: diff --git a/.gitignore b/.gitignore index 414ab1e..fa780cb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules *.vsix .vscode-test .DS_Storedist +out/ diff --git a/extension.js b/extension.js deleted file mode 100644 index 18cfbe8..0000000 --- a/extension.js +++ /dev/null @@ -1,140 +0,0 @@ -const vscode = require("vscode"); -const _string = require("underscore.string"); -const apStyleTitleCase = require("ap-style-title-case"); -const chicagoStyleTitleCase = require("chicago-capitalize"); -const slugify = require("@sindresorhus/slugify"); -const defaultFunction = (commandName, option) => (str) => - _string[commandName](str, option); -const sequence = (str, multiselectData = {}) => { - return str.replace(/-?\d+/g, (n) => { - const isFirst = typeof multiselectData.offset !== "number"; - multiselectData.offset = isFirst ? Number(n) : multiselectData.offset + 1; - return multiselectData.offset; - }); -}; -const increment = (str) => str.replace(/-?\d+/g, (n) => Number(n) + 1); -const decrement = (str) => str.replace(/-?\d+/g, (n) => Number(n) - 1); - -const commandNameFunctionMap = { - titleize: defaultFunction("titleize"), - chop: (n) => defaultFunction("chop", n), - classify: defaultFunction("classify"), - clean: defaultFunction("clean"), - cleanDiacritics: defaultFunction("cleanDiacritics"), - underscored: defaultFunction("underscored"), - dasherize: defaultFunction("dasherize"), - humanize: defaultFunction("humanize"), - reverse: defaultFunction("reverse"), - decapitalize: defaultFunction("decapitalize"), - capitalize: defaultFunction("capitalize"), - sentence: defaultFunction("capitalize", true), - camelize: (str) => - _string.camelize(str.match(/[a-z]/) ? str : str.toLowerCase()), - slugify: slugify, - swapCase: defaultFunction("swapCase"), - snake: (str) => - _string - .underscored(str) - .replace(/([A-Z])[^A-Z]/g, " $1") - .replace(/[^a-z]+/gi, " ") - .trim() - .replace(/\s/gi, "_"), - screamingSnake: (str) => - _string - .underscored(str) - .replace(/([A-Z])[^A-Z]/g, " $1") - .replace(/[^a-z]+/gi, " ") - .trim() - .replace(/\s/gi, "_") - .toUpperCase(), - titleizeApStyle: apStyleTitleCase, - titleizeChicagoStyle: chicagoStyleTitleCase, - truncate: (n) => defaultFunction("truncate", n), - prune: (n) => (str) => str.slice(0, n - 3).trim() + "...", - repeat: (n) => defaultFunction("repeat", n), - increment, - decrement, - duplicateAndIncrement: (str) => str + increment(str), - duplicateAndDecrement: (str) => str + decrement(str), - sequence, - utf8ToChar: (str) => str.match(/\\u[\dA-Fa-f]{4}/g).map((x) => x.slice(2)).map((x) => String.fromCharCode(parseInt(x, 16))).join(""), - charToUtf8: (str) => str.split("").map((x) => `\\u${x.charCodeAt(0).toString(16).padStart(4, '0')}`).join(""), -}; -const numberFunctionNames = [ - "increment", - "decrement", - "sequence", - "duplicateAndIncrement", - "duplicateAndDecrement", -]; -const functionNamesWithArgument = ["chop", "truncate", "prune", "repeat"]; - -const stringFunction = async (commandName, context) => { - const editor = vscode.window.activeTextEditor; - const selectionMap = {}; - if (!editor) return; - - let multiselectData = {}; - editor.selections.forEach(async (selection, index) => { - const text = editor.document.getText(selection); - const textParts = text.split("\n"); - let stringFunc, replaced; - - if (functionNamesWithArgument.includes(commandName)) { - const value = await vscode.window.showInputBox(); - stringFunc = commandNameFunctionMap[commandName](value); - replaced = textParts - .reduce((prev, curr) => prev.push(stringFunc(curr)) && prev, []) - .join("\n"); - } else if (numberFunctionNames.includes(commandName)) { - replaced = commandNameFunctionMap[commandName](text, multiselectData); - } else { - stringFunc = commandNameFunctionMap[commandName]; - replaced = textParts - .reduce((prev, curr) => prev.push(stringFunc(curr)) && prev, []) - .join("\n"); - } - selectionMap[index] = {selection, replaced}; - }); - - editor.edit((builder) => { - Object.keys(selectionMap).forEach((index) => { - const {selection, replaced} = selectionMap[index]; - builder.replace(selection, replaced); - }); - }); - - context.globalState.update('lastAction', commandName); -}; - -const activate = (context) => { - context.globalState.setKeysForSync(['lastAction']); - - context.subscriptions.push( - vscode.commands.registerCommand( - `string-manipulation.repeatLastAction`, - () => { - const lastAction = context.globalState.get('lastAction'); - if (lastAction) { - return stringFunction(lastAction, context) - } - } - ) - ); - - Object.keys(commandNameFunctionMap).forEach((commandName) => { - context.subscriptions.push( - vscode.commands.registerCommand( - `string-manipulation.${commandName}`, - () => stringFunction(commandName, context) - ) - ); - }); -}; - -exports.activate = activate; - -module.exports = { - activate, - commandNameFunctionMap, -}; diff --git a/package-lock.json b/package-lock.json index 39489b4..ac10908 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@sindresorhus/slugify": "^0.3.0", + "@types/underscore.string": "^0.0.41", "ap-style-title-case": "^1.1.2", "chicago-capitalize": "^0.1.0", "underscore.string": "^3.3.5" @@ -24,7 +25,7 @@ "glob": "^7.1.5", "mocha": "^10.2.0", "sinon": "^9.2.4", - "typescript": "^3.9.9", + "typescript": "^3.9.10", "vsce": "^2.15.0", "vscode-test": "^1.6.1" }, @@ -148,6 +149,19 @@ "integrity": "sha512-xRCgeE0Q4pT5UZ189TJ3SpYuX/QGl6QIAOAIeDSbAVAd2gX1NxSZup4jNVK7cxIeP8KDSbJgcckun495isP1jQ==", "dev": true }, + "node_modules/@types/underscore": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz", + "integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==" + }, + "node_modules/@types/underscore.string": { + "version": "0.0.41", + "resolved": "https://registry.npmjs.org/@types/underscore.string/-/underscore.string-0.0.41.tgz", + "integrity": "sha512-d5bdU/o9hkoBsAO/o1hIyqkFBBXKgx5eZYPydgBFR1lw/NKylOYcFGjpmLIue4t/MBmV34J3YOwJ4euQrjpBKw==", + "dependencies": { + "@types/underscore": "*" + } + }, "node_modules/@types/vscode": { "version": "1.67.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.67.0.tgz", @@ -769,9 +783,9 @@ } }, "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -2639,9 +2653,9 @@ } }, "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dev": true, "dependencies": { "isarray": "0.0.1" @@ -2919,9 +2933,9 @@ "dev": true }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3410,9 +3424,9 @@ } }, "node_modules/typescript": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", - "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -3977,6 +3991,19 @@ "integrity": "sha512-xRCgeE0Q4pT5UZ189TJ3SpYuX/QGl6QIAOAIeDSbAVAd2gX1NxSZup4jNVK7cxIeP8KDSbJgcckun495isP1jQ==", "dev": true }, + "@types/underscore": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz", + "integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==" + }, + "@types/underscore.string": { + "version": "0.0.41", + "resolved": "https://registry.npmjs.org/@types/underscore.string/-/underscore.string-0.0.41.tgz", + "integrity": "sha512-d5bdU/o9hkoBsAO/o1hIyqkFBBXKgx5eZYPydgBFR1lw/NKylOYcFGjpmLIue4t/MBmV34J3YOwJ4euQrjpBKw==", + "requires": { + "@types/underscore": "*" + } + }, "@types/vscode": { "version": "1.67.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.67.0.tgz", @@ -4441,9 +4468,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -5829,9 +5856,9 @@ "dev": true }, "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dev": true, "requires": { "isarray": "0.0.1" @@ -6053,9 +6080,9 @@ "dev": true }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "serialize-javascript": { @@ -6431,9 +6458,9 @@ } }, "typescript": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", - "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", "dev": true }, "uc.micro": { diff --git a/package.json b/package.json index 36c4c5e..4b1c815 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "categories": [ "Other" ], - "main": "./extension", + "main": "./out/extension", "activationEvents": [ "onCommand:string-manipulation.titleize", "onCommand:string-manipulation.titleizeApStyle", @@ -333,8 +333,10 @@ }, "scripts": { "test": "node ./test/runTest.js", - "build": "(rm -rf out || true) && mkdir out && cp package.json out && vsce package", - "vsce": "vsce" + "build": "tsc", + "package": "(rm -rf out || true) && mkdir out && cp package.json out && vsce package", + "vsce": "vsce", + "debug": "code --extensionDevelopmentPath=./" }, "devDependencies": { "@types/glob": "^7.1.3", @@ -346,12 +348,13 @@ "glob": "^7.1.5", "mocha": "^10.2.0", "sinon": "^9.2.4", - "typescript": "^3.9.9", + "typescript": "^3.9.10", "vsce": "^2.15.0", "vscode-test": "^1.6.1" }, "dependencies": { "@sindresorhus/slugify": "^0.3.0", + "@types/underscore.string": "^0.0.41", "ap-style-title-case": "^1.1.2", "chicago-capitalize": "^0.1.0", "underscore.string": "^3.3.5" diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..b5ac567 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,174 @@ +import * as vscode from "vscode"; +import * as _string from "underscore.string"; +const apStyleTitleCase = require("ap-style-title-case"); +const chicagoStyleTitleCase = require("chicago-capitalize"); +const slugify = require("@sindresorhus/slugify"); + +interface MultiSelectData { + offset?: number; +} + +const defaultFunction = (commandName: string, option?: any) => (str: string) => + (_string as any)[commandName](str, option); + +const sequence = (str: string, multiselectData: MultiSelectData = {}) => { + return str.replace(/-?\d+/g, (n) => { + const isFirst = typeof multiselectData.offset !== "number"; + multiselectData.offset = isFirst + ? Number(n) + : (multiselectData.offset || 0) + 1; + return String(multiselectData.offset); + }); +}; + +const increment = (str: string) => + str.replace(/-?\d+/g, (n) => String(Number(n) + 1)); + +const decrement = (str: string) => + str.replace(/-?\d+/g, (n) => String(Number(n) - 1)); + +type StringFunction = (str: string) => string; +type CommandFunction = StringFunction | ((...args: any[]) => StringFunction); + +const commandNameFunctionMap: { [key: string]: CommandFunction } = { + titleize: defaultFunction("titleize"), + chop: (n: number) => defaultFunction("chop", n), + classify: defaultFunction("classify"), + clean: defaultFunction("clean"), + cleanDiacritics: defaultFunction("cleanDiacritics"), + underscored: defaultFunction("underscored"), + dasherize: defaultFunction("dasherize"), + humanize: defaultFunction("humanize"), + reverse: defaultFunction("reverse"), + decapitalize: defaultFunction("decapitalize"), + capitalize: defaultFunction("capitalize"), + sentence: defaultFunction("capitalize", true), + camelize: (str: string) => + _string.camelize(/[a-z]/.test(str) ? str : str.toLowerCase()), + slugify: slugify, + swapCase: defaultFunction("swapCase"), + snake: (str: string) => + _string + .underscored(str) + .replace(/([A-Z])[^A-Z]/g, " $1") + .replace(/[^a-z]+/gi, " ") + .trim() + .replace(/\s/gi, "_"), + screamingSnake: (str: string) => + _string + .underscored(str) + .replace(/([A-Z])[^A-Z]/g, " $1") + .replace(/[^a-z]+/gi, " ") + .trim() + .replace(/\s/gi, "_") + .toUpperCase(), + titleizeApStyle: apStyleTitleCase, + titleizeChicagoStyle: chicagoStyleTitleCase, + truncate: (n: number) => defaultFunction("truncate", n), + prune: (n: number) => (str: string) => str.slice(0, n - 3).trim() + "...", + repeat: (n: number) => defaultFunction("repeat", n), + increment, + decrement, + duplicateAndIncrement: (str: string) => str + increment(str), + duplicateAndDecrement: (str: string) => str + decrement(str), + sequence, + utf8ToChar: (str: string) => + str + .match(/\\u[\dA-Fa-f]{4}/g) + ?.map((x) => x.slice(2)) + .map((x) => String.fromCharCode(parseInt(x, 16))) + .join("") || "", + charToUtf8: (str: string) => + str + .split("") + .map((x) => `\\u${x.charCodeAt(0).toString(16).padStart(4, "0")}`) + .join(""), +}; + +const numberFunctionNames = [ + "increment", + "decrement", + "sequence", + "duplicateAndIncrement", + "duplicateAndDecrement", +]; + +const functionNamesWithArgument = ["chop", "truncate", "prune", "repeat"]; + +const stringFunction = async ( + commandName: string, + context: vscode.ExtensionContext +) => { + const editor = vscode.window.activeTextEditor; + if (!editor) return; + + const selectionMap: { + [key: number]: { selection: vscode.Selection; replaced: string }; + } = {}; + + let multiselectData: MultiSelectData = {}; + + let stringFunc: (str: string) => string; + + if (functionNamesWithArgument.includes(commandName)) { + const valueStr = await vscode.window.showInputBox(); + if (valueStr === undefined) { + return; + } + const value = Number(valueStr); + if (isNaN(value)) { + vscode.window.showErrorMessage("Invalid number"); + return; + } + stringFunc = (commandNameFunctionMap[commandName] as Function)(value); + } else if (numberFunctionNames.includes(commandName)) { + stringFunc = (str: string) => + (commandNameFunctionMap[commandName] as Function)(str, multiselectData); + } else { + stringFunc = commandNameFunctionMap[commandName] as StringFunction; + } + + for (const [index, selection] of editor.selections.entries()) { + const text = editor.document.getText(selection); + const textParts = text.split("\n"); + const replaced = textParts.map((part) => stringFunc(part)).join("\n"); + selectionMap[index] = { selection, replaced }; + } + + await editor.edit((builder) => { + Object.values(selectionMap).forEach(({ selection, replaced }) => { + builder.replace(selection, replaced); + }); + }); + + context.globalState.update("lastAction", commandName); +}; + +export function activate(context: vscode.ExtensionContext) { + context.globalState.setKeysForSync(["lastAction"]); + + context.subscriptions.push( + vscode.commands.registerCommand( + "string-manipulation.repeatLastAction", + async () => { + const lastAction = context.globalState.get("lastAction"); + if (lastAction) { + await stringFunction(lastAction, context); + } + } + ) + ); + + Object.keys(commandNameFunctionMap).forEach((commandName) => { + context.subscriptions.push( + vscode.commands.registerCommand( + `string-manipulation.${commandName}`, + async () => { + await stringFunction(commandName, context); + } + ) + ); + }); +} + +export { commandNameFunctionMap }; diff --git a/test/suite/extension.test.js b/test/suite/extension.test.js index 2c2c46e..0243866 100644 --- a/test/suite/extension.test.js +++ b/test/suite/extension.test.js @@ -4,7 +4,7 @@ const { test, suite } = require("mocha"); // You can import and use all API from the 'vscode' module // as well as import your extension to test it const vscode = require("vscode"); -const myExtension = require("../../extension"); +const myExtension = require("../../out/extension"); suite("Extension Test Suite", () => { vscode.window.showInformationMessage("Start all tests."); diff --git a/tsconfig.json b/tsconfig.json index 1e2ee3c..d552b2c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,25 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2019", "module": "commonjs", + "lib": [ + "ES2019" + ], + "outDir": "out", + "rootDir": "src", + "strict": true, "sourceMap": true, - "allowJs": true - } + "noImplicitAny": true, + "moduleResolution": "node", + "esModuleInterop": true, + "experimentalDecorators": true, + "skipLibCheck": true + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + ".vscode-test" + ] }