diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b0870c..44f6525 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,16 +19,16 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v2 - - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/checkout@v4 with: - node-version: 18.13.0 + ref: ${{ github.ref }} + - name: Setup Node.js + uses: actions/setup-node@v4 - name: Install dependencies run: npm install - name: Build package run: npm run build - name: Run headless test - uses: GabrielBB/xvfb-action@v1.0 + uses: coactions/setup-xvfb@v1 with: run: npm test diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml index fd582f9..b6c2350 100644 --- a/.github/workflows/publish-extension.yml +++ b/.github/workflows/publish-extension.yml @@ -3,35 +3,33 @@ name: "Publish to Marketplace" on: workflow_run: workflows: ["CI"] - types: - - completed + types: [completed] + branches: [master] jobs: cd: - if: > - ${{ github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.head_branch == 'master' }} + if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout to branch uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} - name: Setup node.js uses: actions/setup-node@v4 - with: - node-version: 20 - name: "Bump version" - uses: 'phips28/gh-action-bump-version@master' + uses: "phips28/gh-action-bump-version@master" env: GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} with: - minor-wording: 'MINOR' - major-wording: 'MAJOR' - patch-wording: 'PATCH,FIX' - rc-wording: 'RELEASE' + minor-wording: "MINOR" + major-wording: "MAJOR" + patch-wording: "PATCH,FIX" + rc-wording: "RELEASE" - name: Install packages run: npm ci diff --git a/content.txt b/content.txt index 9abcf5c..6853409 100644 --- a/content.txt +++ b/content.txt @@ -43,3 +43,11 @@ abc中文💖 Hello, World! 12345!@#$% Test123! +'She said, "Hello"' +"My name's Minalike" +"He said, 'It's a trap!'" +'She exclaimed, \"Wow!\"' +"'Double' and 'single' quotes" +No quotes at all +'It's' +"My name's %20 Minalike!" diff --git a/package.json b/package.json index 9f7ea95..92de43a 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,11 @@ "title": "Random Case", "category": "String Manipulation", "command": "string-manipulation.randomCase" + }, + { + "title": "Swap Quotes", + "category": "String Manipulation", + "command": "string-manipulation.swapQuotes" } ], "submenus": [ @@ -329,6 +334,10 @@ { "command": "string-manipulation.randomCase", "group": "7_modification" + }, + { + "command": "string-manipulation.swapQuotes", + "group": "7_modification" } ] } diff --git a/src/commands.ts b/src/commands.ts index 16b8597..dd2531b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import * as underscore from "underscore.string"; +import { swapQuotes } from "./commands/swap_quotes"; const apStyleTitleCase = require("ap-style-title-case"); const chicagoStyleTitleCase = require("chicago-capitalize"); const slugify = require("@sindresorhus/slugify"); @@ -39,6 +40,14 @@ const randomCase = (input: string): string => { return result; }; +const snake = (str: string) => + underscore + .underscored(str) + .replace(/([A-Z])[^A-Z]/g, " $1") + .replace(/[^a-z0-9]+/gi, " ") + .trim() + .replace(/\s/gi, "_"); + export type StringFunction = ( str: string, multiselectData?: MultiSelectData @@ -53,7 +62,7 @@ const commandNameFunctionMap: { [key: string]: CommandFunction } = { classify: defaultFunction("classify"), clean: defaultFunction("clean"), cleanDiacritics: defaultFunction("cleanDiacritics"), - underscored: defaultFunction("underscored"), + underscored: snake, dasherize: defaultFunction("dasherize"), humanize: defaultFunction("humanize"), reverse: defaultFunction("reverse"), @@ -64,21 +73,8 @@ const commandNameFunctionMap: { [key: string]: CommandFunction } = { 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(), + snake, + screamingSnake: (str: string) => snake(str).toUpperCase(), titleizeApStyle: apStyleTitleCase, titleizeChicagoStyle: chicagoStyleTitleCase, truncate: (n: number) => defaultFunction("truncate", n), @@ -101,6 +97,7 @@ const commandNameFunctionMap: { [key: string]: CommandFunction } = { .map((x) => `\\u${x.charCodeAt(0).toString(16).padStart(4, "0")}`) .join(""), randomCase, + swapQuotes, }; const numberFunctionNames = [ diff --git a/src/commands/swap_quotes.ts b/src/commands/swap_quotes.ts new file mode 100644 index 0000000..e83ef0c --- /dev/null +++ b/src/commands/swap_quotes.ts @@ -0,0 +1,52 @@ +export const swapQuotes = (str: string): string => { + const singleQuote = "'"; + const doubleQuote = '"'; + + // Check if the string is at least two characters and starts and ends with the same quote + if (str.length < 2) { + return str; // Return as is if not properly quoted + } + + const firstChar = str[0]; + const lastChar = str[str.length - 1]; + + if ( + (firstChar !== singleQuote && firstChar !== doubleQuote) || + firstChar !== lastChar + ) { + // Not properly quoted, return as is + return str; + } + + const originalQuote = firstChar; + const newQuote = originalQuote === singleQuote ? doubleQuote : singleQuote; + let content = str.slice(1, -1); + + // Swap inner quotes + content = content.replace(/['"]/g, (match, offset) => { + // Determine if the quote is part of an apostrophe + const prevChar = content[offset - 1]; + const nextChar = content[offset + 1]; + const isApostrophe = + match === "'" && + /[a-zA-Z]/.test(prevChar || "") && + /[a-zA-Z]/.test(nextChar || ""); + + if (isApostrophe) { + // Handle apostrophe based on the desired output + if (newQuote === singleQuote) { + // Escape apostrophe when outer quote is single quote + return "\\'"; + } else { + // Keep apostrophe as is when outer quote is double quote + return match; + } + } else { + // Swap the quote + return match === originalQuote ? newQuote : originalQuote; + } + }); + + // Return the new string with swapped quotes + return newQuote + content + newQuote; +}; diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 03252a4..a62d1b5 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -66,14 +66,14 @@ suite("Extension Test Suite", () => { assert.equal(false, extension.isActive); }); - test("invoked when running one of the commands", async () => { + test("activates when running one of the commands", async () => { await vscode.commands.executeCommand("string-manipulation.titleize"); assert.equal(true, extension.isActive); }); }); suite("commands", () => { - test("camelize", async () => { + test("camelize converts hyphenated strings to camelCase", async () => { const [output1, output2] = await getTextForSelectionsByCommand( "string-manipulation.camelize", [ @@ -82,11 +82,11 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output1, "mozTransform"); - assert.strictEqual(output2, "MozTransform"); + assert.strictEqual(output1 /* moz-transform */, "mozTransform"); + assert.strictEqual(output2 /* -moz-transform */, "MozTransform"); }); - test("capitalize", async () => { + test("capitalize capitalizes the first character of each selection", async () => { const [output1, output2] = await getTextForSelectionsByCommand( "string-manipulation.capitalize", [ @@ -95,83 +95,86 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output1, "Foo"); - assert.strictEqual(output2, "Bar"); + assert.strictEqual(output1 /* foo */, "Foo"); + assert.strictEqual(output2 /* Bar */, "Bar"); }); - test("clean", async () => { + test("clean collapses multiple spaces into one", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.clean", [{ start: { line: 3, character: 0 }, end: { line: 3, character: 15 } }] ); - assert.strictEqual(output, "foo bar"); + assert.strictEqual(output /* foo bar */, "foo bar"); }); - test("cleanDiacritics", async () => { + test("cleanDiacritics removes diacritic marks from characters", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.cleanDiacritics", [{ start: { line: 4, character: 0 }, end: { line: 4, character: 8 } }] ); - assert.strictEqual(output, "aakkonen"); + assert.strictEqual(output /* ääkkönen */, "aakkonen"); }); - test("sentence", async () => { + test("sentence transforms text to sentence case", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.sentence", [{ start: { line: 5, character: 0 }, end: { line: 5, character: 7 } }] ); - assert.strictEqual(output, "Foo bar"); + assert.strictEqual(output /* foo Bar */, "Foo bar"); }); - test("classify", async () => { + test("classify converts underscored text to PascalCase", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.classify", [{ start: { line: 6, character: 0 }, end: { line: 6, character: 15 } }] ); - assert.strictEqual(output, "SomeClassName"); + assert.strictEqual(output /* some_class_name */, "SomeClassName"); }); - test("dasherize", async () => { + test("dasherize converts camelCase to kebab-case", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.dasherize", [{ start: { line: 7, character: 0 }, end: { line: 7, character: 12 } }] ); - assert.strictEqual(output, "-moz-transform"); + assert.strictEqual(output /* MozTransform */, "-moz-transform"); }); - test("decapitalize", async () => { + test("decapitalize lowercases the first character of each selection", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.decapitalize", [{ start: { line: 8, character: 0 }, end: { line: 8, character: 7 } }] ); - assert.strictEqual(output, "foo Bar"); + assert.strictEqual(output /* Foo Bar */, "foo Bar"); }); - test("humanize", async () => { + test("humanize converts text to human-readable form", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.humanize", [{ start: { line: 9, character: 0 }, end: { line: 9, character: 45 } }] ); - assert.strictEqual(output, "Capitalize dash camel case underscore trim"); + assert.strictEqual( + output /* capitalize dash-CamelCase_underscore trim */, + "Capitalize dash camel case underscore trim" + ); }); - test("reverse", async () => { + test("reverse reverses the characters in the selection", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.reverse", [{ start: { line: 10, character: 0 }, end: { line: 10, character: 3 } }] ); - assert.strictEqual(output, "cbA"); + assert.strictEqual(output /* Abc */, "cbA"); }); - test("slugify", async () => { + test("slugify converts text to a URL-friendly slug", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.slugify", [ @@ -182,10 +185,13 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "un-elephant-a-l-oree-du-bois"); + assert.strictEqual( + output /* Un éléphant à l'orée du bois */, + "un-elephant-a-l-oree-du-bois" + ); }); - test("swapCase", async () => { + test("swapCase inverts the case of each character", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.swapCase", [ @@ -196,10 +202,10 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "helloWORLD"); + assert.strictEqual(output /* HELLOworld */, "helloWORLD"); }); - test("snake", async () => { + test("snake converts text to snake_case", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.snake", [ @@ -210,10 +216,10 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "this_is_snake_case"); + assert.strictEqual(output /* This-is_snake case */, "this_is_snake_case"); }); - test("screamingSnake", async () => { + test("screamingSnake converts text to SCREAMING_SNAKE_CASE", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.screamingSnake", [ @@ -224,10 +230,13 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "SCREAMING_SNAKE_CASE"); + assert.strictEqual( + output /* screaming-snake case */, + "SCREAMING_SNAKE_CASE" + ); }); - test("titleize", async () => { + test("titleize capitalizes the first letter of each word", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.titleize", [ @@ -238,10 +247,10 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "My Name Is Tristan"); + assert.strictEqual(output /* my name is tristan */, "My Name Is Tristan"); }); - test("titleizeApStyle", async () => { + test("titleizeApStyle capitalizes titles according to AP style", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.titleizeApStyle", [ @@ -252,10 +261,10 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "This Is a Test"); + assert.strictEqual(output /* this is a test */, "This Is a Test"); }); - test("titleizeChicagoStyle", async () => { + test("titleizeChicagoStyle capitalizes titles according to Chicago style", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.titleizeChicagoStyle", [ @@ -267,26 +276,48 @@ suite("Extension Test Suite", () => { ); assert.strictEqual( - output, + output /* The quick brown fox jumps over the lazy dog. */, "The Quick Brown Fox Jumps Over the Lazy Dog." ); }); - test("underscored", async () => { - const [output] = await getTextForSelectionsByCommand( - "string-manipulation.underscored", - [ - { - start: { line: 18, character: 0 }, - end: { line: 18, character: 31 }, - }, - ] - ); + suite("underscore", () => { + test("converts text to underscore_case", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.underscored", + [ + { + start: { line: 18, character: 0 }, + end: { line: 18, character: 31 }, + }, + ] + ); + + assert.strictEqual( + output /* Underscored-is-like snake-case */, + "underscored_is_like_snake_case" + ); + }); + + test("removes special characters and converts text to lowercase with underscores", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.underscored", + [ + { + start: { line: 52, character: 0 }, + end: { line: 52, character: 25 }, + }, + ] + ); - assert.strictEqual(output, "underscored_is_like_snake_case"); + assert.strictEqual( + output /* "My name's %20 Minalike!" */, + "my_name_s_20_minalike" + ); + }); }); - test("chop", async () => { + test("chop splits the string into chunks of given length", async () => { vscode.window.showInputBox = async () => { return "2"; }; @@ -301,10 +332,10 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "aa,bb,cc,dd"); + assert.strictEqual(output /* aabbccdd */, "aa,bb,cc,dd"); }); - test("truncate", async () => { + test("truncate shortens the string to specified length and adds ellipsis", async () => { vscode.window.showInputBox = async () => { return "4"; }; @@ -319,10 +350,10 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "aabb..."); + assert.strictEqual(output /* aabbccdd */, "aabb..."); }); - test("prune", async () => { + test("prune truncates the string without breaking words", async () => { vscode.window.showInputBox = async () => { return "8"; }; @@ -337,10 +368,10 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "aabbc..."); + assert.strictEqual(output /* aabbccddaabbccdd */, "aabbc..."); }); - test("repeat", async () => { + test("repeat duplicates the string given number of times", async () => { vscode.window.showInputBox = async () => { return "2"; }; @@ -355,10 +386,10 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "aabbccddaabbccdd"); + assert.strictEqual(output /* aabbccdd */, "aabbccddaabbccdd"); }); - test("increment", async () => { + test("increment increases all numbers in the selection by 1", async () => { const [output1, output2, output3] = await getTextForSelectionsByCommand( "string-manipulation.increment", [ @@ -377,12 +408,21 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output1, "a2 b3 c4 5d 6e 7f 13x y24 35z46"); - assert.strictEqual(output2, "a2 b3 c4 5d 6e7f 13x y24 35z46"); - assert.strictEqual(output3, "a-3 b-2 c-1 0d 1e7f 13x y24 35z46"); + assert.strictEqual( + output1 /* a1 b2 c3 4d 5e 6f 12x y23 34z45 */, + "a2 b3 c4 5d 6e 7f 13x y24 35z46" + ); + assert.strictEqual( + output2 /* a1 b2 c3 4d 5e\n6f 12x y23 34z45 */, + "a2 b3 c4 5d 6e7f 13x y24 35z46" + ); + assert.strictEqual( + output3 /* a-4 b-3 c-2 -1d 0e\n6f 12x y23 34z45 */, + "a-3 b-2 c-1 0d 1e7f 13x y24 35z46" + ); }); - test("decrement", async () => { + test("decrement decreases all numbers in the selection by 1", async () => { const [output1, output2, output3] = await getTextForSelectionsByCommand( "string-manipulation.decrement", [ @@ -401,12 +441,21 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output1, "a0 b1 c2 3d 4e 5f 11x y22 33z44"); - assert.strictEqual(output2, "a0 b1 c2 3d4e 5f 11x y22 33z44"); - assert.strictEqual(output3, "a-4 b-3 c-2 -1d0e 5f 11x y22 33z44"); + assert.strictEqual( + output1 /* a1 b2 c3 4d 5e 6f 12x y23 34z45 */, + "a0 b1 c2 3d 4e 5f 11x y22 33z44" + ); + assert.strictEqual( + output2 /* a1 b2 c3 4d\n5e 6f 12x y23 34z45 */, + "a0 b1 c2 3d4e 5f 11x y22 33z44" + ); + assert.strictEqual( + output3 /* a-3 b-2 c-1 0d\n1e 6f 12x y23 34z45 */, + "a-4 b-3 c-2 -1d0e 5f 11x y22 33z44" + ); }); - test("duplicateAndIncrement", async () => { + test("duplicateAndIncrement duplicates selection and increments numbers in duplicate", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.duplicateAndIncrement", [ @@ -419,11 +468,11 @@ suite("Extension Test Suite", () => { assert.strictEqual( output, - "a1 b2 c3 4d 5e 6f 12x y23 34z45a2 b3 c4 5d 6e 7f 13x y24 35z46" + /* a1 b2 c3 4d 5e 6f 12x y23 34z45\n */ "a1 b2 c3 4d 5e 6f 12x y23 34z45a2 b3 c4 5d 6e 7f 13x y24 35z46" ); }); - test("duplicateAndDecrement", async () => { + test("duplicateAndDecrement duplicates selection and decrements numbers in duplicate", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.duplicateAndDecrement", [ @@ -436,11 +485,11 @@ suite("Extension Test Suite", () => { assert.strictEqual( output, - "a1 b2 c3 4d 5e 6f 12x y23 34z45a0 b1 c2 3d 4e 5f 11x y22 33z44" + /* a1 b2 c3 4d 5e 6f 12x y23 34z45\n */ "a1 b2 c3 4d 5e 6f 12x y23 34z45a0 b1 c2 3d 4e 5f 11x y22 33z44" ); }); - test("sequence", async () => { + test("sequence replaces numbers with a sequence starting from 1", async () => { const [output1, output2] = await getTextForSelectionsByCommand( "string-manipulation.sequence", [ @@ -455,11 +504,17 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output1, "a1 b2 c3 4d 5e 6f 7x y8 9z10"); - assert.strictEqual(output2, "a11 b12 c1314d 15e 16f 17x y18 19z20"); + assert.strictEqual( + output1 /* a1 b2 c3 4d 5e 6f 12x y23 34z45 */, + "a1 b2 c3 4d 5e 6f 7x y8 9z10" + ); + assert.strictEqual( + output2 /* a14 b2 c3\n4d 5e 6f 7x y8 9z12 */, + "a11 b12 c1314d 15e 16f 17x y18 19z20" + ); }); - test("utf8ToChar", async () => { + test("utf8ToChar converts Unicode escapes to characters", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.utf8ToChar", [ @@ -470,10 +525,13 @@ suite("Extension Test Suite", () => { ] ); - assert.strictEqual(output, "abc中文💖"); + assert.strictEqual( + output /* \u0061\u0062\u0063\u4e2d\u6587\ud83d\udc96 */, + "abc中文💖" + ); }); - test("charToUtf8", async () => { + test("charToUtf8 converts characters to Unicode escapes", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.charToUtf8", [ @@ -485,7 +543,7 @@ suite("Extension Test Suite", () => { ); assert.strictEqual( - output, + output /* abc中文💖 */, "\\u0061\\u0062\\u0063\\u4e2d\\u6587\\ud83d\\udc96" ); }); @@ -493,7 +551,7 @@ suite("Extension Test Suite", () => { suite("randomCase", () => { const input = "Hello, World!"; - test("check length and compare lower case", async () => { + test("maintains string length and lowercased content", async () => { const [output] = await getTextForSelectionsByCommand( "string-manipulation.randomCase", [ @@ -504,7 +562,7 @@ suite("Extension Test Suite", () => { ] ); assert.equal(input.length, 13); - assert.equal(input.toLowerCase(), "hello, world!"); + assert.equal(input.toLowerCase() /* Hello, World! */, "hello, world!"); }); test("changes the case of at least one character (statistically)", async () => { @@ -554,7 +612,7 @@ suite("Extension Test Suite", () => { }, ] ); - assert.equal(output, specialChars); + assert.equal(output /* 12345!@#$% */, specialChars); }); test("handles strings with mixed content", async () => { @@ -571,5 +629,113 @@ suite("Extension Test Suite", () => { assert.notEqual(output.replace(/[^a-zA-Z]/g, ""), ""); }); }); + + suite("swapQuotes", () => { + test("swaps outer single quotes to double quotes and inner double quotes to single quotes", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.swapQuotes", + [ + { + start: { line: 45, character: 0 }, + end: { line: 45, character: 19 }, + }, + ] + ); + assert.strictEqual( + output /* 'She said, "Hello"' */, + `"She said, 'Hello'"` + ); + }); + + test("swaps outer double quotes to single quotes and escapes inner apostrophe", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.swapQuotes", + [ + { + start: { line: 46, character: 0 }, + end: { line: 46, character: 20 }, + }, + ] + ); + assert.strictEqual( + output /* "My name's Minalike" */, + `'My name\\'s Minalike'` + ); + }); + + test("swaps outer double quotes to single quotes, inner single quotes to double quotes, and escapes apostrophe in contraction", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.swapQuotes", + [ + { + start: { line: 47, character: 0 }, + end: { line: 47, character: 25 }, + }, + ] + ); + assert.strictEqual( + output /* "He said, 'It's a trap!'" */, + `'He said, "It\\'s a trap!"'` + ); + }); + + test("swaps outer single quotes to double quotes and inner escaped double quotes to escaped single quotes", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.swapQuotes", + [ + { + start: { line: 48, character: 0 }, + end: { line: 48, character: 27 }, + }, + ] + ); + assert.strictEqual( + output /* 'She exclaimed, \\"Wow!\\"' */, + `"She exclaimed, \\'Wow!\\'"` + ); + }); + + test("swaps outer double quotes to single quotes and inner single quotes to double quotes", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.swapQuotes", + [ + { + start: { line: 49, character: 0 }, + end: { line: 49, character: 30 }, + }, + ] + ); + assert.strictEqual( + output /* "'Double' and 'single' quotes" */, + `'"Double" and "single" quotes'` + ); + }); + + test("returns input unchanged when string is not properly quoted", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.swapQuotes", + [ + { + start: { line: 50, character: 0 }, + end: { line: 50, character: 16 }, + }, + ] + ); + assert.strictEqual(output /* No quotes at all */, `No quotes at all`); + }); + + test("swaps outer single quotes to double quotes, preserving inner apostrophe", async () => { + const [output] = await getTextForSelectionsByCommand( + "string-manipulation.swapQuotes", + [ + { + start: { line: 51, character: 0 }, + end: { line: 51, character: 6 }, + }, + ] + ); + assert.strictEqual(output /* 'It's' */, `"It's"`); + }); + }); }); });