Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit d6abd4e

Browse files
committed
progress on tests
1 parent f3bb0e9 commit d6abd4e

File tree

2 files changed

+91
-33
lines changed

2 files changed

+91
-33
lines changed

lib/snippet-body.pegjs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
33
Target grammar:
44
5-
(Based on VS Code and TextMate, with particular emphasis on supporting LSP snippets)
6-
See https://microsoft.github.io/language-server-protocol/specification#snippet_syntax
7-
85
any ::= (text | tabstop | choice | variable)*
96
107
text ::= anything that's not something else
@@ -31,6 +28,15 @@ var ::= [a-zA-Z_][a-zA-Z_0-9]*
3128
3229
int ::= [0-9]+
3330
31+
(Based on VS Code and TextMate, with particular emphasis on supporting LSP snippets)
32+
See https://microsoft.github.io/language-server-protocol/specification#snippet_syntax
33+
34+
Parse issues (such as unclosed tab stops or invalid regexes) are simply treated
35+
as plain text.
36+
37+
NOTE: PEG.js is not designed for efficiency. With appropriate benchmarks, it should
38+
be a significant gain to hand write a parser.
39+
3440
*/
3541

3642
{
@@ -128,7 +134,7 @@ flags = f:[a-z]* { return f; }
128134
// A tab stop that offers a choice between several fixed values. These values are plain text only.
129135
// This feature is not implemented, but the syntax is parsed to reserve it for future use.
130136
// It will currently just default to a regular tab stop with the first value as it's placeholder.
131-
choice = '${' n:integer '|' choiceText (',' choiceText)* '|}'
137+
choice = '${' n:integer '|' a:choiceText b:(',' c:choiceText { return c; } )* '|}' { return { index: n, choices: [a, ...b] }; }
132138

133139
// Syntactically looks like a named tab stop. Variables are resolved in JS and may be
134140
// further processed with a transformation. Unrecognised variables are transformed into
@@ -150,7 +156,7 @@ text = t:([^$\\}])+ { return t.join("") }
150156
tabStopText = text
151157

152158
// None-special text inside a choice. $, {, }, etc. are all regular text in this context.
153-
choiceText = t:[^,|]+ { return t.join(""); }
159+
choiceText = b:(t:[^,|\\]+ { return t.join(""); } / '\\' c:[,|\\] { return c; } / '\\' c:. { return '\\' + c; } )* { return b.join(""); }
154160

155161
// None-special text inside a replace (substitution part of transformation). Same as normal text, but `/` is special (the end of the regex-like pattern)
156162
replaceText = t:[^$\\}/]+ { return t.join(""); }
@@ -168,7 +174,7 @@ escapedChoice = '\\' c:[$\\,|] { return c; }
168174
escapedReplace = '\\' c:[$\\/] { return c; }
169175

170176
// We handle 'modifiers' separately to escapes. These indicate a change in state when building the replacement (e.g., capitalisation)
171-
replaceModifier = '\\' m:[uUlL] { return { modifier: m }; }
177+
replaceModifier = '\\' m:[uUlLeEnr] { return { modifier: m }; }
172178

173179
// Match nonnegative integers like those used for tab stop ordering
174180
integer = digits:[0-9]+ { return parseInt(digits.join(""), 10); }

spec/body-parser-spec.js

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,76 @@
11
const BodyParser = require('../lib/snippet-body-parser');
22

