Skip to content

Commit

Permalink
Merge pull request #56 from marclipovsky/feature/sidebar-preview
Browse files Browse the repository at this point in the history
New feature: Enable sidebar that preview all command outputs before applying commands.
  • Loading branch information
marclipovsky authored Oct 13, 2024
2 parents ff1b7dc + e1c99e8 commit 1b1d6ca
Show file tree
Hide file tree
Showing 5 changed files with 518 additions and 256 deletions.
51 changes: 23 additions & 28 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,31 @@
],
"main": "./dist/extension.js",
"activationEvents": [
"onCommand:string-manipulation.titleize",
"onCommand:string-manipulation.titleizeApStyle",
"onCommand:string-manipulation.titleizeChicagoStyle",
"onCommand:string-manipulation.camelize",
"onCommand:string-manipulation.chop",
"onCommand:string-manipulation.clean",
"onCommand:string-manipulation.cleanDiacritics",
"onCommand:string-manipulation.classify",
"onCommand:string-manipulation.underscored",
"onCommand:string-manipulation.dasherize",
"onCommand:string-manipulation.snake",
"onCommand:string-manipulation.screamingSnake",
"onCommand:string-manipulation.humanize",
"onCommand:string-manipulation.slugify",
"onCommand:string-manipulation.reverse",
"onCommand:string-manipulation.swapCase",
"onCommand:string-manipulation.decapitalize",
"onCommand:string-manipulation.capitalize",
"onCommand:string-manipulation.sentence",
"onCommand:string-manipulation.prune",
"onCommand:string-manipulation.truncate",
"onCommand:string-manipulation.repeat",
"onCommand:string-manipulation.increment",
"onCommand:string-manipulation.decrement",
"onCommand:string-manipulation.duplicateAndIncrement",
"onCommand:string-manipulation.duplicateAndDecrement",
"onCommand:string-manipulation.sequence",
"onCommand:string-manipulation.randomCase"
"onCommand:string-manipulation.*",
"onView:stringManipulationSidebar"
],
"contributes": {
"configuration": {
"type": "object",
"title": "String Manipulation Configuration",
"properties": {
"stringManipulation.labs": {
"type": "boolean",
"default": false,
"description": "Enable experimental String Manipulation Labs features."
}
}
},
"views": {
"explorer": [
{
"type": "webview",
"id": "stringManipulationSidebar",
"name": "String Manipulation",
"when": "config.stringManipulation.labs"
}
]
},
"commands": [
{
"title": "Titleize",
Expand Down
205 changes: 205 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import * as vscode from "vscode";
import * as underscore 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) =>
(underscore 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));

const randomCase = (input: string): string => {
let result = "";
for (const char of input) {
if (Math.random() < 0.5) {
result += char.toLowerCase();
} else {
result += char.toUpperCase();
}
}
return result;
};

export type StringFunction = (
str: string,
multiselectData?: MultiSelectData
) => string;
export 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) =>
underscore.camelize(/[a-z]/.test(str) ? str : str.toLowerCase()),
slugify: slugify,
swapCase: defaultFunction("swapCase"),
snake: (str: string) =>
underscore
.underscored(str)
.replace(/([A-Z])[^A-Z]/g, " $1")
.replace(/[^a-z]+/gi, " ")
.trim()
.replace(/\s/gi, "_"),
screamingSnake: (str: string) =>
underscore
.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(""),
randomCase,
};

const numberFunctionNames = [
"increment",
"decrement",
"sequence",
"duplicateAndIncrement",
"duplicateAndDecrement",
];

export const functionNamesWithArgument = [
"chop",
"truncate",
"prune",
"repeat",
];

export const stringFunction = async (
commandName: string,
context: vscode.ExtensionContext,
shouldApply = true
): Promise<{ replacedSelections: string[] } | undefined> => {
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;

let replacedSelections = [];

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");
replacedSelections.push(replaced);
selectionMap[index] = { selection, replaced };
}

if (shouldApply) {
await editor.edit((builder) => {
Object.values(selectionMap).forEach(({ selection, replaced }) => {
builder.replace(selection, replaced);
});
});

context.globalState.update("lastAction", commandName);
}

return await Promise.resolve({ replacedSelections });
};

export function activate(context: vscode.ExtensionContext) {
context.globalState.setKeysForSync(["lastAction"]);

context.subscriptions.push(
vscode.commands.registerCommand(
"string-manipulation.repeatLastAction",
() => {
const lastAction = context.globalState.get<string>("lastAction");
if (lastAction) {
return stringFunction(lastAction, context);
}
}
)
);

Object.keys(commandNameFunctionMap).forEach((commandName) => {
context.subscriptions.push(
vscode.commands.registerCommand(
`string-manipulation.${commandName}`,
() => stringFunction(commandName, context)
)
);
});
}

export { commandNameFunctionMap };
Loading

0 comments on commit 1b1d6ca

Please sign in to comment.