Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ Simple VS Code Extension to toggle text features! With `vext` commands you can..
- Keybinding: <kbd>Cmd</kbd>+<kbd>Opt</kbd>+<kbd>a</kbd> (_Mac_), <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>a</kbd> (_Other_)
- Settings:
- `vext.caseExtraWordChars`: Additional characters that will be considered a part of `\w` when parsing words to toggle case. For example, if '-' is specified, then 'super-secret' would be considered a single word. Defaults to _most_ special characters.
***

- `Toggle URL Encoding`: Toggle a word or selection to URL-encoded and back

![URL Encoding Demo](resources/demos/url-encode.gif)
- Keybinding: <kbd>Cmd</kbd>+<kbd>Opt</kbd>+<kbd>u</kbd> (_Mac_), <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>u</kbd> (_Other_)
- Settings: N/A
***

- `Toggle Base64 Encoding`: Toggle a word or selection to base64-encoded and back

![Base64 Encoding Demo](resources/demos/base64-encode.gif)
- Keybinding: <kbd>Cmd</kbd>+<kbd>Opt</kbd>+<kbd>6</kbd> (_Mac_), <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>6</kbd> (_Other_)
- Settings: N/A

## Keybindings

Expand Down
18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@
"command": "vext.toggleCase",
"title": "Toggle Text Casing"
},
{
"command": "vext.toggleUrlEncoding",
"title": "Toggle URL Encoding"
},
{
"command": "vext.toggleBase64Encoding",
"title": "Toggle Base64 Encoding"
},
{
"command": "vext.toggleVariableNamingFormat",
"title": "Toggle Variable Naming Format"
Expand Down Expand Up @@ -107,6 +115,16 @@
"key": "ctrl+alt+a",
"mac": "cmd+alt+a"
},
{
"command": "vext.toggleUrlEncoding",
"key": "ctrl+alt+u",
"mac": "cmd+alt+u"
},
{
"command": "vext.toggleBase64Encoding",
"key": "ctrl+alt+6",
"mac": "cmd+alt+6"
},
{
"command": "vext.toggleVariableNamingFormat",
"key": "ctrl+alt+v",
Expand Down
Binary file added resources/demos/base64-encode.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/demos/url-encode.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import vscode from 'vscode';

import { TOGGLE_BASE64_ENCODING_CMD, toggleBase64Encoding } from './toggleBase64Encoding';
import { toggleCase, TOGGLE_CASE_CMD } from './toggleCase';
import { toggleCommentType, TOGGLE_COMMENT_TYPE_CMD } from './toggleCommentType';
import { toggleJsonToJsToYaml, TOGGLE_JSON_TO_JS_TO_YAML_CMD } from './toggleJsonToJsToYaml';
import { TOGGLE_NEWLINE_CHARS_CMD, toggleNewlineChars } from './toggleNewlineChars';
import { toggleQuotes, TOGGLE_QUOTES_CMD } from './toggleQuotes';
import { TOGGLE_URL_ENCODING_CMD, toggleUrlEncoding } from './toggleUrlEncoding';
import { toggleVariableNamingFormat, TOGGLE_VARIABLE_NAMING_FORMAT_CMD } from './toggleVariableNamingFormat';
import { EXTENSION_NAME } from '../constants';

Expand All @@ -14,6 +16,11 @@ export function registerAllCommands(context: vscode.ExtensionContext): void {
vscode.commands.registerTextEditorCommand(`${EXTENSION_NAME}.${TOGGLE_COMMENT_TYPE_CMD}`, toggleCommentType),
vscode.commands.registerTextEditorCommand(`${EXTENSION_NAME}.${TOGGLE_QUOTES_CMD}`, toggleQuotes),
vscode.commands.registerTextEditorCommand(`${EXTENSION_NAME}.${TOGGLE_CASE_CMD}`, toggleCase),
vscode.commands.registerTextEditorCommand(`${EXTENSION_NAME}.${TOGGLE_URL_ENCODING_CMD}`, toggleUrlEncoding),
vscode.commands.registerTextEditorCommand(
`${EXTENSION_NAME}.${TOGGLE_BASE64_ENCODING_CMD}`,
toggleBase64Encoding
),
vscode.commands.registerTextEditorCommand(
`${EXTENSION_NAME}.${TOGGLE_VARIABLE_NAMING_FORMAT_CMD}`,
toggleVariableNamingFormat
Expand Down
47 changes: 47 additions & 0 deletions src/commands/shared/regexBasedBinaryToggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import vscode from 'vscode';

import { CursorWordOptions } from '../../types';
import { getCursorWordAsSelection, handleError, isHighlightedSelection } from '../../utils';

/**
* This function will use a regex to test a selection or word, transforming it according to provided transformFn.
* It allows for a multiline selection, but assumes all lines follow the pattern of the first line.
*/
export async function regexBasedBinaryToggle(
editor: vscode.TextEditor,
regex: RegExp,
transformFn: (str: string, isMatch: boolean) => string,
cursorWordOptions?: CursorWordOptions
): Promise<void> {
await handleError(async () => {
const selectionsToToggle: vscode.Selection[] = [];

// Will have multiple selections if multi-line cursor is used
for (const selection of editor.selections) {
if (isHighlightedSelection(selection)) {
// Toggle the whole selection
selectionsToToggle.push(selection);
} else {
// Toggle the current word the cursor is in
const cursorSelection = getCursorWordAsSelection(editor, selection, cursorWordOptions);
selectionsToToggle.push(cursorSelection);
}
}

if (selectionsToToggle.length) {
// Use first selection to drive pattern decision for all others
const isMatch = regex.test(editor.document.getText(selectionsToToggle[0]));

await editor.edit((builder) => {
for (const selection of selectionsToToggle) {
const newText = transformFn(editor.document.getText(selection), isMatch);
builder.replace(selection, newText);
}
});
/* c8 ignore next 4 */
} else {
// I don't know if this can happen
throw Error('No selections found!');
}
});
}
34 changes: 34 additions & 0 deletions src/commands/toggleBase64Encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import vscode from 'vscode';

import { regexBasedBinaryToggle } from './shared/regexBasedBinaryToggle';
import { handleError } from '../utils';

export const TOGGLE_BASE64_ENCODING_CMD = 'toggleBase64Encoding';

/**
* When cursor is in the middle of a word--or there is an explicit selection--toggle the base64 encoding.
*
* @param editor the vscode TextEditor object
*/
export async function toggleBase64Encoding(editor: vscode.TextEditor): Promise<void> {
await handleError(async () => {
// NOTE: Base64 attempts to translate 3 ascii/utf digits into 4 encoded digits using a defined set of 64 characters.
// Equals signs may appear at end of encoded string if number of input string characters isn't cleanly divisible by 3,
// which effectively pads the length of the encoded string to be divisible by 4
const urlEncodedRegex = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/;
await regexBasedBinaryToggle(editor, urlEncodedRegex, transform, { useWhitespaceDelimiter: true });
});
}

function transform(originalText: string, isBase64Encoded: boolean): string {
// TODO: Use base64 here
return isBase64Encoded ? decodeBase64(originalText) : encodeBase64(originalText);
}

function encodeBase64(plaintextStr: string): string {
return Buffer.from(plaintextStr).toString('base64');
}

function decodeBase64(base64Str: string): string {
return Buffer.from(base64Str, 'base64').toString('utf-8');
}
44 changes: 7 additions & 37 deletions src/commands/toggleCase.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import _ from 'lodash';
import vscode from 'vscode';

import { regexBasedBinaryToggle } from './shared/regexBasedBinaryToggle';
import { getConfig } from '../configuration';
import { CASE_EXTRA_WORD_CHARS } from '../configuration/configuration.constants';
import { getCursorWordAsSelection, handleError, isHighlightedSelection } from '../utils';
import { handleError } from '../utils';

export const TOGGLE_CASE_CMD = 'toggleCase';

Expand All @@ -14,43 +15,12 @@ export const TOGGLE_CASE_CMD = 'toggleCase';
*/
export async function toggleCase(editor: vscode.TextEditor): Promise<void> {
await handleError(async () => {
const caseExtraWordChars = getConfig<string[]>(CASE_EXTRA_WORD_CHARS);
const selectionsToToggle: vscode.Selection[] = [];

// Will have multiple selections if multi-line cursor is used
for (const selection of editor.selections) {
if (isHighlightedSelection(selection)) {
// Toggle the whole selection
selectionsToToggle.push(selection);
} else {
// Toggle the current word the cursor is in
const cursorSelection = getCursorWordAsSelection(editor, selection, caseExtraWordChars);
selectionsToToggle.push(cursorSelection);
}
}

if (selectionsToToggle.length) {
// Use first selection to drive casing decision for all others
const transformFn = getCaseTransformFn(editor.document.getText(selectionsToToggle[0]));

await editor.edit((builder) => {
for (const selection of selectionsToToggle) {
const newText = transformFn(editor.document.getText(selection));
builder.replace(selection, newText);
}
});
/* c8 ignore next 4 */
} else {
// I don't know if this can happen
throw Error('No selections found!');
}
const extraWordChars = getConfig<string[]>(CASE_EXTRA_WORD_CHARS);
const hasLowercaseRegex = /[a-z]/;
await regexBasedBinaryToggle(editor, hasLowercaseRegex, transform, { extraWordChars });
});
}

/**
* Get target casing transform function (i.e., upper or lower) based on current text.
*/
function getCaseTransformFn(originalText: string): (s: string) => string {
const hasLowercase = /[a-z]/.test(originalText);
return hasLowercase ? _.toUpper : _.toLower;
function transform(originalText: string, hasLowercase: boolean): string {
return hasLowercase ? _.toUpper(originalText) : _.toLower(originalText);
}
2 changes: 1 addition & 1 deletion src/commands/toggleQuotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export async function toggleQuotes(editor: vscode.TextEditor): Promise<void> {
// let's add quotes to this unquoted word.
if (!quoteMatch) {
try {
const cursorWordSelection = getCursorWordAsSelection(editor, selection, extraWordChars);
const cursorWordSelection = getCursorWordAsSelection(editor, selection, { extraWordChars });
quoteMatch = {
startLine: lineNumber,
endLine: lineNumber,
Expand Down
24 changes: 24 additions & 0 deletions src/commands/toggleUrlEncoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import vscode from 'vscode';

import { regexBasedBinaryToggle } from './shared/regexBasedBinaryToggle';
import { handleError } from '../utils';

export const TOGGLE_URL_ENCODING_CMD = 'toggleUrlEncoding';

/**
* When cursor is in the middle of a word--or there is an explicit selection--toggle the URL encoding.
*
* @param editor the vscode TextEditor object
*/
export async function toggleUrlEncoding(editor: vscode.TextEditor): Promise<void> {
await handleError(async () => {
// Looking for any % followed by 2 hex digits, signifying an actual replacement has been done. Otherwise,
// there is nothing to "decode"
const urlEncodedRegex = /%[0-9a-fA-F]{2}/;
await regexBasedBinaryToggle(editor, urlEncodedRegex, transform, { useWhitespaceDelimiter: true });
});
}

function transform(originalText: string, isUrlEncoded: boolean): string {
return isUrlEncoded ? decodeURIComponent(originalText) : encodeURIComponent(originalText);
}
2 changes: 1 addition & 1 deletion src/commands/toggleVariableNamingFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function toggleVariableNamingFormat(editor: vscode.TextEditor): Pro
}

// Toggle the current word the cursor is in
const cursorSelection = getCursorWordAsSelection(editor, selection, ['-']);
const cursorSelection = getCursorWordAsSelection(editor, selection, { extraWordChars: ['-'] });
selectionsToToggle.push(cursorSelection);
}

Expand Down
73 changes: 73 additions & 0 deletions src/test/suite/toggleBase64Encoding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect } from 'chai';
import dedent from 'dedent';
import _ from 'lodash';
import vscode from 'vscode';

import { toggleBase64Encoding } from '../../commands/toggleBase64Encoding';
import {
openEditorWithContent,
openEditorWithContentAndSelectAll,
openEditorWithContentAndSetCursor,
} from '../utils/test-utils';

describe('toggleBase64Encoding cycles the base 64 encoding of a selection or word', () => {
afterEach(async () => {
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
});

describe('of a selection', () => {
it('basic usage', async () => {
const editor = await openEditorWithContentAndSelectAll('javascript', 'Base 64 encode this string!');
await toggleBase64Encoding(editor);
expect(editor.document.getText()).to.equal('QmFzZSA2NCBlbmNvZGUgdGhpcyBzdHJpbmch');
await toggleBase64Encoding(editor);
expect(editor.document.getText()).to.equal('Base 64 encode this string!');
});
});

describe('of a word', () => {
it('basic usage', async () => {
const editor = await openEditorWithContentAndSetCursor(
'javascript',
`ignore? encode=me? ignore?`,
`ignore? enco`.length
);
await toggleBase64Encoding(editor);
expect(editor.document.getText()).to.equal(`ignore? ZW5jb2RlPW1lPw== ignore?`);
await toggleBase64Encoding(editor);
expect(editor.document.getText()).to.equal(`ignore? encode=me? ignore?`);
});

it('multiple cursors - all selections use casing of first selection', async () => {
const editor = await openEditorWithContent(
'javascript',
dedent`
four?
four?
four?
`
);
for (const _iter of _.times(2)) {
await vscode.commands.executeCommand('editor.action.insertCursorBelow');
}
// All lines should be toggled to the same new quote character
await toggleBase64Encoding(editor);
expect(editor.document.getText()).to.equal(dedent`
Zm91cj8=
Zm91cj8=
Zm91cj8=
`);
await toggleBase64Encoding(editor);
expect(editor.document.getText()).to.equal(dedent`
four?
four?
four?
`);
});

it('error when cursor is not inside of word', async () => {
const editor = await openEditorWithContentAndSetCursor('javascript', 'three spaces', 'three '.length);
await expect(toggleBase64Encoding(editor)).to.be.rejectedWith('Cursor must be located within a word!');
});
});
});
Loading
Loading