33
describe("Snippet Body Parser", () => {
4-
function t(snippetBody) {
5-
return BodyParser.parse(snippetBody);
4+
function expectMatch(input, tree) {
5+
expect(BodyParser.parse(input)).toEqual(tree);
66
}
77

8-
it("breaks a snippet body into lines, with each line containing tab stops at the appropriate position", () => {
9-
const bodyTree = BodyParser.parse(`\
10-
the quick brown $1fox \${2:jumped \${3:over}
11-
}the \${4:lazy} dog\
12-
`
13-
);
8+
it("parses snippets with no special behaviour as plain text", () => {
9+
const plainSnippets = [
10+
"foo $ bar",
11+
"$% $ 1 ${/upcase} \n ${|world|} ${3foo}",
12+
];
1413

15-
expect(bodyTree).toEqual([
14+
for (const plain of plainSnippets) {
15+
expectMatch(plain, [plain]);
16+
}
17+
});
18+
19+
it("parses simple tab stops", () => {
20+
expectMatch("hello$1world${2}", [
21+
"hello", {index: 1, content: []}, "world", {index: 2, content: []},
22+
]);
23+
});
24+
25+
it("doesn't find escaped tab stops", () => {
26+
expectMatch("\\$1", ["$1"]);
27+
})
28+
29+
it("parses simple variables", () => {
30+
expectMatch("hello$foo2__bar&baz${abc}d", [
31+
"hello", {variable: "foo2__bar"}, "&baz", {variable: "abc"}, "d"
32+
]);
33+
});
34+
35+
describe("only escapes a select few characters", () => {
36+
const escapeTest = "\\$ \\\\ \\} \\% \\* \\, \\| \\{ \\n \\r \\:";
37+
38+
const escapeResolveTop = "$ \\ } \\% \\* \\, \\| \\{ \\n \\r \\:";
39+
40+
const escapeResolveChoice = "\\$ \\ \\} \\% \\* , | \\{ \\n \\r \\:";
41+
42+
it("only escapes '$', '\\', and '}' in top level text", () => {
43+
expectMatch(escapeTest, [
44+
escapeResolveTop
45+
]);
46+
});
47+
48+
it("escapes the same characters inside tab stop placeholders as in top level text", () => {
49+
expectMatch(`\${1:${escapeTest}}`, [
50+
{index: 1, content: [escapeResolveTop]},
51+
]);
52+
});
53+
54+
it("escapes the same characters inside variable placeholders as in top level text", () => {
55+
expectMatch(`\${foo:${escapeTest}}`, [
56+
{variable: "foo", content: [escapeResolveTop]},
57+
]);
58+
});
59+
60+
it("escapes ',', '|', and '\\' in choice text", () => {
61+
expectMatch(`\${1|${escapeTest}|}`, [
62+
{index: 1, choices: [escapeResolveChoice]},
63+
]);
64+
});
65+
});
66+
67+
it("does not recognise a tab stop with transformation if the transformation is invalid regex", () => {
68+
expectMatch("${1/foo/bar/a}", ["${1/foo/bar/a}"]); // invalid flag
69+
expectMatch("${1/fo)o/bar/}", ["${1/fo)o/bar/}"]); // invalid body
70+
});
71+
72+
it("breaks a snippet body into lines, with each line containing tab stops at the appropriate position", () => {
73+
expectMatch("the quick brown $1fox ${2:jumped ${3:over}\n}the ${4:lazy} dog", [
1674
"the quick brown ",
1775
{index: 1, content: []},
1876
"fox ",
@@ -31,35 +89,29 @@ the quick brown $1fox \${2:jumped \${3:over}
3189
});
3290

3391
it("removes interpolated variables in placeholder text (we don't currently support it)", () => {
34-
const bodyTree = BodyParser.parse("module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}}");
35-
expect(bodyTree).toEqual([
92+
expect(t("module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}}")).toEqual([
3693
"module ",
3794
{
38-
"index": 1,
39-
"content": ["ActiveRecord::", ""]
95+
index: 1,
96+
content: ["ActiveRecord::", ""]
4097
}
4198
]);
4299
});
43100

44101
it("skips escaped tabstops", () => {
45-
const bodyTree = BodyParser.parse("snippet $1 escaped \\$2 \\\\$3");
46-
expect(bodyTree).toEqual([
47-
"snippet ",
48-
{
49-
index: 1,
50-
content: []
51-
},
52-
" escaped $2 \\",
53-
{
54-
index: 3,
55-
content: []
56-
}
102+
expectMatch("$1 \\$2 $3 \\\\$4 \\\\\\$5 $6", [
103+
{index: 1, content: []},
104+
' $2 ',
105+
{index: 3, content: []},
106+
' \\',
107+
{index: 4, content: []},
108+
' \\$5 ',
109+
{index: 6, content: []}
57110
]);
58111
});
59112

60113
it("includes escaped right-braces", () => {
61-
const bodyTree = BodyParser.parse("snippet ${1:{\\}}");
62-
expect(bodyTree).toEqual([
114+
expectMatch("snippet ${1:{\\}}", [
63115
"snippet ",
64116
{
65117
index: 1,

0 commit comments

Comments
 (0)