Skip to content

Commit 3f81c5b

Browse files
authored
Fix escaping in snippets (#1362)
- Fixes #1323 ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [ ] I have not broken the cheatsheet
1 parent 043a1dd commit 3f81c5b

File tree

3 files changed

+83
-10
lines changed

3 files changed

+83
-10
lines changed
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
tag: user.cursorless
22
-
33

4-
{user.cursorless_homophone} record: user.run_rpc_command("cursorless.recordTestCase")
5-
{user.cursorless_homophone} pause: user.run_rpc_command("cursorless.pauseRecording")
6-
{user.cursorless_homophone} resume: user.run_rpc_command("cursorless.resumeRecording")
4+
{user.cursorless_homophone} record:
5+
user.run_rpc_command("cursorless.recordTestCase")
6+
{user.cursorless_homophone} pause:
7+
user.run_rpc_command("cursorless.pauseRecording")
8+
{user.cursorless_homophone} resume:
9+
user.run_rpc_command("cursorless.resumeRecording")
710

811
{user.cursorless_homophone} record navigation:
912
user.cursorless_record_navigation_test()
1013
{user.cursorless_homophone} record error: user.cursorless_record_error_test()
1114
{user.cursorless_homophone} record highlights:
1215
user.cursorless_record_highlights_test()
1316
{user.cursorless_homophone} record that mark:
17+
user.cursorless_record_that_mark_test()
1418

1519
{user.cursorless_homophone} update cheatsheet:
16-
user.cursorless_cheat_sheet_update_json()
20+
user.cursorless_cheat_sheet_update_json()
21+
22+
test snippet make <user.cursorless_target>:
23+
user.cursorless_single_target_command_no_wait("generateSnippet", cursorless_target, "testSnippet")

packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { FlashStyle, isTesting, Range } from "@cursorless/common";
22
import { Offsets } from "../../processTargets/modifiers/surroundingPair/types";
33
import { ide } from "../../singletons/ide.singleton";
4-
import { Target } from "../../typings/target.types";
54
import { Graph } from "../../typings/Graph";
5+
import { Target } from "../../typings/target.types";
6+
import { matchAll } from "../../util/regex";
67
import { ensureSingleTarget, flashTargets } from "../../util/targetUtils";
78
import { Action, ActionReturnValue } from "../actions.types";
89
import { constructSnippetBody } from "./constructSnippetBody";
@@ -114,10 +115,22 @@ export default class GenerateSnippet implements Action {
114115
),
115116
);
116117

117-
/** The text of the snippet, with placeholders inserted for variables */
118-
const snippetBodyText = editText(
119-
editor.document.getText(target.contentRange),
120-
variables.map(({ offsets, defaultName, placeholderIndex }) => ({
118+
const originalText = editor.document.getText(target.contentRange);
119+
120+
/**
121+
* The text of the snippet, with placeholders inserted for variables and
122+
* special characters `$`, `\`, and `}` escaped twice to make it through
123+
* both meta snippet and user snippet.
124+
*/
125+
const snippetBodyText = editText(originalText, [
126+
...matchAll(originalText, /\$|\\/g, (match) => ({
127+
offsets: {
128+
start: match.index!,
129+
end: match.index! + match[0].length,
130+
},
131+
text: match[0] === "\\" ? `\\${match[0]}` : `\\\\${match[0]}`,
132+
})),
133+
...variables.map(({ offsets, defaultName, placeholderIndex }) => ({
121134
offsets,
122135
// Note that the reason we use the substituter here is primarily so
123136
// that the `\` below doesn't get escaped upon conversion to json.
@@ -140,7 +153,7 @@ export default class GenerateSnippet implements Action {
140153
].join(""),
141154
),
142155
})),
143-
);
156+
]);
144157

145158
const snippetLines = constructSnippetBody(snippetBodyText, linePrefix);
146159

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
languageId: plaintext
2+
command:
3+
version: 4
4+
spokenForm: test snippet make line
5+
action:
6+
name: generateSnippet
7+
args: [testSnippet]
8+
targets:
9+
- type: primitive
10+
modifiers:
11+
- type: containingScope
12+
scopeType: {type: line}
13+
usePrePhraseSnapshot: true
14+
initialState:
15+
documentContents: \textbf{$foo}
16+
selections:
17+
- anchor: {line: 0, character: 9}
18+
active: {line: 0, character: 12}
19+
marks: {}
20+
finalState:
21+
documentContents: |-
22+
{
23+
"testSnippet": {
24+
"definitions": [
25+
{
26+
"scope": {
27+
"langIds": [
28+
"plaintext"
29+
]
30+
},
31+
"body": [
32+
"\\textbf{\\$$variable1}"
33+
]
34+
}
35+
],
36+
"description": "",
37+
"variables": {
38+
"variable1": {}
39+
}
40+
}
41+
}
42+
selections:
43+
- anchor: {line: 10, character: 24}
44+
active: {line: 10, character: 33}
45+
- anchor: {line: 16, character: 7}
46+
active: {line: 16, character: 16}
47+
thatMark:
48+
- type: UntypedTarget
49+
contentRange:
50+
start: {line: 0, character: 0}
51+
end: {line: 0, character: 13}
52+
isReversed: false
53+
hasExplicitRange: true

0 commit comments

Comments
 (0)