Skip to content

Commit

Permalink
Merge pull request #20 from savetheclocktower/support-transform-flags…
Browse files Browse the repository at this point in the history
…-on-sed-replacements

Extend support for simple transformation flags to sed-style replacements
  • Loading branch information
savetheclocktower committed Apr 14, 2024
2 parents 6aa1607 + 4235978 commit f10649f
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 51 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 22 additions & 15 deletions lib/replacer.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const FLAGS = require('./simple-transformations')

const ESCAPES = {
u: (flags) => {
flags.lowercaseNext = false
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
47 changes: 47 additions & 0 deletions lib/simple-transformations.js
Original file line number Diff line number Diff line change
@@ -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
36 changes: 1 addition & 35 deletions lib/variable.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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 }
Expand Down
82 changes: 82 additions & 0 deletions spec/snippets-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:)/}'
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit f10649f

Please sign in to comment.