From 42359781e9ba5a04c32e2ed951c0fa41e37fe5f7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 5 Apr 2024 18:54:00 -0700 Subject: [PATCH] Extend support for simple transformation flags to sed-style replacements --- README.md | 20 ++++++++- lib/replacer.js | 37 +++++++++------- lib/simple-transformations.js | 47 ++++++++++++++++++++ lib/variable.js | 36 +-------------- spec/snippets-spec.js | 82 +++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 51 deletions(-) create mode 100644 lib/simple-transformations.js diff --git a/README.md b/README.md index 4693abc..5f15fe4 100644 --- a/README.md +++ b/README.md @@ -128,10 +128,28 @@ Pulsar supports the three flags defined in the [LSP snippets specification][lsp] * `/upcase` (`foo` → `FOO`) * `/downcase` (`BAR` → `bar`) -* `/capitalize` (`lorem ipsum dolor` → `Lorem ipsum dolor`) +* `/capitalize` (`lorem ipsum dolor` → `Lorem ipsum dolor`) *(first letter uppercased; rest of input left intact)* * `/camelcase` (`foo bar` → `fooBar`, `lorem-ipsum.dolor` → `loremIpsumDolor`) * `/pascalcase` (`foo bar` → `FooBar`, `lorem-ipsum.dolor` → `LoremIpsumDolor`) +It also supports two other common transformations: + +* `/snakecase` (`foo bar` → `foo_bar`, `lorem-ipsum.dolor` → `lorem_ipsum_dolor`) +* `/kebabcase` (`foo bar` → `foo-bar`, `lorem-ipsum.dolor` → `lorem-ipsum-dolor`) + +These transformation flags can also be applied on backreferences in `sed`-style replacements for transformed tab stops. Given the following example snippet body… + +``` +[$1] becomes [${1/(.*)/${1:/upcase}/}] +``` + +…invoking the snippet and typing `Lorem ipsum dolor` will produce: + +``` +[Lorem ipsum dolor] becomes [LOREM IPSUM DOLOR] +``` + + #### Variable caveats * `WORKSPACE_NAME`, `WORKSPACE_FOLDER`, and `RELATIVE_PATH` all rely on the presence of a root project folder, but a Pulsar project can technically have multiple root folders. While this is rare, it is handled by `snippets` as follows: whichever project path is an ancestor of the currently active file is treated as the project root — or the first one found if multiple roots are ancestors. diff --git a/lib/replacer.js b/lib/replacer.js index 78be522..75d2229 100644 --- a/lib/replacer.js +++ b/lib/replacer.js @@ -1,3 +1,5 @@ +const FLAGS = require('./simple-transformations') + const ESCAPES = { u: (flags) => { flags.lowercaseNext = false @@ -71,23 +73,28 @@ class Replacer { } else if (token.escape) { ESCAPES[token.escape](this.flags, result) } else if (token.backreference) { - let {iftext, elsetext} = token - if (iftext != null && elsetext != null) { - // If-else syntax makes choices based on the presence or absence of a - // capture group backreference. - let m = match[token.backreference] - let tokenToHandle = m ? iftext : elsetext - if (Array.isArray(tokenToHandle)) { - result.push(...tokenToHandle.map(handleToken.bind(this))) + if (token.transform && (token.transform in FLAGS)) { + let transformed = FLAGS[token.transform](match[token.backreference]) + result.push(transformed) + } else { + let {iftext, elsetext} = token + if (iftext != null && elsetext != null) { + // If-else syntax makes choices based on the presence or absence of a + // capture group backreference. + let m = match[token.backreference] + let tokenToHandle = m ? iftext : elsetext + if (Array.isArray(tokenToHandle)) { + result.push(...tokenToHandle.map(handleToken.bind(this))) + } else { + result.push(handleToken.call(this, tokenToHandle)) + } } else { - result.push(handleToken.call(this, tokenToHandle)) + let transformed = transformTextWithFlags( + match[token.backreference], + this.flags + ) + result.push(transformed) } - } else { - let transformed = transformTextWithFlags( - match[token.backreference], - this.flags - ) - result.push(transformed) } } } diff --git a/lib/simple-transformations.js b/lib/simple-transformations.js new file mode 100644 index 0000000..fde568a --- /dev/null +++ b/lib/simple-transformations.js @@ -0,0 +1,47 @@ +// Simple transformation flags that can convert a string in various ways. They +// are specified for variables and for transforming substitution +// backreferences, so we need to use them in two places. +const FLAGS = { + // These are included in the LSP spec. + upcase: value => (value || '').toLocaleUpperCase(), + downcase: value => (value || '').toLocaleLowerCase(), + capitalize: (value) => { + return !value ? '' : (value[0].toLocaleUpperCase() + value.substr(1)) + }, + + // These are supported by VSCode. + pascalcase (value) { + const match = value.match(/[a-z0-9]+/gi) + if (!match) { + return value + } + return match.map(word => { + return word.charAt(0).toUpperCase() + word.substr(1) + }).join('') + }, + camelcase (value) { + const match = value.match(/[a-z0-9]+/gi) + if (!match) { + return value + } + return match.map((word, index) => { + if (index === 0) { + return word.charAt(0).toLowerCase() + word.substr(1) + } + return word.charAt(0).toUpperCase() + word.substr(1) + }).join('') + }, + + // No reason not to implement these also. + snakecase (value) { + let camel = this.camelcase(value) + return camel.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`) + }, + + kebabcase (value) { + let camel = this.camelcase(value) + return camel.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) + } +} + +module.exports = FLAGS diff --git a/lib/variable.js b/lib/variable.js index a68731a..dc0ac50 100644 --- a/lib/variable.js +++ b/lib/variable.js @@ -1,6 +1,7 @@ const path = require('path') const crypto = require('crypto') const Replacer = require('./replacer') +const FLAGS = require('./simple-transformations') const {remote} = require('electron') function resolveClipboard () { @@ -191,41 +192,6 @@ if (('randomUUID' in crypto) && (typeof crypto.randomUUID === 'function')) { } -// Simple transformation flags that can convert a string in various ways. They -// are specified for variables but not for transforms, which is why this logic -// isn't included in the `Replacer` class. -const FLAGS = { - // These are included in the LSP spec. - upcase: value => (value || '').toLocaleUpperCase(), - downcase: value => (value || '').toLocaleLowerCase(), - capitalize: (value) => { - return !value ? '' : (value[0].toLocaleUpperCase() + value.substr(1)) - }, - - // These are supported by VSCode. - pascalcase: (value) => { - const match = value.match(/[a-z0-9]+/gi) - if (!match) { - return value - } - return match.map(word => { - return word.charAt(0).toUpperCase() + word.substr(1) - }).join('') - }, - camelcase: (value) => { - const match = value.match(/[a-z0-9]+/gi) - if (!match) { - return value - } - return match.map((word, index) => { - if (index === 0) { - return word.charAt(0).toLowerCase() + word.substr(1) - } - return word.charAt(0).toUpperCase() + word.substr(1) - }).join('') - } -} - function replaceByFlag (text, flag) { let replacer = FLAGS[flag] if (!replacer) { return text } diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 5adc137..637a55e 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -321,6 +321,46 @@ third tabstop $3\ prefix: "bannerCorrect", body: "// $1\n// ${1/./=/g}" }, + "transform with simple flag on replacement (upcase)": { + prefix: 't_simple_upcase', + body: "$1 ${1/(.*)/${1:/upcase}/}" + }, + "transform with simple flag on replacement (downcase)": { + prefix: 't_simple_downcase', + body: "$1 ${1/(.*)/${1:/downcase}/}" + }, + "transform with simple flag on replacement (capitalize)": { + prefix: 't_simple_capitalize', + body: "$1 ${1/(.*)/${1:/capitalize}/}" + }, + "transform with simple flag on replacement (camelcase)": { + prefix: 't_simple_camelcase', + body: "$1 ${1/(.*)/${1:/camelcase}/}" + }, + "transform with simple flag on replacement (pascalcase)": { + prefix: 't_simple_pascalcase', + body: "$1 ${1/(.*)/${1:/pascalcase}/}" + }, + "transform with simple flag on replacement (snakecase)": { + prefix: 't_simple_snakecase', + body: "$1 ${1/(.*)/${1:/snakecase}/}" + }, + "transform with simple flag on replacement (kebabcase)": { + prefix: 't_simple_kebabcase', + body: "$1 ${1/(.*)/${1:/kebabcase}/}" + }, + "variable reference with simple flag on replacement (upcase)": { + prefix: 'v_simple_upcase', + body: "$CLIPBOARD ${CLIPBOARD/(\\S*)(.*)/${1}${2:/upcase}/}$0" + }, + "variable reference with simple flag on replacement (pascal)": { + prefix: 'v_simple_pascalcase', + body: "$CLIPBOARD ${CLIPBOARD/(\\S*)(.*)/${1} ${2:/pascalcase}/}$0" + }, + "variable reference with simple flag on replacement (snakecase)": { + prefix: 'v_simple_snakecase', + body: "$CLIPBOARD ${CLIPBOARD/(\\S*)(.*)/${1} ${2:/snakecase}/}$0" + }, 'TM iftext but no elsetext': { prefix: 'ifelse1', body: '$1 ${1/(wat)/(?1:hey:)/}' @@ -1049,6 +1089,48 @@ foo\ }); }); + describe("when the snippet contains a transformation with a simple transform flag on a substitution", () => { + let expectations = { + upcase: `LOREM IPSUM DOLOR`, + downcase: `lorem ipsum dolor`, + capitalize: `Lorem Ipsum Dolor`, + camelcase: 'loremIpsumDolor', + pascalcase: 'LoremIpsumDolor', + snakecase: 'lorem_ipsum_dolor', + kebabcase: 'lorem-ipsum-dolor' + }; + for (let [flag, expected] of Object.entries(expectations)) { + it(`should transform ${flag} correctly`, () => { + let trigger = `t_simple_${flag}`; + editor.setText(trigger); + editor.setCursorScreenPosition([0, trigger.length]); + simulateTabKeyEvent(); + editor.insertText('lorem Ipsum Dolor'); + expect(editor.getText()).toBe(`lorem Ipsum Dolor ${expected}`); + }); + } + }); + + describe("when the snippet contains a variable with a simple transform flag within a sed-style substitution", () => { + let expectations = { + upcase: 'lorem IPSUM DOLOR', + pascalcase: 'lorem IpsumDolor', + snakecase: 'lorem ipsum_dolor', + }; + for (let [flag, expected] of Object.entries(expectations)) { + it(`should transform ${flag} correctly`, () => { + atom.clipboard.write('lorem Ipsum Dolor'); + let trigger = `v_simple_${flag}`; + console.log('expanding:', trigger); + editor.setText(trigger); + editor.setCursorScreenPosition([0, trigger.length]); + simulateTabKeyEvent(); + console.log('TEXT:', editor.getText()); + expect(editor.getText()).toBe(`lorem Ipsum Dolor ${expected}`); + }); + } + }); + describe("when the snippet contains multiple tab stops, some with transformations and some without", () => { it("does not get confused", () => { editor.setText('t14');