From d366f1ba6b147ac05a389fa15200b06ee9bb2d09 Mon Sep 17 00:00:00 2001 From: Adam C Hamlin Date: Sun, 20 Jul 2025 19:54:43 +0200 Subject: [PATCH] feat: add toggleUrlEncoding and toggleBase64Encoding commands --- README.md | 14 ++++ package.json | 18 ++++ resources/demos/base64-encode.gif | Bin 0 -> 16748 bytes resources/demos/url-encode.gif | Bin 0 -> 12144 bytes src/commands/index.ts | 7 ++ src/commands/shared/regexBasedBinaryToggle.ts | 47 +++++++++++ src/commands/toggleBase64Encoding.ts | 34 ++++++++ src/commands/toggleCase.ts | 44 ++-------- src/commands/toggleQuotes.ts | 2 +- src/commands/toggleUrlEncoding.ts | 24 ++++++ src/commands/toggleVariableNamingFormat.ts | 2 +- src/test/suite/toggleBase64Encoding.spec.ts | 73 ++++++++++++++++ src/test/suite/toggleUrlEncoding.spec.ts | 78 ++++++++++++++++++ src/types/index.ts | 21 +++++ src/utils.ts | 50 ++++++----- 15 files changed, 353 insertions(+), 61 deletions(-) create mode 100644 resources/demos/base64-encode.gif create mode 100644 resources/demos/url-encode.gif create mode 100644 src/commands/shared/regexBasedBinaryToggle.ts create mode 100644 src/commands/toggleBase64Encoding.ts create mode 100644 src/commands/toggleUrlEncoding.ts create mode 100644 src/test/suite/toggleBase64Encoding.spec.ts create mode 100644 src/test/suite/toggleUrlEncoding.spec.ts diff --git a/README.md b/README.md index f4931f2..6deab78 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,20 @@ Simple VS Code Extension to toggle text features! With `vext` commands you can.. - Keybinding: Cmd+Opt+a (_Mac_), Ctrl+Alt+a (_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: Cmd+Opt+u (_Mac_), Ctrl+Alt+u (_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: Cmd+Opt+6 (_Mac_), Ctrl+Alt+6 (_Other_) + - Settings: N/A ## Keybindings diff --git a/package.json b/package.json index 34c487b..ed6a7f2 100644 --- a/package.json +++ b/package.json @@ -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" @@ -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", diff --git a/resources/demos/base64-encode.gif b/resources/demos/base64-encode.gif new file mode 100644 index 0000000000000000000000000000000000000000..433fadc077b9ebf3eac39504ffb19d97a291120a GIT binary patch literal 16748 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 K|DQEvhyMb3Dnlv& literal 0 HcmV?d00001 diff --git a/resources/demos/url-encode.gif b/resources/demos/url-encode.gif new file mode 100644 index 0000000000000000000000000000000000000000..dfe2b072fc418c699313535c2a0a162465961233 GIT binary patch literal 12144 zcmeI2cTf}i-|sicCIpf|LJgrMfb&2vgosfiQbJKvD4`n40TfFp z0t!m#Ac~u+7%8E!U^mh3)UE-YU)napE`A_vZ15qY*($g`*h2>wzf8LM{jf2#rAWO?%w`h z$;AtOgM9--L&MTx>7~)p%U5Kwi5oZW+_^I|GxO}(vp=4|{{x%CA_w5{(=#%&1lc*c zdHDs0gjvwSl3z>9$`2o@IC|{(2~lNLbI6IzW+e}aAx+xy+=>yo;`o@GULgs z`8RLhz3+Yf`=`%e7QX)a@f!eBY77;L`k*jFOu5j42LV*|p$pF9s-av-k=bo&Gu0SI zUcW&I$f5oL>c#|nipMoz863GmSST8 z)|=ft2QMiH%)Gd?pXw%?=)rI1Jwi{C@DYJUX?r0)qr#v?2MNnk@aitA7$Hq?Q|w3K zX>gD73KGn}7k01rrEqY|Y=LT8>f#=)_=W)gnIR;sK*{|UDd(}41UFx988?`CJNAH-y zO3b2{RqC(pbtfs!P5gvkS?>T$P3|X@Q&yi2zp1r(i?ca^2t9rPvlXAjlVnaEP<@|y z6D$hW)BvE;1XXS>jR|DuM4Q~5#Nbk*ySwT*laBFG3YUO}!o0(v3W{A0;NA@cu zB_68{tfz*!-q^M`HX(as|(jR}t*o!~r~9aKwSwYQq2 z9&)?%*L}Bqq|xd@GHo8VkL&^euE_F09KLUm@ z>Mvgh8HSJ>Js%RVXPaBj4XB^(>3H#5k1&K@UgL4DigfT!}dVH?AKUbk0)7!vg?ndRVaNloG^qEW!zjsV_>7?o&iDm-x=`l)Ig5|isPnz z`=IU@-GG#f658*wZC5?Pd!|>r7dx~*g{SI|=1MLtZ!d|YWk~UR=|4K&O zpV*>u5I?B+Ns?&kK8(T0VI6tfoyhZ*$h{0N{dtb{D!L!ab{?wlZVc<-LsV5D)i*Fy z=t@C#b&j{2bbJj$3rk013Lyr=M8wD&52Zz9qo>XI>0?)xdAP5&Y=7K!9YZ{nGW#=C zzYH7fLyPEgU#9#Z(m$$g$d|LQ-55jeDW`E1is_X=8ms8}JYWm+Tleq@BYOnXUvSdW zADz*!Q%PA9;Vz^&1b1Wg(XUsGqzhCW;aaAeUPUKoPu@4d`G0!I!XEUmBr=^xTfE}{ z4N=aS^Dd?7A)F&T_G3Gj4FafCo{c^X1S2c2c`%1_`)@?~t~l4Z;mGWU3?gG2@YE|a z(+;}qWEA(U*;u2@Ymd8B*)XMgb%Ei-I*K>-+<9}PTWQGAUkoq5aecNPC~ez&(l|X} zG4b5A_uJ#}Mb0UrXtPdmN=YINZ)h2^S=Ky6sm{_aDrcK<@M(5cZToz`D1DR>^fb(j z@@pHUw5B14Db-s$ee{2n~SS&Ha6P9uHLRBR^4f^PvqC-u_io}?DIz+ z57qO-boY#YzSJK5?i8n5;xly9;80_W^Vxj)2A%1y{W+F-hpO~~mZ5-_(&bD-hKX12 z#+Albj0=sEk2-C%+vs(HVs;$$BL3xy^^bl0%I(F)@t=pT7p=E58@C>GAjx$6^k2K! zOJjo!EDz()(*Z0iGF)rwQfFi-Zpbny68W(cz6EG=xif2V*8C{G9?{+&K}RKwz~1(9 zOlGZZ7ejZT{Pnw*A-doq^Pkjjjn%~2Fc++czuyuS2_L71lx;Hc1=BC)wkQQHp<-|w7S^$6LneqE4d>yU+m3d?U z(Q*Fqjc-XKSccVl4DOefsqFr8h`qm|W9fV9ob<_t*P($iK(fC)9hll0e(0mtT*{*+ zUyZ%D@4i=QJF^w%?J>$s{Nm)Pb_b{K-&Un%vIA+I^0SDcWUPM%>D)cgOCVhF)F09C z^qkYH)oeSxF~20^i)2k>L3^W7;myJEFSoj`w4Y5Hf3aoZ%cP{k3&4svaQwoQbWKM~ z+2Z)iXt#yivVxAby5Lu_;R|=Bt~j--Q8@Yv;X4&%9bF^Azr|H7+?`wF+^Rw6b{$xV z8Vc<^|1|h@%JlSo46(0yt^Bv79#rLQL%Kp3g{4^{nKEK{C>5OHBEotgPDGyak$VsY z-Bh?0`;$V+uBC_-j?|-nLdd>BNOyInm66FU9tVCy828P7{ovedAxk$wx~P#P+$&{t zKDTooDUPe~>Li5LL%kh7@sFiTcqZxYiejAtSN0_X?zga@~OVtaG}!6MLs z0;_S}i(-LolNrOc&?h5MOFEO{mxT3$1=9~0&1VqUyj22bh!%LsXX%a)ex0=WUZ*&c z2>MTf=3>x51`_3f6A=u+Cigyt72c9F%3Vx{g}sepMA&o>am@MSO)8EBeZ zq&x}eh#=Wr`>&KUG2-H~dT0roW4eQB-kpCLnHazV$DN_RGH^AmXx)wkr{ofgH;K%gWPqhFujf%LPnJ zIv6NS7%PR6q-FZ@GLCu4xZh5TS_9PU#28Yo1I3{F1sIYcfFX7Nx$Xps1byWnV` zkbRlWEOUV^XC1fv%-nsOTW5YkMGOgK!OUr}{hiD}G0ch_M`9hL-iDOAR9F#0YU*Fpl~r}Q7E+m%W=Jl(_>`qfhPld8$3C32#>OdjCg+Vn36gjN7Hg;uChKHY zKCK(y2(_ji8r6h;D1&_900mjc8 zF6P+1@oBv7dgjJyra2LOxABa$^!QbR-NV{5a>A>#GZtrF-8zFe0u-=kX3w2{dFSk% z;a&Hm&%SkSdcV2pLrT-9BTZkhqsudwwP74SoE}5O}E%Aw2*aL?cG`( z!&{wFTU{zzU3*&Hr(0Jov{H21sBUdu;cY&tZGIJP0X=O&(`{=O+GslM>)qNz!rMbr z+rui_H}|whOt)`YXs7FRY<2613h#(c?O;@N#P)PBr#p5pbg*yq!#fjGJJ}VT zDLtKOmd)J18VHpo?zXamI08@r3vdk#3`k~VBd5Tnvrr@mEovBlEj0hLs&M~fVIbfL zL_x4X2GPjm?_wf2(RA&9v=WYC2V!EP|EV54qcVM>1%5Hv|5N+$>r(p=!ztg%|GRof z$PuOdSv~wm-B8gWJbvb{y5aE2mSgp8XPeF}6%8lPcGZcy8(MqB9T!@Fk5~uCz5xF;f2yM&{d!!bo8(RC=;6u1ky{1V4#Qc)Y9V| z!gyIJ2Oy+ zTvJc~2318bpOKZFI|~YnJQW5HSv3YD#yXzLu0KF1c_EJJCxiCre#vPMOvLV3A%0yo zjH{&NFF^Y75g8=Iur$F#XrYppVXT?kTP_@qz($V&b~XH572BvMu@h}!JFasT)JI>Uv9H| z!Ra?1-XRDbDd```Ca5B%^oONnZX@+BBjDxC!@Qr=ZCD!i;naKtLdT`vu22HU>3y)C zlNowvA@y8!r*=q7f4SL5mmsBN*D8W8g-zC(;hSrt4m7J>f9RA!P>7GC$+#Ig%3A5H zSt1Bkz-e-X1`u4@e4p{$sf|BVwqMXPU_bNvm=8B_e6OgeO%dzup?;iJ#Mhw4@Qo6$CExT;!?9dQzF*cSM!IoI#{Q z9Ayq5TKST=&vW@$sRz1wnM8s$4T4`onLjFAhngrM6Cs)<34LpqFWTSyUz42s$JikGTXvBP8?v zNFB(N1-Rx!d$&-5zGKeT@Qn7qQF-rauqUE$+{8S*3+?Z_1AXV z_I}wqx@XhksBpcatxGZwtHHvS#hQk_*nAP}YHl+B9x2m;!I`ohN}7YJun>Y&__!5p z12mxVI2zBB8bqE#5hw#tMGzm-hd%6DrMC8S!u;kr5*7F`=?or|@;bhFe#x&Qp5s(JuHx&=ntdUa!EC zp#8h;&VQMIn%nU!)MneS5+0g_ooz5FxvmUTsEGY;3n8ZcYyi<26D2F9-=ww8D+_N@ z?(UnkPKPELQSN`}AW79DU60_e3=oAp^o@Xh*5v|tPSW{BI4yZ>S`iG9nJbSChg!)7 zWA9c$LxV=tW8Yy)grtl4Yz~POq<~tp6eh++kg;``H4I8lA2%4W_OJ)qBog>S+Uhzs zp=fC044zBR0UT*uWUU!Q?dWUAWg&OaIl~ZbM{n>ae{_gI=KAR{aT$%tc~e_&Xc-6O z3c>(%&cu*y<|2+gj^9Fw0-zNVi8`X3Way*7SB#?gzBN*QrL28YY87OqlFQvzG7R0~ zv6O9b<((f+=Bcw|BMEZJx>{*bm>wq?oTvO=>oQD8^F*2g+;%~{Gd|W(=olr#?89Sy zTa0!ni#!Nx#oXJ?&58SK`V6+02T}>^5ja7goWd`MiRp&CfUJJm2RM=fI23hXg7)$` zIHm-!lo~cV?cm|wkCP@=l&M@xM{0%vL_0?x6tLICAH7bn=gd1fZ#`g zP@O6Ut#uPeTX(?4H6R@N{(6J`kX4Aolqs5CWxdCHME!%u=Gd8yK@7#nDcTNZIv>LZper|(YPwY|-bheIKs4_T(@n6; z)?|(rBk6G##&aCRmU&r!p9E%4r(nXc$EbrA3BLKEPwo#MCkUdg9Pl0ps;Eh_l8S(i zTWt57QG6d=8vo)e<(i|A{0*_MJm@rJX%szd$P=B{?PaE@`Q(J07J-LzZ6lOOu=EWr zn%@2;kSaC3?|u}}^b_SnI>(d3;!A635(OVlIl4p+yW0SSDY7`2qZ8gv|Ff2N59KzA-^hGI;djPVRfGgzqzZ*>p4RDQ9ex+8N%ugHO;{ z-Kt5=WG6}ScK1iBLTHxHfPaJ4%JkQ&kuv2u09tZ~$x{x=)WL))7w(OVvU*nwGXeU# z@!|r?(cdU9AL?6a`=*<0i@mb$xcycn1>$;j(~YlhL~s)bcOsdCzswz~xfcG6P-j$v z6z*8(hs#!G0$N`JOZ0~StO%4)Q+G?R?#e{m2;23tV5uTl`J0lg;Qr1j7=C{I^?`+j z2Y@LBkn7F!biT@wS}?8iLJsk-L}tAdTddCfoya&#iEQaN6rO(QR4)9_AoicHFQu)e zOaxUx=ha<$5&AFx@K03xBh!&r|0$4?{)<40j)`6}M*pNW>bA^(6-68V2T^q7zl$R7 zf0aW`rPY7Sp}*4FKU(N-C)6Y9zR>@t5E}kl2wlGXe;L(wDM7Q<>6c%nT$KM6)fjsd z#&`ETm}jSnWVNq9oj-7}Y+;eZ{2x&7E<7xHHs+`GvoVtnpW;u`ZXROrRs@R zKM-SN##PpGYrAYUm>xy?U3Pl=^kin>?U-a|UUi2jejg>Wq!0a=G zmba$6nJzk^4Kc!^@DYT1vH^4f6B!gqt!EK1Sotl8A{MNHD2~)WqIi4inl6Hl-nv4f zg0&+s03nAg|7HJ~Wy^z6mVU+qNQVo~#T<~}M?qeb`T|Hh>|8%m`m?DQ=tSfgL!FfO z=Ho^xy*U_MEN2_{ot>N#sgRDnhqa5S+maep+70)|ko_e{FI7&z!o)*g5Q>>EyFmtZ zg^v^*S`=fBhg+1BaLP^yh|=M578k>8$)h)jqGo$S%cD25|~xFEg)Hp|TdKRT`(e)3dIYmw914d&vV1 zma#lak*D+)Zy(i#JW%G9x6=!a-vZ^G^qP{Xnvi8;v?^hK`AgW4er|nVP?T5t@#;MP zSJHDMHIAJcOZp0*En-r{{pW5>7n~dv==JWZfp_Dp=6Q zAk{b=or^JGhYu<_;_3V*vj+Fk-rs5+Bit;biY$YjX1|sw+*546m(jJ#YOq(+h}%*a zO2v2#OXFzo##b0sF7KWglIOP>n--u{)x_q`V5mVeK(!7Q?ev#A0{x0cq=y_zHH|3r z(6f@GKv82nFW8sp_$5e_O|QYJvQBdP2J#Qhlb?8|*?{mPEIO~spK;~0EoN-r+b0HX z_B0&%>*4gdx^5d9>_|L^V{5bPemBlf750Q-Xu12l;mw*QR$pVhsIhS3ru;X^I^Wdq z-|{6*i{GE`lu<*!I#55)`%-=e=Q+qA5%D=qf|&8(V)H~`H7mo)Wu6y string, + cursorWordOptions?: CursorWordOptions +): Promise { + 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!'); + } + }); +} diff --git a/src/commands/toggleBase64Encoding.ts b/src/commands/toggleBase64Encoding.ts new file mode 100644 index 0000000..1628935 --- /dev/null +++ b/src/commands/toggleBase64Encoding.ts @@ -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 { + 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'); +} diff --git a/src/commands/toggleCase.ts b/src/commands/toggleCase.ts index fc9e780..3f530b7 100644 --- a/src/commands/toggleCase.ts +++ b/src/commands/toggleCase.ts @@ -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'; @@ -14,43 +15,12 @@ export const TOGGLE_CASE_CMD = 'toggleCase'; */ export async function toggleCase(editor: vscode.TextEditor): Promise { await handleError(async () => { - const caseExtraWordChars = getConfig(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(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); } diff --git a/src/commands/toggleQuotes.ts b/src/commands/toggleQuotes.ts index 465cb4d..ef9f429 100644 --- a/src/commands/toggleQuotes.ts +++ b/src/commands/toggleQuotes.ts @@ -96,7 +96,7 @@ export async function toggleQuotes(editor: vscode.TextEditor): Promise { // 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, diff --git a/src/commands/toggleUrlEncoding.ts b/src/commands/toggleUrlEncoding.ts new file mode 100644 index 0000000..7d05ec9 --- /dev/null +++ b/src/commands/toggleUrlEncoding.ts @@ -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 { + 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); +} diff --git a/src/commands/toggleVariableNamingFormat.ts b/src/commands/toggleVariableNamingFormat.ts index 93b4665..fcc56b1 100644 --- a/src/commands/toggleVariableNamingFormat.ts +++ b/src/commands/toggleVariableNamingFormat.ts @@ -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); } diff --git a/src/test/suite/toggleBase64Encoding.spec.ts b/src/test/suite/toggleBase64Encoding.spec.ts new file mode 100644 index 0000000..c2154cd --- /dev/null +++ b/src/test/suite/toggleBase64Encoding.spec.ts @@ -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!'); + }); + }); +}); diff --git a/src/test/suite/toggleUrlEncoding.spec.ts b/src/test/suite/toggleUrlEncoding.spec.ts new file mode 100644 index 0000000..bd07732 --- /dev/null +++ b/src/test/suite/toggleUrlEncoding.spec.ts @@ -0,0 +1,78 @@ +import { expect } from 'chai'; +import dedent from 'dedent'; +import _ from 'lodash'; +import vscode from 'vscode'; + +import { toggleUrlEncoding } from '../../commands/toggleUrlEncoding'; +import { + openEditorWithContent, + openEditorWithContentAndSelectAll, + openEditorWithContentAndSetCursor, +} from '../utils/test-utils'; + +describe('toggleUrlEncoding cycles the URL 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', + 'I am 99% sure this is not URL encoded/translated' + ); + await toggleUrlEncoding(editor); + expect(editor.document.getText()).to.equal( + 'I%20am%2099%25%20sure%20this%20is%20not%20URL%20encoded%2Ftranslated' + ); + await toggleUrlEncoding(editor); + expect(editor.document.getText()).to.equal('I am 99% sure this is not URL encoded/translated'); + }); + }); + + describe('of a word', () => { + it('basic usage', async () => { + const editor = await openEditorWithContentAndSetCursor( + 'javascript', + `ignore? encode=this? ignore?`, + `ignore? enco`.length + ); + await toggleUrlEncoding(editor); + expect(editor.document.getText()).to.equal(`ignore? encode%3Dthis%3F ignore?`); + await toggleUrlEncoding(editor); + expect(editor.document.getText()).to.equal(`ignore? encode=this? ignore?`); + }); + + it('multiple cursors - all selections use casing of first selection', async () => { + const editor = await openEditorWithContent( + 'javascript', + dedent` + encode? + encode? + encode? + ` + ); + 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 toggleUrlEncoding(editor); + expect(editor.document.getText()).to.equal(dedent` + encode%3F + encode%3F + encode%3F + `); + await toggleUrlEncoding(editor); + expect(editor.document.getText()).to.equal(dedent` + encode? + encode? + encode? + `); + }); + + it('error when cursor is not inside of word', async () => { + const editor = await openEditorWithContentAndSetCursor('javascript', 'three spaces', 'three '.length); + await expect(toggleUrlEncoding(editor)).to.be.rejectedWith('Cursor must be located within a word!'); + }); + }); +}); diff --git a/src/types/index.ts b/src/types/index.ts index b63afcf..bf5d135 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,3 +12,24 @@ export interface Match { type Json = string | number | boolean | null | { [key: string]: Json } | Json[]; export type JsonObjectOrArray = { [key: string]: Json } | Json[]; + +/** + * Options used for detecting "words" when no explicit highlighting is used + */ +export type CursorWordOptions = + | { + /** + * Characters beyond alphanumerics and underscore that should be used to detect "words" + */ + extraWordChars?: string[]; + /** + * If true, provided regex will be wrapped in ^ and $ + */ + matchFullLine?: boolean; + } + | { + /** + * If true, words will be parsed along whitespace as delimiter + */ + useWhitespaceDelimiter: true; + }; diff --git a/src/utils.ts b/src/utils.ts index 5978eb6..b99d944 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,9 @@ import { parse } from 'comment-json'; import _ from 'lodash'; +import { match } from 'ts-pattern'; import vscode from 'vscode'; -import { JsonObjectOrArray, Match } from './types'; +import { CursorWordOptions, JsonObjectOrArray, Match } from './types'; // Some error types used to drive user messages (see handleError below) export class UserError extends Error {} @@ -112,9 +113,9 @@ export function getNextElement(arr: T[], currentValue: T): T { export function getCursorWordAsSelection( editor: vscode.TextEditor, selection: vscode.Selection, - extraWordChars: string[] = [] + cursorWordOptions: CursorWordOptions = {} ): vscode.Selection { - const regex = getWordsRegex(extraWordChars, false); + const regex = getWordsRegex(cursorWordOptions); const lineText = editor.document.lineAt(selection.start.line).text; const matches: Match[] = []; @@ -143,7 +144,7 @@ export function getCursorWordAsSelection( * @returns true if text is a single word */ export function isWord(text: string, extraWordChars: string[]): boolean { - return getWordsRegex(extraWordChars, true).test(text); + return getWordsRegex({ extraWordChars, matchFullLine: true }).test(text); } /** @@ -230,24 +231,29 @@ export async function shrinkEditorSelections(editor: vscode.TextEditor, options: /** * Helper to get a regex for words, including the provided extra characters */ -function getWordsRegex(extraWordChars: string[], matchFullLine: boolean): RegExp { - // We're using a regex "character class" (i.e., brackets), so we only need to escape '^', '-', ']', and '\' - const escapedExtraWordChars = extraWordChars.map((char) => { - if (char.length !== 1) { - throw new UserError( - `All configured extra word characters must have length 1! The following is invalid: '${char}'` - ); - } else if (/[\^\-\]\\]/.test(char)) { - return '\\' + char; - } else { - return char; - } - }); - let regexStr = `[\\w${escapedExtraWordChars.join('')}]+`; - if (matchFullLine) { - regexStr = `^${regexStr}$`; - } - return new RegExp(regexStr, 'g'); +function getWordsRegex(cursorWordOptions: CursorWordOptions): RegExp { + return match(cursorWordOptions) + .with({ useWhitespaceDelimiter: true }, () => /\S+/g) + .otherwise((matched) => { + // Default to traditional "word" characters, with some customization options + const escapedExtraWordChars = (matched.extraWordChars ?? []).map((char) => { + if (char.length !== 1) { + throw new UserError( + `All configured extra word characters must have length 1! The following is invalid: '${char}'` + ); + } else if (/[\^\-\]\\]/.test(char)) { + return '\\' + char; + } else { + return char; + } + }); + // We're using a regex "character class" (i.e., brackets), so we only need to escape '^', '-', ']', and '\' + let regexStr = `[\\w${escapedExtraWordChars.join('')}]+`; + if (matched.matchFullLine) { + regexStr = `^${regexStr}$`; + } + return new RegExp(regexStr, 'g'); + }); } type CollectTxFunction = (el: T, idx: number) => R | undefined;