1- import { range , zip } from "lodash" ;
21import { ensureSingleTarget } from "../../util/targetUtils" ;
32
43import { open } from "fs/promises" ;
54import { join } from "path" ;
6- import { commands , window , workspace } from "vscode" ;
7- import { performEditsAndUpdateSelections } from "../../core/updateSelections/updateSelections " ;
5+ import { commands , Range , window , workspace } from "vscode" ;
6+ import { Offsets } from "../../processTargets/modifiers/surroundingPair/types " ;
87import { Target } from "../../typings/target.types" ;
98import { Graph } from "../../typings/Types" ;
10- import { performDocumentEdits } from "../../util/performDocumentEdits" ;
119import { Action , ActionReturnValue } from "../actions.types" ;
12- import Substituter from "./Substituter" ;
1310import { constructSnippetBody } from "./constructSnippetBody" ;
11+ import { editText } from "./editText" ;
12+ import Substituter from "./Substituter" ;
1413
1514interface Variable {
15+ /**
16+ * The start an end offsets of the variable relative to the text of the
17+ * snippet that contains it
18+ */
19+ offsets : Offsets ;
20+
1621 /**
1722 * The default name for the given variable that will appear as the placeholder
1823 * text in the meta snippet
@@ -42,12 +47,13 @@ interface Variable {
4247 *
4348 * 1. Ask user for snippet name if not provided as arg
4449 * 2. Find all cursor selections inside target
45- * 3. Replace cursor selections in document with random ids that won't be
50+ * 3. Extract text of target
51+ * 4. Replace cursor selections in text with random ids that won't be
4652 * affected by json serialization. After serialization we'll replace these
47- * id's by snippet placeholders. Note that this modifies the document, so we
48- * need to reset later.
49- * 4 . Construct a snippet body as a list of strings
50- * 5. Construct a snippet as a javascript object
53+ * id's by snippet placeholders.
54+ * 4. Construct the user snippet body as a list of strings
55+ * 5 . Construct a javascript object that will be json-ified to become the meta
56+ * snippet
5157 * 6. Serialize the javascript object to json
5258 * 7. Perform replacements on the random id's appearing in this json to get the
5359 * text we desire. This modified json output is the meta snippet.
@@ -88,72 +94,100 @@ export default class GenerateSnippet implements Action {
8894 /** The next placeholder index to use for the meta snippet */
8995 let nextPlaceholderIndex = 1 ;
9096
91- /**
92- * The original selections and the editor that will become variables in the
93- * user snippet
94- */
95- const originalVariableSelections = editor . selections . filter ( ( selection ) =>
96- target . contentRange . contains ( selection )
97- ) ;
98-
99- /**
100- * The original text of the selections where the variables were in the
101- * document to use to restore the document contents when we're done.
102- */
103- const originalVariableTexts = originalVariableSelections . map ( ( selection ) =>
104- editor . document . getText ( selection )
105- ) ;
97+ const baseOffset = editor . document . offsetAt ( target . contentRange . start ) ;
10698
10799 /**
108100 * The variables that will appear in the user snippet. Note that
109101 * `placeholderIndex` here is the placeholder index in the meta snippet not
110102 * the user snippet.
111103 */
112- const variables : Variable [ ] = range ( originalVariableSelections . length ) . map (
113- ( index ) => ( {
104+ const variables : Variable [ ] = editor . selections
105+ . filter ( ( selection ) => target . contentRange . contains ( selection ) )
106+ . map ( ( selection , index ) => ( {
107+ offsets : {
108+ start : editor . document . offsetAt ( selection . start ) - baseOffset ,
109+ end : editor . document . offsetAt ( selection . end ) - baseOffset ,
110+ } ,
114111 defaultName : `variable${ index + 1 } ` ,
115112 placeholderIndex : nextPlaceholderIndex ++ ,
116- } )
117- ) ;
113+ } ) ) ;
118114
119115 /**
120116 * Constructs random ids that can be put into the text that won't be
121117 * modified by json serialization.
122118 */
123119 const substituter = new Substituter ( ) ;
124120
125- const [ variableSelections , [ targetSelection ] ] =
126- await performEditsAndUpdateSelections (
127- this . graph . rangeUpdater ,
128- editor ,
129- originalVariableSelections . map ( ( selection , index ) => ( {
130- editor,
131- range : selection ,
132- text : substituter . addSubstitution (
133- [
134- "\\$${" ,
135- variables [ index ] . placeholderIndex ,
136- ":" ,
137- variables [ index ] . defaultName ,
138- "}" ,
139- ] . join ( "" )
140- ) ,
141- } ) ) ,
142- [ originalVariableSelections , [ target . contentSelection ] ]
143- ) ;
144-
145- const snippetLines = constructSnippetBody ( editor , targetSelection ) ;
121+ const linePrefix = editor . document . getText (
122+ new Range (
123+ target . contentRange . start . with ( undefined , 0 ) ,
124+ target . contentRange . start
125+ )
126+ ) ;
146127
147- await performDocumentEdits (
148- this . graph . rangeUpdater ,
149- editor ,
150- zip ( variableSelections , originalVariableTexts ) . map ( ( [ range , text ] ) => ( {
151- editor,
152- range : range ! ,
153- text : text ! ,
128+ /** The text of the snippet, with placeholders inserted for variables */
129+ const snippetBodyText = editText (
130+ editor . document . getText ( target . contentRange ) ,
131+ variables . map ( ( { offsets, defaultName, placeholderIndex } ) => ( {
132+ offsets,
133+ text : substituter . addSubstitution (
134+ [
135+ // This `\$` will end up being a `$` in the final document. It
136+ // indicates the start of a variable in the user snippet. We need
137+ // the `\` so that the meta-snippet doesn't see it as one of its
138+ // placeholders.
139+ // Note that the reason we use the substituter here is primarily so
140+ // that the `\` here doesn't get escaped upon conversion to json.
141+ "\\$" ,
142+
143+ // The remaining text here is a placeholder in the meta-snippet
144+ // that the user can use to name their snippet variable that will
145+ // be in the user snippet.
146+ "${" ,
147+ placeholderIndex ,
148+ ":" ,
149+ defaultName ,
150+ "}" ,
151+ ] . join ( "" )
152+ ) ,
154153 } ) )
155154 ) ;
156155
156+ const snippetLines = constructSnippetBody ( snippetBodyText , linePrefix ) ;
157+
158+ /**
159+ * Constructs a key-value entry for use in the variable description section
160+ * of the user snippet definition. It contains tabstops for use in the
161+ * meta-snippet.
162+ * @param variable The variable
163+ * @returns A [key, value] pair for use in the meta-snippet
164+ */
165+ const constructVariableDescriptionEntry = ( {
166+ placeholderIndex,
167+ } : Variable ) : [ string , string ] => {
168+ // The key will have the same placeholder index as the other location
169+ // where this variable appears.
170+ const key = "$" + placeholderIndex ;
171+
172+ // The value will end up being an empty object with a tabstop in the
173+ // middle so that the user can add information about the variable, such
174+ // as wrapperScopeType. Ie the output will look like `{|}` (with the `|`
175+ // representing a tabstop in the meta-snippet)
176+ //
177+ // NB: We use the subsituter here, with `isQuoted=true` because in order
178+ // to make this work for the meta-snippet, we want to end up with
179+ // something like `{$3}`, which is not valid json. So we instead arrange
180+ // to end up with json like `"hgidfsivhs"`, and then replace the whole
181+ // string (including quotes) with `{$3}` after json-ification
182+ const value = substituter . addSubstitution (
183+ "{$" + nextPlaceholderIndex ++ + "}" ,
184+ true
185+ ) ;
186+
187+ return [ key , value ] ;
188+ } ;
189+
190+ /** An object that will be json-ified to become the meta-snippet */
157191 const snippet = {
158192 [ snippetName ] : {
159193 definitions : [
@@ -166,23 +200,22 @@ export default class GenerateSnippet implements Action {
166200 ] ,
167201 description : `$${ nextPlaceholderIndex ++ } ` ,
168202 variables :
169- originalVariableSelections . length === 0
203+ variables . length === 0
170204 ? undefined
171205 : Object . fromEntries (
172- range ( originalVariableSelections . length ) . map ( ( index ) => [
173- `$${ variables [ index ] . placeholderIndex } ` ,
174- substituter . addSubstitution (
175- `{$${ nextPlaceholderIndex ++ } }` ,
176- true
177- ) ,
178- ] )
206+ variables . map ( constructVariableDescriptionEntry )
179207 ) ,
180208 } ,
181209 } ;
210+
211+ /**
212+ * This is the text of the meta-snippet in Textmate format that we will
213+ * insert into the new document where the user will fill out their snippet
214+ * definition
215+ */
182216 const snippetText = substituter . makeSubstitutions (
183217 JSON . stringify ( snippet , null , 2 )
184218 ) ;
185- console . debug ( snippetText ) ;
186219
187220 const userSnippetsDir = workspace
188221 . getConfiguration ( "cursorless.experimental" )
0 commit comments