From a1ff497aabbcf20c231a3445307692662b081d36 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 6 Feb 2019 00:01:06 +1000 Subject: [PATCH 01/77] Start on LSP syntax parser --- lib/lsp-snippet-body.pegjs | 91 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 lib/lsp-snippet-body.pegjs diff --git a/lib/lsp-snippet-body.pegjs b/lib/lsp-snippet-body.pegjs new file mode 100644 index 00000000..e00d05f0 --- /dev/null +++ b/lib/lsp-snippet-body.pegjs @@ -0,0 +1,91 @@ +{ + // Joins all consecutive strings in a collection without clobbering any + // non-string members. + function coalesce (parts) { + const result = []; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const ri = result.length - 1; + if (typeof part === 'string' && typeof result[ri] === 'string') { + result[ri] = result[ri] + part; + } else { + result.push(part); + } + } + return result; + } + + function flatten (parts) { + return parts.reduce(function (flat, rest) { + return flat.concat(Array.isArray(rest) ? flatten(rest) : rest); + }, []); + } + + function makeInteger(i) { + return parseInt(i.join(""), 10); + } +} + +bodyContent = content:(tabstop / placeholder / choice / variable)* { return content; } + +tabstop = simpleTabstop / tabStopWithoutPlaceholder / tabStopWithTransformation + +simpleTabstop = '$' index:int { + return { index: makeInteger(index), content: [] } +} + +tabStopWithoutPlaceholder = '${' index:int '}' { + return { index: makeInteger(index), content: [] } +} + +tabStopWithTransformation = '${' index:int substitution:transform '}' { + return { + index: makeInteger(index), + content: [], + substitution: substitution + } +} + + +placeholder = '${' index:int ':' content:bodyContent '}' { + return { index: makeInteger(index), content: content } +} + +choice = '${' index:int '|' foo:choicecontents '|}' { + return { foo } +} + +choicecontents = elem:choicetext rest:(',' rest:choicecontents { return rest } )? { + if (rest) { + return [elem, ...rest] + } + return [elem] +} + +choicetext = choicetext:(escaped / [^|,] / '|' [^}] )+ { + return choicetext.join('') +} + +transform = '/' regex:regex '/' replace:replace '/' { + return { regex: regex, format: replace } +} + +regex = regex:(escaped / [^/])* { + return regex.join('') +} + +replace = format + +format = '$' index:int { + return { index: makeInteger(index) } +} + +variable = [a-zA-Z_]+ + +int = [0-9]+ + +escaped = '\\' char:. { return char } + +token = escaped / !tabstop char:. { return char } + +text = text:token+ { return text.join('') } \ No newline at end of file From bb033361054ad0ac007f9188d92abc53ca47621a Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 6 Feb 2019 15:32:50 +1000 Subject: [PATCH 02/77] Update grammar for 95% compliance --- lib/snippet-body.pegjs | 163 ++++++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 61 deletions(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index 476c65af..1a29e148 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -1,82 +1,123 @@ { - // Joins all consecutive strings in a collection without clobbering any - // non-string members. - function coalesce (parts) { - const result = []; - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const ri = result.length - 1; - if (typeof part === 'string' && typeof result[ri] === 'string') { - result[ri] = result[ri] + part; - } else { - result.push(part); - } - } - return result; + function makeInteger(i) { + return parseInt(i.join(""), 10); } +} - function flatten (parts) { - return parts.reduce(function (flat, rest) { - return flat.concat(Array.isArray(rest) ? flatten(rest) : rest); - }, []); - } +bodyContent = content:(tabstop / choice / variable / text)* { return content; } + +innerBodyContent = content:(tabstop / choice / variable / nonCloseBraceText)* { return content; } + +tabstop = simpleTabstop / tabstopWithoutPlaceholder / tabstopWithPlaceholder / tabstopWithTransform + +simpleTabstop = '$' index:int { + return { index: makeInteger(index), content: [] } } -bodyContent = content:(tabStop / bodyContentText)* { return content; } -bodyContentText = text:bodyContentChar+ { return text.join(''); } -bodyContentChar = escaped / !tabStop char:. { return char; } -escaped = '\\' char:. { return char; } -tabStop = tabStopWithTransformation / tabStopWithPlaceholder / tabStopWithoutPlaceholder / simpleTabStop +tabstopWithoutPlaceholder = '${' index:int '}' { + return { index: makeInteger(index), content: [] } +} -simpleTabStop = '$' index:[0-9]+ { - return { index: parseInt(index.join("")), content: [] }; +tabstopWithPlaceholder = '${' index:int ':' content:innerBodyContent '}' { + return { index: makeInteger(index), content: content } } -tabStopWithoutPlaceholder = '${' index:[0-9]+ '}' { - return { index: parseInt(index.join("")), content: [] }; + +tabstopWithTransform = '${' index:int substitution:transform '}' { + return { + index: makeInteger(index), + content: [], + substitution: substitution + } } -tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' { - return { index: parseInt(index.join("")), content: content }; + +choice = '${' index:int '|' choice:choicecontents '|}' { + return { index: makeInteger(index), choice: choice } } -tabStopWithTransformation = '${' index:[0-9]+ substitution:transformationSubstitution '}' { - return { - index: parseInt(index.join(""), 10), - content: [], - substitution: substitution - }; + +choicecontents = elem:choicetext rest:(',' val:choicetext { return val } )* { + return [elem, ...rest] } -placeholderContent = content:(tabStop / placeholderContentText / variable )* { return flatten(content); } -placeholderContentText = text:placeholderContentChar+ { return coalesce(text); } -placeholderContentChar = escaped / placeholderVariableReference / !tabStop !variable char:[^}] { return char; } +choicetext = choicetext:(escaped / [^|,] / barred:('|' &[^}]) { return barred.join('') } )+ { + return choicetext.join('') +} -placeholderVariableReference = '$' digit:[0-9]+ { - return { index: parseInt(digit.join(""), 10), content: [] }; +// Transform is applied when tabbed off +transform = '/' regex:regex '/' replace:replace '/' flags:flags { + return { regex: regex, format: replace, flags: flags } } -variable = '${' variableContent '}' { - return ''; // we eat variables and do nothing with them for now +regex = regex:(escaped / [^/])* { + return regex.join('') // TODO: make regex } -variableContent = content:(variable / variableContentText)* { return content; } -variableContentText = text:variableContentChar+ { return text.join(''); } -variableContentChar = !variable char:('\\}' / [^}]) { return char; } -escapedForwardSlash = pair:'\\/' { return pair; } +replace = (format / replacetext)* + +// TODO: Format with conditionals on match +format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform + +simpleFormat = '$' index:int { + return { index: makeInteger(index) } +} -// A pattern and replacement for a transformed tab stop. -transformationSubstitution = '/' find:(escapedForwardSlash / [^/])* '/' replace:formatString* '/' flags:[imy]* { - let reFind = new RegExp(find.join(''), flags.join('') + 'g'); - return { find: reFind, replace: replace[0] }; +formatWithoutPlaceholder = '${' index:int '}' { + return { index: makeInteger(index) } } -formatString = content:(formatStringEscape / formatStringReference / escapedForwardSlash / [^/])+ { - return content; +formatWithCaseTransform = '${' index:int ':' casetransform:casetransform '}' { + return { index: makeInteger(index), transform: casetransform } } -// Backreferencing a substitution. Different from a tab stop. -formatStringReference = '$' digits:[0-9]+ { - return { backreference: parseInt(digits.join(''), 10) }; -}; -// One of the special control flags in a format string for case folding and -// other tasks. -formatStringEscape = '\\' flag:[ULulErn$] { - return { escape: flag }; + +casetransform = '/' type:[a-zA-Z]* { + type = type.join('') + switch (type) { + case 'upcase': + case 'downcase': + case 'capitalize': + return type + default: + return 'none' + } +} + +replacetext = replacetext:(escaped / !format char:[^/] { return char })+ { + return replacetext.join('') } + +variable = simpleVariable / variableWithoutPlaceholder / variableWithPlaceholder / variableWithTransform + +simpleVariable = '$' name:variableName { + return { variable: name } +} + +variableWithoutPlaceholder = '${' name:variableName '}' { + return { variable: name } +} + +variableWithPlaceholder = '${' name:variableName ':' content:innerBodyContent '}' { + return { variable: name, content: content } +} + +variableWithTransform = '${' name:variableName substitution:transform '}' { + return { variable: name, substitution: substitution } +} + +variableName = first:[a-zA-Z_] rest:[a-zA-Z_0-9]* { + return first + rest.join('') +} + +int = [0-9]+ + +escaped = '\\' char:. { return char } + +token = escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:. { return char } + +flags = flags:[a-z]* { + return flags.join('') + 'g' +} + +text = text:token+ { return text.join('') } + +nonCloseBraceText = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:[^}] { return char })+ { + return text.join('') +} \ No newline at end of file From 08019c5f83ed6f7e0ab64be6bef142b567cdefb2 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 6 Feb 2019 15:54:00 +1000 Subject: [PATCH 03/77] Ony escape necessary characters --- lib/snippet-body.pegjs | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index 1a29e148..0fdc96e3 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -38,7 +38,7 @@ choicecontents = elem:choicetext rest:(',' val:choicetext { return val } )* { return [elem, ...rest] } -choicetext = choicetext:(escaped / [^|,] / barred:('|' &[^}]) { return barred.join('') } )+ { +choicetext = choicetext:(choiceEscaped / [^|,] / barred:('|' &[^}]) { return barred.join('') } )+ { return choicetext.join('') } @@ -72,11 +72,11 @@ casetransform = '/' type:[a-zA-Z]* { type = type.join('') switch (type) { case 'upcase': - case 'downcase': - case 'capitalize': - return type - default: - return 'none' + case 'downcase': + case 'capitalize': + return type + default: + return 'none' } } @@ -108,7 +108,29 @@ variableName = first:[a-zA-Z_] rest:[a-zA-Z_0-9]* { int = [0-9]+ -escaped = '\\' char:. { return char } +escaped = '\\' char:. { + switch (char) { + case '$': + case '\\': + case '\x7D': + return char + default: + return '\\' + char + } +} + +choiceEscaped = '\\' char:. { + switch (char) { + case '$': + case '\\': + case '\x7D': + case '|': + case ',': + return char + default: + return '\\' + char + } +} token = escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:. { return char } From 675b7d4a1cd3c5b71340634bcfe96a792fd7edd4 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 6 Feb 2019 15:55:41 +1000 Subject: [PATCH 04/77] return regex --- lib/snippet-body.pegjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index 0fdc96e3..902527b4 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -48,7 +48,7 @@ transform = '/' regex:regex '/' replace:replace '/' flags:flags { } regex = regex:(escaped / [^/])* { - return regex.join('') // TODO: make regex + return new RegExp(regex.join('')) } replace = (format / replacetext)* From df90657c769b6353ddf09714b1f6182c0052e590 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 6 Feb 2019 15:59:42 +1000 Subject: [PATCH 05/77] formatting and tweaks --- lib/snippet-body.pegjs | 64 +++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index 902527b4..e7cf6b90 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -11,87 +11,87 @@ innerBodyContent = content:(tabstop / choice / variable / nonCloseBraceText)* { tabstop = simpleTabstop / tabstopWithoutPlaceholder / tabstopWithPlaceholder / tabstopWithTransform simpleTabstop = '$' index:int { - return { index: makeInteger(index), content: [] } + return { index: makeInteger(index), content: [] } } tabstopWithoutPlaceholder = '${' index:int '}' { - return { index: makeInteger(index), content: [] } + return { index: makeInteger(index), content: [] } } tabstopWithPlaceholder = '${' index:int ':' content:innerBodyContent '}' { - return { index: makeInteger(index), content: content } + return { index: makeInteger(index), content: content } } tabstopWithTransform = '${' index:int substitution:transform '}' { - return { - index: makeInteger(index), - content: [], - substitution: substitution - } + return { + index: makeInteger(index), + content: [], + substitution: substitution + } } choice = '${' index:int '|' choice:choicecontents '|}' { - return { index: makeInteger(index), choice: choice } + return { index: makeInteger(index), choice: choice } } choicecontents = elem:choicetext rest:(',' val:choicetext { return val } )* { - return [elem, ...rest] + return [elem, ...rest] } choicetext = choicetext:(choiceEscaped / [^|,] / barred:('|' &[^}]) { return barred.join('') } )+ { - return choicetext.join('') + return choicetext.join('') } // Transform is applied when tabbed off transform = '/' regex:regex '/' replace:replace '/' flags:flags { - return { regex: regex, format: replace, flags: flags } + return { regex: regex, format: replace, flags: flags } } regex = regex:(escaped / [^/])* { - return new RegExp(regex.join('')) + return new RegExp(regex.join('')) } replace = (format / replacetext)* -// TODO: Format with conditionals on match +// TODO: Support conditionals format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform simpleFormat = '$' index:int { - return { index: makeInteger(index) } + return { index: makeInteger(index) } } formatWithoutPlaceholder = '${' index:int '}' { - return { index: makeInteger(index) } + return { index: makeInteger(index) } } formatWithCaseTransform = '${' index:int ':' casetransform:casetransform '}' { - return { index: makeInteger(index), transform: casetransform } + return { index: makeInteger(index), transform: casetransform } } casetransform = '/' type:[a-zA-Z]* { - type = type.join('') - switch (type) { - case 'upcase': + type = type.join('') + switch (type) { + case 'upcase': case 'downcase': case 'capitalize': - return type + return type default: - return 'none' + return 'none' } } replacetext = replacetext:(escaped / !format char:[^/] { return char })+ { - return replacetext.join('') + return replacetext.join('') } variable = simpleVariable / variableWithoutPlaceholder / variableWithPlaceholder / variableWithTransform simpleVariable = '$' name:variableName { - return { variable: name } + return { variable: name } } variableWithoutPlaceholder = '${' name:variableName '}' { - return { variable: name } + return { variable: name } } variableWithPlaceholder = '${' name:variableName ':' content:innerBodyContent '}' { @@ -99,11 +99,11 @@ variableWithPlaceholder = '${' name:variableName ':' content:innerBodyContent '} } variableWithTransform = '${' name:variableName substitution:transform '}' { - return { variable: name, substitution: substitution } + return { variable: name, substitution: substitution } } variableName = first:[a-zA-Z_] rest:[a-zA-Z_0-9]* { - return first + rest.join('') + return first + rest.join('') } int = [0-9]+ @@ -112,7 +112,7 @@ escaped = '\\' char:. { switch (char) { case '$': case '\\': - case '\x7D': + case '\x7D': // back brace; PEGjs would treat it as the JS scope end though return char default: return '\\' + char @@ -132,13 +132,13 @@ choiceEscaped = '\\' char:. { } } -token = escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:. { return char } - flags = flags:[a-z]* { - return flags.join('') + 'g' + return flags.join('') + 'g' } -text = text:token+ { return text.join('') } +text = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:. { return char })+ { + return text.join('') +} nonCloseBraceText = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:[^}] { return char })+ { return text.join('') From 2cf43b8e57a1db44401eedddb627e11229eacc19 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 6 Feb 2019 16:01:28 +1000 Subject: [PATCH 06/77] remove unused file --- lib/lsp-snippet-body.pegjs | 91 -------------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 lib/lsp-snippet-body.pegjs diff --git a/lib/lsp-snippet-body.pegjs b/lib/lsp-snippet-body.pegjs deleted file mode 100644 index e00d05f0..00000000 --- a/lib/lsp-snippet-body.pegjs +++ /dev/null @@ -1,91 +0,0 @@ -{ - // Joins all consecutive strings in a collection without clobbering any - // non-string members. - function coalesce (parts) { - const result = []; - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const ri = result.length - 1; - if (typeof part === 'string' && typeof result[ri] === 'string') { - result[ri] = result[ri] + part; - } else { - result.push(part); - } - } - return result; - } - - function flatten (parts) { - return parts.reduce(function (flat, rest) { - return flat.concat(Array.isArray(rest) ? flatten(rest) : rest); - }, []); - } - - function makeInteger(i) { - return parseInt(i.join(""), 10); - } -} - -bodyContent = content:(tabstop / placeholder / choice / variable)* { return content; } - -tabstop = simpleTabstop / tabStopWithoutPlaceholder / tabStopWithTransformation - -simpleTabstop = '$' index:int { - return { index: makeInteger(index), content: [] } -} - -tabStopWithoutPlaceholder = '${' index:int '}' { - return { index: makeInteger(index), content: [] } -} - -tabStopWithTransformation = '${' index:int substitution:transform '}' { - return { - index: makeInteger(index), - content: [], - substitution: substitution - } -} - - -placeholder = '${' index:int ':' content:bodyContent '}' { - return { index: makeInteger(index), content: content } -} - -choice = '${' index:int '|' foo:choicecontents '|}' { - return { foo } -} - -choicecontents = elem:choicetext rest:(',' rest:choicecontents { return rest } )? { - if (rest) { - return [elem, ...rest] - } - return [elem] -} - -choicetext = choicetext:(escaped / [^|,] / '|' [^}] )+ { - return choicetext.join('') -} - -transform = '/' regex:regex '/' replace:replace '/' { - return { regex: regex, format: replace } -} - -regex = regex:(escaped / [^/])* { - return regex.join('') -} - -replace = format - -format = '$' index:int { - return { index: makeInteger(index) } -} - -variable = [a-zA-Z_]+ - -int = [0-9]+ - -escaped = '\\' char:. { return char } - -token = escaped / !tabstop char:. { return char } - -text = text:token+ { return text.join('') } \ No newline at end of file From 8f022fc4f864ed3478cfdd9cb60a037268e9ca34 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 7 Feb 2019 12:28:32 +1000 Subject: [PATCH 07/77] Add variable support --- lib/snippet-expansion.js | 13 ++++++--- lib/snippet.js | 61 ++++++++++++++++++++++++++++------------ lib/snippets.js | 10 +++++-- lib/variable-resolver.js | 27 ++++++++++++++++++ package.json | 6 ++++ 5 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 lib/variable-resolver.js diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 54a525bc..d8ffa682 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -14,14 +14,19 @@ module.exports = class SnippetExpansion { this.selections = [this.cursor.selection] const startPosition = this.cursor.selection.getBufferRange().start - let {body, tabStopList} = this.snippet - let tabStops = tabStopList.toArray() + let {body, tabStopList} = this.snippet.toString() + + this.tabStopList = tabStopList + + let tabStops = this.tabStopList.toArray() let indent = this.editor.lineTextForBufferRow(startPosition.row).match(/^\s*/)[0] if (this.snippet.lineCount > 1 && indent) { // Add proper leading indentation to the snippet body = body.replace(/\n/g, `\n${indent}`) + // TODO: Remove concept of "body"; build on the fly each time to resolve variables and their transformations + tabStops = tabStops.map(tabStop => tabStop.copyWithIndent(indent)) } @@ -29,7 +34,7 @@ module.exports = class SnippetExpansion { this.ignoringBufferChanges(() => { this.editor.transact(() => { const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) - if (this.snippet.tabStopList.length > 0) { + if (this.tabStopList.length > 0) { this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) this.placeTabStopMarkers(startPosition, tabStops) @@ -152,7 +157,7 @@ module.exports = class SnippetExpansion { } else { // The user has tabbed past the last tab stop. If the last tab stop is a // $0, we shouldn't move the cursor any further. - if (this.snippet.tabStopList.hasEndStop) { + if (this.tabStopList.hasEndStop) { foo this.destroy() return false } else { diff --git a/lib/snippet.js b/lib/snippet.js index fcdfed90..69e0c51f 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -1,56 +1,81 @@ const {Range} = require('atom') const TabStopList = require('./tab-stop-list') + +/* + +1. Snippet stores the parsed snippet template + +2. Template variables are resolved on demand + +3. Followed by insertion + +*/ + module.exports = class Snippet { - constructor({name, prefix, bodyText, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree}) { + constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, variableResolver}) { this.name = name this.prefix = prefix - this.bodyText = bodyText this.description = description this.descriptionMoreURL = descriptionMoreURL this.rightLabelHTML = rightLabelHTML this.leftLabel = leftLabel this.leftLabelHTML = leftLabelHTML - this.tabStopList = new TabStopList(this) - this.body = this.extractTabStops(bodyTree) + this.bodyTree = bodyTree + this.variableResolver = variableResolver } - extractTabStops (bodyTree) { + toString () { + const tabStopList = new TabStopList(this) const bodyText = [] let row = 0 let column = 0 // recursive helper function; mutates vars above let extractTabStops = bodyTree => { - for (const segment of bodyTree) { + for (let segment of bodyTree) { if (segment.index != null) { let {index, content, substitution} = segment if (index === 0) { index = Infinity; } const start = [row, column] extractTabStops(content) const range = new Range(start, [row, column]) - const tabStop = this.tabStopList.findOrCreate({ + const tabStop = tabStopList.findOrCreate({ index, snippet: this }) tabStop.addInsertion({ range, substitution }) - } else if (typeof segment === 'string') { - bodyText.push(segment) - var segmentLines = segment.split('\n') - column += segmentLines.shift().length - let nextLine - while ((nextLine = segmentLines.shift()) != null) { - row += 1 - column = nextLine.length + } else { + if (segment.variable != undefined) { + debugger + const value = this.variableResolver.resolve({ name: segment.variable }) + if (value === undefined) { + if (segment.content) { + extractTabStops(segment.content) + } + } else { + segment = value + } + } + + if (typeof segment === 'string') { + bodyText.push(segment) + var segmentLines = segment.split('\n') + column += segmentLines.shift().length + let nextLine + while ((nextLine = segmentLines.shift()) != null) { + row += 1 + column = nextLine.length + } } } } } - extractTabStops(bodyTree) + extractTabStops(this.bodyTree) this.lineCount = row + 1 - this.insertions = this.tabStopList.getInsertions() + this.insertions = tabStopList.getInsertions() - return bodyText.join('') + return { body: bodyText.join(''), tabStopList } } } diff --git a/lib/snippets.js b/lib/snippets.js index 1c73f14e..48228739 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -9,6 +9,7 @@ const ScopedPropertyStore = require('scoped-property-store') const Snippet = require('./snippet') const SnippetExpansion = require('./snippet-expansion') const EditorStore = require('./editor-store') +const VariableResolver = require('./variable-resolver') const {getPackageRoot} = require('./helpers') module.exports = { @@ -19,6 +20,7 @@ module.exports = { this.snippetsByPackage = new Map this.parsedSnippetsById = new Map this.editorMarkerLayers = new WeakMap + this.variableResolver = new VariableResolver this.scopedPropertyStore = new ScopedPropertyStore // The above ScopedPropertyStore will store the main registry of snippets. @@ -418,7 +420,7 @@ module.exports = { if (snippet == null) { let {id, prefix, name, body, bodyTree, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML} = attributes if (bodyTree == null) { bodyTree = this.getBodyParser().parse(body) } - snippet = new Snippet({id, name, prefix, bodyTree, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyText: body}) + snippet = new Snippet({id, name, prefix, bodyTree, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyText: body, variableResolver: this.variableResolver}) this.parsedSnippetsById.set(attributes.id, snippet) } return snippet @@ -624,7 +626,7 @@ module.exports = { if (cursor == null) { cursor = editor.getLastCursor() } if (typeof snippet === 'string') { const bodyTree = this.getBodyParser().parse(snippet) - snippet = new Snippet({name: '__anonymous', prefix: '', bodyTree, bodyText: snippet}) + snippet = new Snippet({name: '__anonymous', prefix: '', bodyTree, bodyText: snippet, variableResolver: this.variableResolver}) } return new SnippetExpansion(snippet, editor, cursor, this) }, @@ -662,5 +664,9 @@ module.exports = { onUndoOrRedo (editor, isUndo) { const activeExpansions = this.getExpansions(editor) activeExpansions.forEach(expansion => expansion.onUndoOrRedo(isUndo)) + }, + + provideVariableResolver (resolver) { + this.variableResolver.add(resolver) } } diff --git a/lib/variable-resolver.js b/lib/variable-resolver.js new file mode 100644 index 00000000..a972de28 --- /dev/null +++ b/lib/variable-resolver.js @@ -0,0 +1,27 @@ +module.exports = class VariableResolver { + constructor (resolvers = new Map) { + this.resolvers = new Map([ + ["CLIPBOARD", resolveClipboard], + ...resolvers + ]) + } + + add (variable, resolver) { + this.resolvers.set(variable, resolver) + } + + resolve (params) { + + const resolver = this.resolvers.get(params.name) + + if (resolver) { + return resolver(params) + } + + return undefined + } +} + +function resolveClipboard () { + return atom.clipboard.read() +} diff --git a/package.json b/package.json index a5c033a8..2855bf9c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,12 @@ "versions": { "0.1.0": "provideSnippets" } + }, + "snippetsVariableResolver": { + "description": "Provide custom functions to resolve snippet variables.", + "versions": { + "0.0.0": "provideVariableResolver" + } } }, "devDependencies": { From 1c42d1bfe2fbad42c87f93b1661d0fbd7d58588b Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 7 Feb 2019 16:48:29 +1000 Subject: [PATCH 08/77] Hah, pass all specs --- lib/insertion.js | 1 + lib/snippet-body.pegjs | 26 +++++++++++++++----------- lib/snippet-expansion.js | 5 ++++- lib/snippet.js | 3 ++- spec/body-parser-spec.coffee | 32 +++++++++++++++++++++++++------- spec/snippet-loading-spec.coffee | 30 +++++++++++++++--------------- spec/snippets-spec.coffee | 10 +++++----- 7 files changed, 67 insertions(+), 40 deletions(-) diff --git a/lib/insertion.js b/lib/insertion.js index 96065d1e..13f64131 100644 --- a/lib/insertion.js +++ b/lib/insertion.js @@ -85,6 +85,7 @@ class Insertion { } transform (input) { + debugger let { substitution } = this if (!substitution) { return input } return input.replace(substitution.find, this.replacer) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index e7cf6b90..837049d2 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -43,29 +43,33 @@ choicetext = choicetext:(choiceEscaped / [^|,] / barred:('|' &[^}]) { return bar } // Transform is applied when tabbed off -transform = '/' regex:regex '/' replace:replace '/' flags:flags { - return { regex: regex, format: replace, flags: flags } +transform = '/' regex:regexString '/' replace:replace '/' flags:flags { + return { find: new RegExp(regex, flags), replace: replace } } -regex = regex:(escaped / [^/])* { - return new RegExp(regex.join('')) +regexString = regex:(escaped / [^/])* { + return regex.join('') } replace = (format / replacetext)* // TODO: Support conditionals -format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform +format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatEscape simpleFormat = '$' index:int { - return { index: makeInteger(index) } + return { backreference: makeInteger(index) } } formatWithoutPlaceholder = '${' index:int '}' { - return { index: makeInteger(index) } + return { backreference: makeInteger(index) } } formatWithCaseTransform = '${' index:int ':' casetransform:casetransform '}' { - return { index: makeInteger(index), transform: casetransform } + return { backreference: makeInteger(index), transform: casetransform } +} + +formatEscape = '\\' flag:[ULulErn$] { + return { escape: flag } } casetransform = '/' type:[a-zA-Z]* { @@ -80,7 +84,7 @@ casetransform = '/' type:[a-zA-Z]* { } } -replacetext = replacetext:(escaped / !format char:[^/] { return char })+ { +replacetext = replacetext:(!formatEscape escaped / !format char:[^/] { return char })+ { return replacetext.join('') } @@ -133,7 +137,7 @@ choiceEscaped = '\\' char:. { } flags = flags:[a-z]* { - return flags.join('') + 'g' + return flags.join('') } text = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:. { return char })+ { @@ -142,4 +146,4 @@ text = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char: nonCloseBraceText = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:[^}] { return char })+ { return text.join('') -} \ No newline at end of file +} diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index d8ffa682..fa0f946c 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -108,6 +108,9 @@ module.exports = class SnippetExpansion { if (!insertion.isTransformation()) { continue } var outputText = insertion.transform(inputText) + + console.log(`Transformed ${inputText} to ${outputText}`) + this.editor.transact(() => this.editor.setTextInBufferRange(range, outputText)) const newRange = new Range( range.start, @@ -157,7 +160,7 @@ module.exports = class SnippetExpansion { } else { // The user has tabbed past the last tab stop. If the last tab stop is a // $0, we shouldn't move the cursor any further. - if (this.tabStopList.hasEndStop) { foo + if (this.tabStopList.hasEndStop) { this.destroy() return false } else { diff --git a/lib/snippet.js b/lib/snippet.js index 69e0c51f..18aff8eb 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -13,7 +13,7 @@ const TabStopList = require('./tab-stop-list') */ module.exports = class Snippet { - constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, variableResolver}) { + constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, bodyText, variableResolver}) { this.name = name this.prefix = prefix this.description = description @@ -22,6 +22,7 @@ module.exports = class Snippet { this.leftLabel = leftLabel this.leftLabelHTML = leftLabelHTML this.bodyTree = bodyTree + this.bodyText = bodyText this.variableResolver = variableResolver } diff --git a/spec/body-parser-spec.coffee b/spec/body-parser-spec.coffee index 2f8a28e7..6f7cdf4f 100644 --- a/spec/body-parser-spec.coffee +++ b/spec/body-parser-spec.coffee @@ -33,7 +33,25 @@ describe "Snippet Body Parser", -> "module ", { "index": 1, - "content": ["ActiveRecord::", ""] + "content": [ + "ActiveRecord::", + { + "variable": "TM_FILENAME", + "substitution": { + "find": /(?:\A|_)([A-Za-z0-9]+)(?:\.rb)?/g, + "replace": [ + "(?2::", + { + "escape": 'u' + }, + { + "backreference": 1 + }, + ")" + ] + } + } + ] } ] @@ -78,7 +96,7 @@ describe "Snippet Body Parser", -> '>', {index: 0, content: []}, '' ] @@ -94,7 +112,7 @@ describe "Snippet Body Parser", -> index: 1, content: [], substitution: { - find: /(.)/g, + find: /(.)/, replace: [ {escape: 'u'}, {backreference: 1} @@ -110,7 +128,7 @@ describe "Snippet Body Parser", -> index: 2, content: [], substitution: { - find: /^(.*)$/g, + find: /^(.*)$/, replace: [ {escape: 'L'}, {backreference: 1} @@ -134,7 +152,7 @@ describe "Snippet Body Parser", -> index: 1, content: [], substitution: { - find: /(.)/g, + find: /(.)/, replace: [ {escape: 'u'}, {backreference: 1} @@ -160,7 +178,7 @@ describe "Snippet Body Parser", -> index: 1, content: [], substitution: { - find: /(.)(.*)/g, + find: /(.)(.*)/, replace: [ {escape: 'u'}, {backreference: 1}, @@ -188,7 +206,7 @@ describe "Snippet Body Parser", -> index: 1, content: [], substitution: { - find: /(.)\/(.*)/g, + find: /(.)\/(.*)/, replace: [ {escape: 'u'}, {backreference: 1}, diff --git a/spec/snippet-loading-spec.coffee b/spec/snippet-loading-spec.coffee index 12a6c2aa..c0bbf178 100644 --- a/spec/snippet-loading-spec.coffee +++ b/spec/snippet-loading-spec.coffee @@ -39,16 +39,16 @@ describe "Snippet Loading", -> jsonSnippet = snippetsService.snippetsForScopes(['.source.json'])['snip'] expect(jsonSnippet.name).toBe 'Atom Snippet' expect(jsonSnippet.prefix).toBe 'snip' - expect(jsonSnippet.body).toContain '"prefix":' - expect(jsonSnippet.body).toContain '"body":' - expect(jsonSnippet.tabStopList.length).toBeGreaterThan(0) + expect(jsonSnippet.toString().body).toContain '"prefix":' + expect(jsonSnippet.toString().body).toContain '"body":' + expect(jsonSnippet.toString().tabStopList.length).toBeGreaterThan(0) csonSnippet = snippetsService.snippetsForScopes(['.source.coffee'])['snip'] expect(csonSnippet.name).toBe 'Atom Snippet' expect(csonSnippet.prefix).toBe 'snip' - expect(csonSnippet.body).toContain "'prefix':" - expect(csonSnippet.body).toContain "'body':" - expect(csonSnippet.tabStopList.length).toBeGreaterThan(0) + expect(csonSnippet.toString().body).toContain "'prefix':" + expect(csonSnippet.toString().body).toContain "'body':" + expect(csonSnippet.toString().tabStopList.length).toBeGreaterThan(0) it "loads non-hidden snippet files from atom packages with snippets directories", -> activateSnippetsPackage() @@ -56,22 +56,22 @@ describe "Snippet Loading", -> runs -> snippet = snippetsService.snippetsForScopes(['.test'])['test'] expect(snippet.prefix).toBe 'test' - expect(snippet.body).toBe 'testing 123' + expect(snippet.toString().body).toBe 'testing 123' snippet = snippetsService.snippetsForScopes(['.test'])['testd'] expect(snippet.prefix).toBe 'testd' - expect(snippet.body).toBe 'testing 456' + expect(snippet.toString().body).toBe 'testing 456' expect(snippet.description).toBe 'a description' expect(snippet.descriptionMoreURL).toBe 'http://google.com' snippet = snippetsService.snippetsForScopes(['.test'])['testlabelleft'] expect(snippet.prefix).toBe 'testlabelleft' - expect(snippet.body).toBe 'testing 456' + expect(snippet.toString().body).toBe 'testing 456' expect(snippet.leftLabel).toBe 'a label' snippet = snippetsService.snippetsForScopes(['.test'])['testhtmllabels'] expect(snippet.prefix).toBe 'testhtmllabels' - expect(snippet.body).toBe 'testing 456' + expect(snippet.toString().body).toBe 'testing 456' expect(snippet.leftLabelHTML).toBe 'Label' expect(snippet.rightLabelHTML).toBe 'Label' @@ -99,7 +99,7 @@ describe "Snippet Loading", -> runs -> snippet = snippetsService.snippetsForScopes(['.source.js'])['log'] - expect(snippet.body).toBe "from-a-community-package" + expect(snippet.toString().body).toBe "from-a-community-package" describe "::onDidLoadSnippets(callback)", -> it "invokes listeners when all snippets are loaded", -> @@ -135,7 +135,7 @@ describe "Snippet Loading", -> runs -> expect(snippet.name).toBe 'foo snippet' expect(snippet.prefix).toBe "foo" - expect(snippet.body).toBe "bar1" + expect(snippet.toString().body).toBe "bar1" describe "when that file changes", -> it "reloads the snippets", -> @@ -152,7 +152,7 @@ describe "Snippet Loading", -> waitsFor "snippets to be changed", -> snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] - snippet?.body is 'bar2' + snippet?.toString().body is 'bar2' runs -> fs.writeFileSync path.join(configDirPath, 'snippets.json'), "" @@ -179,7 +179,7 @@ describe "Snippet Loading", -> runs -> expect(snippet.name).toBe 'foo snippet' expect(snippet.prefix).toBe "foo" - expect(snippet.body).toBe "bar1" + expect(snippet.toString().body).toBe "bar1" describe "when that file changes", -> it "reloads the snippets", -> @@ -192,7 +192,7 @@ describe "Snippet Loading", -> waitsFor "snippets to be changed", -> snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] - snippet?.body is 'bar2' + snippet?.toString().body is 'bar2' runs -> fs.writeFileSync path.join(configDirPath, 'snippets.cson'), "" diff --git a/spec/snippets-spec.coffee b/spec/snippets-spec.coffee index 1439bffe..053e66c8 100644 --- a/spec/snippets-spec.coffee +++ b/spec/snippets-spec.coffee @@ -223,29 +223,29 @@ describe "Snippets extension", -> "transform with non-transforming mirrors": prefix: "t13" body: """ - ${1:placeholder}\n${1/(.)/\\u$1/}\n$1 + ${1:placeholder}\n${1/(.)/\\u$1/g}\n$1 """ "multiple tab stops, some with transforms and some without": prefix: "t14" body: """ - ${1:placeholder} ${1/(.)/\\u$1/} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2 + ${1:placeholder} ${1/(.)/\\u$1/g} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2 """ "has a transformed tab stop without a corresponding ordinary tab stop": prefix: 't15' body: """ - ${1/(.)/\\u$1/} & $2 + ${1/(.)/\\u$1/g} & $2 """ "has a transformed tab stop that occurs before the corresponding ordinary tab stop": prefix: 't16' body: """ - & ${1/(.)/\\u$1/} & ${1:q} + & ${1/(.)/\\u$1/g} & ${1:q} """ "has a placeholder that mirrors another tab stop's content": prefix: 't17' body: "$4console.${3:log}('${2:uh $1}', $1);$0" "has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step": prefix: 't18' - body: '// $1\n// ${1/./=/}' + body: '// $1\n// ${1/./=/g}' it "parses snippets once, reusing cached ones on subsequent queries", -> spyOn(Snippets, "getBodyParser").andCallThrough() From 6cc94461861da3d04e3de891fc306dd7c5a621ab Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 7 Feb 2019 17:17:38 +1000 Subject: [PATCH 09/77] Pass editor and cursor to variable resolver --- lib/snippet-expansion.js | 2 +- lib/snippet.js | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index fa0f946c..8727bc04 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -14,7 +14,7 @@ module.exports = class SnippetExpansion { this.selections = [this.cursor.selection] const startPosition = this.cursor.selection.getBufferRange().start - let {body, tabStopList} = this.snippet.toString() + let {body, tabStopList} = this.snippet.toString({ editor: this.editor, cursor: this.cursor }) this.tabStopList = tabStopList diff --git a/lib/snippet.js b/lib/snippet.js index 18aff8eb..336b8e46 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -1,17 +1,6 @@ const {Range} = require('atom') const TabStopList = require('./tab-stop-list') - -/* - -1. Snippet stores the parsed snippet template - -2. Template variables are resolved on demand - -3. Followed by insertion - -*/ - module.exports = class Snippet { constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, bodyText, variableResolver}) { this.name = name @@ -26,7 +15,7 @@ module.exports = class Snippet { this.variableResolver = variableResolver } - toString () { + toString (params) { const tabStopList = new TabStopList(this) const bodyText = [] let row = 0 @@ -49,7 +38,7 @@ module.exports = class Snippet { } else { if (segment.variable != undefined) { debugger - const value = this.variableResolver.resolve({ name: segment.variable }) + const value = this.variableResolver.resolve({ name: segment.variable, ...params }) if (value === undefined) { if (segment.content) { extractTabStops(segment.content) From 9d32670bf8448ad69c60ca6bb94fbd1369c9ae53 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 7 Feb 2019 17:20:30 +1000 Subject: [PATCH 10/77] remove debugger --- lib/insertion.js | 1 - lib/snippet.js | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/insertion.js b/lib/insertion.js index 13f64131..96065d1e 100644 --- a/lib/insertion.js +++ b/lib/insertion.js @@ -85,7 +85,6 @@ class Insertion { } transform (input) { - debugger let { substitution } = this if (!substitution) { return input } return input.replace(substitution.find, this.replacer) diff --git a/lib/snippet.js b/lib/snippet.js index 336b8e46..7edf33fb 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -37,7 +37,6 @@ module.exports = class Snippet { tabStop.addInsertion({ range, substitution }) } else { if (segment.variable != undefined) { - debugger const value = this.variableResolver.resolve({ name: segment.variable, ...params }) if (value === undefined) { if (segment.content) { From c60b3adbcdde4192f4fd5514df6dd66aa10b7ae4 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 7 Feb 2019 20:04:33 +1000 Subject: [PATCH 11/77] Resolve all the things --- lib/variable-resolver.js | 141 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/lib/variable-resolver.js b/lib/variable-resolver.js index a972de28..571e9a36 100644 --- a/lib/variable-resolver.js +++ b/lib/variable-resolver.js @@ -1,7 +1,36 @@ +const path = require("path") + module.exports = class VariableResolver { constructor (resolvers = new Map) { this.resolvers = new Map([ ["CLIPBOARD", resolveClipboard], + + ["TM_SELECTED_TEXT", resolveSelected], + ["TM_CURRENT_LINE", resolveCurrentLine], + ["TM_CURRENT_WORD", resolveCurrentWord], + ["TM_LINE_INDEX", resolveLineIndex], + ["TM_LINE_NUMBER", resolveLineNumber], + ["TM_FILENAME", resolveFileName], + ["TM_FILENAME_BASE", resolveFileNameBase], + ["TM_DIRECTORY", resolveFileDirectory], + ["TM_FILEPATH", resolveFilePath], + + ["CURRENT_YEAR", resolveYear], + ["CURRENT_YEAR_SHORT", resolveYearShort], + ["CURRENT_MONTH", resolveMonth], + ["CURRENT_MONTH_NAME", resolveMonthName], + ["CURRENT_MONTH_NAME_SHORT", resolveMonthNameShort], + ["CURRENT_DATE", resolveDate], + ["CURRENT_DAY_NAME", resolveDayName], + ["CURRENT_DAY_NAME_SHORT", resolveDayNameShort], + ["CURRENT_HOUR", resolveHour], + ["CURRENT_MINUTE", resolveMinute], + ["CURRENT_SECOND", resolveSecond], + + ["BLOCK_COMMENT_START", resolveBlockCommentStart], + ["BLOCK_COMMENT_END", resolveBlockCommentEnd], + ["LINE_COMMENT", resolveLineComment], + ...resolvers ]) } @@ -11,7 +40,6 @@ module.exports = class VariableResolver { } resolve (params) { - const resolver = this.resolvers.get(params.name) if (resolver) { @@ -25,3 +53,114 @@ module.exports = class VariableResolver { function resolveClipboard () { return atom.clipboard.read() } + +function resolveSelected ({editor}) { + return editor.getSelectedText() +} + +function resolveCurrentLine ({editor, cursor}) { + return editor.lineTextForBufferRow(cursor.getBufferRow()) +} + +function resolveCurrentWord ({editor, cursor}) { + return editor.getTextInBufferRange(cursor.getCurrentWordBufferRange()) +} + +function resolveLineIndex ({cursor}) { + return cursor.getBufferRow() +} + +function resolveLineNumber ({cursor}) { + return cursor.getBufferRow() + 1 +} + +function resolveFileName ({editor}) { + return editor.getTitle() +} + +function resolveFileNameBase ({editor}) { + const fileName = resolveFileName({editor}) + if (!fileName) { return undefined } + + const index = fileName.lastIndexOf('.') + if (index >= 0) { + return fileName.slice(0, index) + } + + return fileName +} + +function resolveFileDirectory ({editor}) { + return path.dirname(editor.getPath()) +} + +function resolveFilePath ({editor}) { + return editor.getPath() +} + + +// TODO: Use correct locale +function resolveYear () { + return new Date().toLocaleString('en-us', { year: 'numeric' }) +} + +function resolveYearShort () { // last two digits of year + return new Date().toLocaleString('en-us', { year: '2-digit' }) +} + +function resolveMonth () { + return new Date().toLocaleString('en-us', { month: '2-digit' }) +} + +function resolveMonthName () { + return new Date().toLocaleString('en-us', { month: 'long' }) +} + +function resolveMonthNameShort () { + return new Date().toLocaleString('en-us', { month: 'short' }) +} + +function resolveDate () { + return new Date().toLocaleString('en-us', { day: '2-digit' }) +} + +function resolveDayName () { + return new Date().toLocaleString('en-us', { weekday: 'long' }) +} + +function resolveDayNameShort () { + return new Date().toLocaleString('en-us', { weekday: 'short' }) +} + +function resolveHour () { + return new Date().toLocaleString('en-us', { hour: '2-digit' }) +} + +function resolveMinute () { + return new Date().toLocaleString('en-us', { minute: '2-digit' }) +} + +function resolveSecond () { + return new Date().toLocaleString('en-us', { second: '2-digit' }) +} + +// TODO: wait for https://github.com/atom/atom/issues/18812 +// Could make a start with what we have; one of the two should be available +function getEditorCommentStringsForPoint (_editor, _point) { + return { line: '//', start: '/*', end: '*/' } +} + +function resolveBlockCommentStart ({editor, cursor}) { + const delims = getEditorCommentStringsForPoint(editor, cursor.getBufferPosition()) + return delims.start +} + +function resolveBlockCommentEnd ({editor, cursor}) { + const delims = getEditorCommentStringsForPoint(editor, cursor.getBufferPosition()) + return delims.end +} + +function resolveLineComment ({editor, cursor}) { + const delims = getEditorCommentStringsForPoint(editor, cursor.getBufferPosition()) + return delims.line +} From 4be086c894e36f36731fb7fa4986e3d10063dc8c Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 7 Feb 2019 22:23:22 +1000 Subject: [PATCH 12/77] Fix selection contents & line numbers --- lib/snippet-expansion.js | 4 ++-- lib/snippets-available.js | 2 +- lib/snippets.js | 7 ++++--- lib/variable-resolver.js | 17 +++++++++++------ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 8727bc04..58197660 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -1,7 +1,7 @@ const {CompositeDisposable, Range, Point} = require('atom') module.exports = class SnippetExpansion { - constructor(snippet, editor, cursor, snippets) { + constructor(snippet, editor, cursor, oldSelectionRange, snippets) { this.settingTabStop = false this.isIgnoringBufferChanges = false this.onUndoOrRedo = this.onUndoOrRedo.bind(this) @@ -14,7 +14,7 @@ module.exports = class SnippetExpansion { this.selections = [this.cursor.selection] const startPosition = this.cursor.selection.getBufferRange().start - let {body, tabStopList} = this.snippet.toString({ editor: this.editor, cursor: this.cursor }) + let {body, tabStopList} = this.snippet.toString({ editor: this.editor, cursor: this.cursor, selectionRange: oldSelectionRange }) this.tabStopList = tabStopList diff --git a/lib/snippets-available.js b/lib/snippets-available.js index d244cb16..659618f1 100644 --- a/lib/snippets-available.js +++ b/lib/snippets-available.js @@ -28,7 +28,7 @@ export default class SnippetsAvailable { }, didConfirmSelection: (snippet) => { for (const cursor of this.editor.getCursors()) { - this.snippets.insert(snippet.bodyText, this.editor, cursor) + this.snippets.insert(snippet.bodyText, this.editor, cursor, cursor.selection.getBufferRange()) } this.cancel() }, diff --git a/lib/snippets.js b/lib/snippets.js index 48228739..96f454ef 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -528,8 +528,9 @@ module.exports = { for (const cursor of cursors) { const cursorPosition = cursor.getBufferPosition() const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) + const oldSelectionRange = cursor.selection.getBufferRange() cursor.selection.setBufferRange([startPoint, cursorPosition]) - this.insert(snippet, editor, cursor) + this.insert(snippet, editor, cursor, oldSelectionRange) } }) return true @@ -621,14 +622,14 @@ module.exports = { this.getStore(editor).makeCheckpoint() }, - insert (snippet, editor, cursor) { + insert (snippet, editor, cursor, oldSelectionRange) { if (editor == null) { editor = atom.workspace.getActiveTextEditor() } if (cursor == null) { cursor = editor.getLastCursor() } if (typeof snippet === 'string') { const bodyTree = this.getBodyParser().parse(snippet) snippet = new Snippet({name: '__anonymous', prefix: '', bodyTree, bodyText: snippet, variableResolver: this.variableResolver}) } - return new SnippetExpansion(snippet, editor, cursor, this) + return new SnippetExpansion(snippet, editor, cursor, oldSelectionRange, this) }, getUnparsedSnippets () { diff --git a/lib/variable-resolver.js b/lib/variable-resolver.js index 571e9a36..7ea02c12 100644 --- a/lib/variable-resolver.js +++ b/lib/variable-resolver.js @@ -54,8 +54,10 @@ function resolveClipboard () { return atom.clipboard.read() } -function resolveSelected ({editor}) { - return editor.getSelectedText() +function resolveSelected ({editor, selectionRange}) { + debugger + if (!selectionRange || selectionRange.isEmpty()) return undefined + return editor.getTextInBufferRange(selectionRange) } function resolveCurrentLine ({editor, cursor}) { @@ -67,11 +69,11 @@ function resolveCurrentWord ({editor, cursor}) { } function resolveLineIndex ({cursor}) { - return cursor.getBufferRow() + return `${cursor.getBufferRow()}` } function resolveLineNumber ({cursor}) { - return cursor.getBufferRow() + 1 + return `${cursor.getBufferRow() + 1}` } function resolveFileName ({editor}) { @@ -91,7 +93,10 @@ function resolveFileNameBase ({editor}) { } function resolveFileDirectory ({editor}) { - return path.dirname(editor.getPath()) + debugger + const filePath = editor.getPath() + if (filePath === undefined) return undefined + return path.dirname(filePath) } function resolveFilePath ({editor}) { @@ -133,7 +138,7 @@ function resolveDayNameShort () { } function resolveHour () { - return new Date().toLocaleString('en-us', { hour: '2-digit' }) + return new Date().toLocaleString('en-us', { hour12: false, hour: '2-digit' }) } function resolveMinute () { From 4863ca6759ce80e2676a21c026f153c7bd47d7cf Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 7 Feb 2019 23:33:28 +1000 Subject: [PATCH 13/77] remove debugger --- lib/variable-resolver.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/variable-resolver.js b/lib/variable-resolver.js index 7ea02c12..33b3ac2e 100644 --- a/lib/variable-resolver.js +++ b/lib/variable-resolver.js @@ -55,7 +55,6 @@ function resolveClipboard () { } function resolveSelected ({editor, selectionRange}) { - debugger if (!selectionRange || selectionRange.isEmpty()) return undefined return editor.getTextInBufferRange(selectionRange) } @@ -93,7 +92,6 @@ function resolveFileNameBase ({editor}) { } function resolveFileDirectory ({editor}) { - debugger const filePath = editor.getPath() if (filePath === undefined) return undefined return path.dirname(filePath) From 139e08a8a94ec8c71c09612d4a4636c55be3d0e0 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 15:16:55 +1000 Subject: [PATCH 14/77] Apply (some) variable transforms --- lib/snippet-body.pegjs | 10 +-------- lib/snippet.js | 50 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index 837049d2..f72e180c 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -73,15 +73,7 @@ formatEscape = '\\' flag:[ULulErn$] { } casetransform = '/' type:[a-zA-Z]* { - type = type.join('') - switch (type) { - case 'upcase': - case 'downcase': - case 'capitalize': - return type - default: - return 'none' - } + return type.join('') } replacetext = replacetext:(!formatEscape escaped / !format char:[^/] { return char })+ { diff --git a/lib/snippet.js b/lib/snippet.js index 7edf33fb..f3516fd8 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -36,13 +36,17 @@ module.exports = class Snippet { }) tabStop.addInsertion({ range, substitution }) } else { - if (segment.variable != undefined) { + if (segment.variable !== undefined) { const value = this.variableResolver.resolve({ name: segment.variable, ...params }) if (value === undefined) { if (segment.content) { extractTabStops(segment.content) } } else { + if (segment.substitution) { + value = applyVariableTransformation(value, segment.substitution) + } + segment = value } } @@ -68,3 +72,47 @@ module.exports = class Snippet { return { body: bodyText.join(''), tabStopList } } } + +function applyVariableTransformation (value, substitution) { + const regex = substitution.find + const match = regex.exec(value) + if (match === null) { return '' } // TODO: This is where an else branch would be triggered + + const replace = substitution.replace + const result = '' + + for (let i = 0; i < replace.length; i++) { + if (typeof replace[i] === "string") { + result += replace[i] + continue + } + + const format = replace[i] + + const index = format.backreference + if (index >= match.length) { continue } + + let capture = match[index] + if (capture === undefined) { continue } + + if (format.transform) { + // TODO: Support custom transforms as well? + switch (format.transform) { + case 'upcase': + capture = capture.toLocaleUpperCase() + break + case 'downcase': + capture = capture.toLocaleLowerCase() + break + case 'capitalize': + capture = capture ? capture[0].toLocaleUpperCase() + capture.substr(1) : '' + break + default: {} + } + } + + result += capture + } + + return result +} From fc7d2272ef56bdea88f827020297cf35ad0a5057 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 15:20:10 +1000 Subject: [PATCH 15/77] Refrain from assigning to constant --- lib/snippet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/snippet.js b/lib/snippet.js index f3516fd8..5409f812 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -79,7 +79,7 @@ function applyVariableTransformation (value, substitution) { if (match === null) { return '' } // TODO: This is where an else branch would be triggered const replace = substitution.replace - const result = '' + let result = '' for (let i = 0; i < replace.length; i++) { if (typeof replace[i] === "string") { From b9d967ac7ee952e2b71c127f0415e888cbbbe7bf Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 15:22:04 +1000 Subject: [PATCH 16/77] Take previous advice --- lib/snippet.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/snippet.js b/lib/snippet.js index 5409f812..f7d6a7cd 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -37,7 +37,7 @@ module.exports = class Snippet { tabStop.addInsertion({ range, substitution }) } else { if (segment.variable !== undefined) { - const value = this.variableResolver.resolve({ name: segment.variable, ...params }) + let value = this.variableResolver.resolve({ name: segment.variable, ...params }) if (value === undefined) { if (segment.content) { extractTabStops(segment.content) @@ -116,3 +116,5 @@ function applyVariableTransformation (value, substitution) { return result } + +foo From f9a2d72e013614e11775bf4f89e61561ee569887 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 15:22:21 +1000 Subject: [PATCH 17/77] ... --- lib/snippet.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/snippet.js b/lib/snippet.js index f7d6a7cd..2361420c 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -116,5 +116,3 @@ function applyVariableTransformation (value, substitution) { return result } - -foo From 7c5cc772353efd65b496fb1760037db232cb7a36 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 15:32:10 +1000 Subject: [PATCH 18/77] Resolve global replacement properly --- lib/snippet.js | 67 +++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/lib/snippet.js b/lib/snippet.js index 2361420c..bdc09f95 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -74,45 +74,44 @@ module.exports = class Snippet { } function applyVariableTransformation (value, substitution) { - const regex = substitution.find - const match = regex.exec(value) - if (match === null) { return '' } // TODO: This is where an else branch would be triggered - const replace = substitution.replace - let result = '' - - for (let i = 0; i < replace.length; i++) { - if (typeof replace[i] === "string") { - result += replace[i] - continue - } - - const format = replace[i] - - const index = format.backreference - if (index >= match.length) { continue } - - let capture = match[index] - if (capture === undefined) { continue } + const result = value.replace(substitution.find, (...match) => { + let interimResult = '' + for (let i = 0; i < replace.length; i++) { + if (typeof replace[i] === "string") { + interimResult += replace[i] + continue + } - if (format.transform) { - // TODO: Support custom transforms as well? - switch (format.transform) { - case 'upcase': - capture = capture.toLocaleUpperCase() - break - case 'downcase': - capture = capture.toLocaleLowerCase() - break - case 'capitalize': - capture = capture ? capture[0].toLocaleUpperCase() + capture.substr(1) : '' - break - default: {} + const format = replace[i] + + const index = format.backreference + if (index >= match.length - 2) { continue } + + let capture = match[index] + if (capture === undefined) { continue } + + if (format.transform) { + // TODO: Support custom transforms as well? + switch (format.transform) { + case 'upcase': + capture = capture.toLocaleUpperCase() + break + case 'downcase': + capture = capture.toLocaleLowerCase() + break + case 'capitalize': + capture = capture ? capture[0].toLocaleUpperCase() + capture.substr(1) : '' + break + default: {} + } } + + interimResult += capture } - result += capture - } + return interimResult + }) return result } From 574627684a7445dab5753a97cbc84b443a81ec02 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 15:38:13 +1000 Subject: [PATCH 19/77] Informative error when failing to transform variable --- lib/snippet.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/snippet.js b/lib/snippet.js index bdc09f95..1d0a7998 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -44,9 +44,12 @@ module.exports = class Snippet { } } else { if (segment.substitution) { - value = applyVariableTransformation(value, segment.substitution) + try { + value = applyVariableTransformation(value, segment.substitution) + } catch (e) { + atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, { stack: e }) + } } - segment = value } } @@ -74,6 +77,8 @@ module.exports = class Snippet { } function applyVariableTransformation (value, substitution) { + // TODO: Better bounds and type checking so errors aren't as cryptic + const replace = substitution.replace const result = value.replace(substitution.find, (...match) => { let interimResult = '' From 85107f4433610581de2128301808ba6cf1b04486 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 15:40:57 +1000 Subject: [PATCH 20/77] Actually throw an error --- lib/snippet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/snippet.js b/lib/snippet.js index 1d0a7998..7552fbad 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -47,7 +47,7 @@ module.exports = class Snippet { try { value = applyVariableTransformation(value, segment.substitution) } catch (e) { - atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, { stack: e }) + atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, { detail: e }) } } segment = value @@ -91,7 +91,7 @@ function applyVariableTransformation (value, substitution) { const format = replace[i] const index = format.backreference - if (index >= match.length - 2) { continue } + if (index >= match.length - 2) { throw new Error ("Index too high") } let capture = match[index] if (capture === undefined) { continue } From c9b2996a9135973bd0041d0a18d36401877bc2f7 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 19:26:59 +1000 Subject: [PATCH 21/77] general idea --- lib/autocomplete-choice.js | 39 ++++++++++++++++++++++++++++++++++++++ lib/insertion.js | 8 +++++++- lib/snippet-body.pegjs | 4 ++-- lib/snippet-expansion.js | 13 +++++++++++-- lib/snippet.js | 7 ++++++- lib/snippets.js | 9 ++++++++- lib/tab-stop.js | 4 ++-- package.json | 9 ++++++++- 8 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 lib/autocomplete-choice.js diff --git a/lib/autocomplete-choice.js b/lib/autocomplete-choice.js new file mode 100644 index 00000000..e9512893 --- /dev/null +++ b/lib/autocomplete-choice.js @@ -0,0 +1,39 @@ +module.exports = class ChoiceProvider { + constructor () { + this.selector = '*' + this.inclusionPriority = -Infinity + this.suggestionPriority = 100 + this.filterSuggestions = true + this.excludeLowerPriority = false + this.active = false + this.choices = [] + } + + getSuggestions () { + // TODO: Show all when just on default, show filtered and sorted when started typing + // TODO: Show even when no prefix + if (!this.active) { return undefined } + return this.choices.map(c => { + return { + text: c, + type: "constant" + } + }) + } + + activate (choices) { + this.active = true + this.inclusionPriority = 1000 + this.suggestionPriority = 1000 + this.excludeLowerPriority = true + this.choices = choices + } + + deactivate () { + this.active = false + this.inclusionPriority = -Infinity + this.suggestionPriority = -Infinity + this.excludeLowerPriority = false + this.choices = [] + } +} diff --git a/lib/insertion.js b/lib/insertion.js index 96065d1e..d5a2e863 100644 --- a/lib/insertion.js +++ b/lib/insertion.js @@ -45,7 +45,7 @@ function transformText (str, flags) { } class Insertion { - constructor ({ range, substitution }) { + constructor ({ range, substitution, choices=[] }) { this.range = range this.substitution = substitution if (substitution) { @@ -54,12 +54,18 @@ class Insertion { } this.replacer = this.makeReplacer(substitution.replace) } + + this.choices = choices } isTransformation () { return !!this.substitution } + isChoices () { + return this.choices.length > 0 + } + makeReplacer (replace) { return function replacer (...match) { let flags = { diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index f72e180c..adae693f 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -31,7 +31,8 @@ tabstopWithTransform = '${' index:int substitution:transform '}' { } choice = '${' index:int '|' choice:choicecontents '|}' { - return { index: makeInteger(index), choice: choice } + const content = choice.length > 0 ? [choice[0]] : [] + return { index: makeInteger(index), choice: choice, content: content } } choicecontents = elem:choicetext rest:(',' val:choicetext { return val } )* { @@ -42,7 +43,6 @@ choicetext = choicetext:(choiceEscaped / [^|,] / barred:('|' &[^}]) { return bar return choicetext.join('') } -// Transform is applied when tabbed off transform = '/' regex:regexString '/' replace:replace '/' flags:flags { return { find: new RegExp(regex, flags), replace: replace } } diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 58197660..2336e3b5 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -109,8 +109,6 @@ module.exports = class SnippetExpansion { var outputText = insertion.transform(inputText) - console.log(`Transformed ${inputText} to ${outputText}`) - this.editor.transact(() => this.editor.setTextInBufferRange(range, outputText)) const newRange = new Range( range.start, @@ -179,6 +177,7 @@ module.exports = class SnippetExpansion { this.tabStopIndex = tabStopIndex this.settingTabStop = true let markerSelected = false + let choices = [] const items = this.tabStopMarkers[this.tabStopIndex] if (items.length === 0) { return false } @@ -193,9 +192,18 @@ module.exports = class SnippetExpansion { this.hasTransforms = true continue } + if (insertion.isChoices()) { + choices = insertion.choices + } ranges.push(marker.getBufferRange()) } + if (choices.length > 0) { + this.snippets.snippetChoiceProvider.activate(choices) + } else { + this.snippets.snippetChoiceProvider.deactivate() + } + if (ranges.length > 0) { for (const selection of this.selections.slice(ranges.length)) { selection.destroy() } this.selections = this.selections.slice(0, ranges.length) @@ -240,6 +248,7 @@ module.exports = class SnippetExpansion { this.tabStopMarkers = [] this.snippets.stopObservingEditor(this.editor) this.snippets.clearExpansions(this.editor) + this.snippets.snippetChoiceProvider.deactivate() // TODO: Move to clearExpansions? } getMarkerLayer () { diff --git a/lib/snippet.js b/lib/snippet.js index 7552fbad..edb21482 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -34,7 +34,12 @@ module.exports = class Snippet { index, snippet: this }) - tabStop.addInsertion({ range, substitution }) + + let choices = [] + if (segment.choice && segment.choice.length > 0) { + choices = segment.choice + } + tabStop.addInsertion({ range, substitution, choices }) } else { if (segment.variable !== undefined) { let value = this.variableResolver.resolve({ name: segment.variable, ...params }) diff --git a/lib/snippets.js b/lib/snippets.js index 96f454ef..cccc2d3b 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -10,6 +10,7 @@ const Snippet = require('./snippet') const SnippetExpansion = require('./snippet-expansion') const EditorStore = require('./editor-store') const VariableResolver = require('./variable-resolver') +const ChoiceProvider = require('./autocomplete-choice') const {getPackageRoot} = require('./helpers') module.exports = { @@ -21,6 +22,7 @@ module.exports = { this.parsedSnippetsById = new Map this.editorMarkerLayers = new WeakMap this.variableResolver = new VariableResolver + this.snippetChoiceProvider = new ChoiceProvider this.scopedPropertyStore = new ScopedPropertyStore // The above ScopedPropertyStore will store the main registry of snippets. @@ -85,6 +87,7 @@ module.exports = { } this.emitter = null this.editorSnippetExpansions = null + this.snippetChoiceProvider.deactivate() atom.config.transact(() => this.subscriptions.dispose()) }, @@ -667,7 +670,11 @@ module.exports = { activeExpansions.forEach(expansion => expansion.onUndoOrRedo(isUndo)) }, - provideVariableResolver (resolver) { + consumeVariableResolver (resolver) { this.variableResolver.add(resolver) + }, + + provideAutocomplete () { + return this.snippetChoiceProvider } } diff --git a/lib/tab-stop.js b/lib/tab-stop.js index 61a423e4..3e42b69d 100644 --- a/lib/tab-stop.js +++ b/lib/tab-stop.js @@ -20,8 +20,8 @@ class TabStop { return !all } - addInsertion ({ range, substitution }) { - let insertion = new Insertion({ range, substitution }) + addInsertion ({ range, substitution, choices=[] }) { + let insertion = new Insertion({ range, substitution, choices }) let insertions = this.insertions insertions.push(insertion) insertions = insertions.sort((i1, i2) => { diff --git a/package.json b/package.json index 2855bf9c..a7294f1b 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,17 @@ "0.1.0": "provideSnippets" } }, + "autocomplete.provider": { + "versions": { + "4.0.0": "provideAutocomplete" + } + } + }, + "consumedServices": { "snippetsVariableResolver": { "description": "Provide custom functions to resolve snippet variables.", "versions": { - "0.0.0": "provideVariableResolver" + "0.0.0": "consumeVariableResolver" } } }, From 82625e47279a9ccaa08437af92bf72f7b55f7830 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Fri, 8 Feb 2019 23:17:32 +1000 Subject: [PATCH 22/77] Mockup choices presentation --- lib/autocomplete-choice.js | 12 +++++++++++- lib/snippet-expansion.js | 25 +++++++++++++++++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/autocomplete-choice.js b/lib/autocomplete-choice.js index e9512893..1cd0829b 100644 --- a/lib/autocomplete-choice.js +++ b/lib/autocomplete-choice.js @@ -3,7 +3,7 @@ module.exports = class ChoiceProvider { this.selector = '*' this.inclusionPriority = -Infinity this.suggestionPriority = 100 - this.filterSuggestions = true + this.filterSuggestions = false this.excludeLowerPriority = false this.active = false this.choices = [] @@ -12,6 +12,8 @@ module.exports = class ChoiceProvider { getSuggestions () { // TODO: Show all when just on default, show filtered and sorted when started typing // TODO: Show even when no prefix + console.log("getting suggestions") + // debugger if (!this.active) { return undefined } return this.choices.map(c => { return { @@ -27,6 +29,13 @@ module.exports = class ChoiceProvider { this.suggestionPriority = 1000 this.excludeLowerPriority = true this.choices = choices + + this.oldConfig = atom.config.get("autocomplete-plus.autoActivationEnabled") + atom.config.set("autocomplete-plus.autoActivationEnabled", false) + + setTimeout(() => { + atom.commands.dispatch(document.activeElement, "autocomplete-plus:activate") // TODO: Remove dependency on specific provider + }, 5) // Because expanding the snippet from the autocomplete-menu immediately to a choice catches the close of the existing menu } deactivate () { @@ -35,5 +44,6 @@ module.exports = class ChoiceProvider { this.suggestionPriority = -Infinity this.excludeLowerPriority = false this.choices = [] + atom.config.set("autocomplete-plus.autoActivationEnabled", this.oldConfig) } } diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 2336e3b5..d354d950 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -58,6 +58,12 @@ module.exports = class SnippetExpansion { if (itemWithCursor && !itemWithCursor.insertion.isTransformation()) { return } + if (itemWithCursor && itemWithCursor.insertion.choices.length > 0) { + setTimeout(() => { + atom.commands.dispatch(document.activeElement, "autocomplete-plus:activate") // TODO: Remove dependency on specific provider + }, 5) + } + this.destroy() } @@ -188,21 +194,17 @@ module.exports = class SnippetExpansion { const {marker, insertion} = item if (marker.isDestroyed()) { continue } if (!marker.isValid()) { continue } + if (insertion.isChoices()) { + choices = insertion.choices + } if (insertion.isTransformation()) { this.hasTransforms = true continue } - if (insertion.isChoices()) { - choices = insertion.choices - } ranges.push(marker.getBufferRange()) } - if (choices.length > 0) { - this.snippets.snippetChoiceProvider.activate(choices) - } else { - this.snippets.snippetChoiceProvider.deactivate() - } + if (ranges.length > 0) { for (const selection of this.selections.slice(ranges.length)) { selection.destroy() } @@ -226,6 +228,13 @@ module.exports = class SnippetExpansion { // made to the editor so that we can update the transformed tab stops. if (this.hasTransforms) { this.snippets.observeEditor(this.editor) } + if (choices.length > 0) { + console.log("Activating choices") + this.snippets.snippetChoiceProvider.activate(choices) + } else { + this.snippets.snippetChoiceProvider.deactivate() + } + return markerSelected } From 31fe36eafed013a6edbc180630fb237ed68f9ad6 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Sat, 9 Feb 2019 22:44:41 +1000 Subject: [PATCH 23/77] don't save config change --- lib/autocomplete-choice.js | 4 ++-- lib/snippet-expansion.js | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/autocomplete-choice.js b/lib/autocomplete-choice.js index 1cd0829b..1c08eb9a 100644 --- a/lib/autocomplete-choice.js +++ b/lib/autocomplete-choice.js @@ -31,7 +31,7 @@ module.exports = class ChoiceProvider { this.choices = choices this.oldConfig = atom.config.get("autocomplete-plus.autoActivationEnabled") - atom.config.set("autocomplete-plus.autoActivationEnabled", false) + atom.config.set("autocomplete-plus.autoActivationEnabled", false, { save: false }) setTimeout(() => { atom.commands.dispatch(document.activeElement, "autocomplete-plus:activate") // TODO: Remove dependency on specific provider @@ -44,6 +44,6 @@ module.exports = class ChoiceProvider { this.suggestionPriority = -Infinity this.excludeLowerPriority = false this.choices = [] - atom.config.set("autocomplete-plus.autoActivationEnabled", this.oldConfig) + atom.config.set("autocomplete-plus.autoActivationEnabled", this.oldConfig, { save: false }) } } diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index d354d950..e05deff9 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -58,12 +58,6 @@ module.exports = class SnippetExpansion { if (itemWithCursor && !itemWithCursor.insertion.isTransformation()) { return } - if (itemWithCursor && itemWithCursor.insertion.choices.length > 0) { - setTimeout(() => { - atom.commands.dispatch(document.activeElement, "autocomplete-plus:activate") // TODO: Remove dependency on specific provider - }, 5) - } - this.destroy() } From 3a29fd93ae268d9a75871f37533cf94520fb7a39 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Sun, 10 Feb 2019 22:18:42 +1000 Subject: [PATCH 24/77] Refactor --- lib/autocomplete-choice.js | 15 +-- lib/insertion.js | 2 +- lib/snippet-body.pegjs | 4 +- lib/snippet-expansion.js | 52 ++++------ lib/snippet.js | 191 +++++++++++++++++++++++++------------ lib/tab-stop-list.js | 4 + lib/tab-stop.js | 26 +---- lib/variable-resolver.js | 22 ++++- package.json | 2 +- 9 files changed, 181 insertions(+), 137 deletions(-) diff --git a/lib/autocomplete-choice.js b/lib/autocomplete-choice.js index 1c08eb9a..8cc46fbe 100644 --- a/lib/autocomplete-choice.js +++ b/lib/autocomplete-choice.js @@ -1,3 +1,5 @@ +// NOTE: This provider is not currently in use. + module.exports = class ChoiceProvider { constructor () { this.selector = '*' @@ -10,10 +12,7 @@ module.exports = class ChoiceProvider { } getSuggestions () { - // TODO: Show all when just on default, show filtered and sorted when started typing - // TODO: Show even when no prefix - console.log("getting suggestions") - // debugger + // TODO: Show all initially and when no prefix, show filtered and sorted when started typing if (!this.active) { return undefined } return this.choices.map(c => { return { @@ -29,13 +28,6 @@ module.exports = class ChoiceProvider { this.suggestionPriority = 1000 this.excludeLowerPriority = true this.choices = choices - - this.oldConfig = atom.config.get("autocomplete-plus.autoActivationEnabled") - atom.config.set("autocomplete-plus.autoActivationEnabled", false, { save: false }) - - setTimeout(() => { - atom.commands.dispatch(document.activeElement, "autocomplete-plus:activate") // TODO: Remove dependency on specific provider - }, 5) // Because expanding the snippet from the autocomplete-menu immediately to a choice catches the close of the existing menu } deactivate () { @@ -44,6 +36,5 @@ module.exports = class ChoiceProvider { this.suggestionPriority = -Infinity this.excludeLowerPriority = false this.choices = [] - atom.config.set("autocomplete-plus.autoActivationEnabled", this.oldConfig, { save: false }) } } diff --git a/lib/insertion.js b/lib/insertion.js index d5a2e863..9024d97f 100644 --- a/lib/insertion.js +++ b/lib/insertion.js @@ -62,7 +62,7 @@ class Insertion { return !!this.substitution } - isChoices () { + isChoice () { return this.choices.length > 0 } diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index adae693f..251e9e6a 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -91,7 +91,7 @@ variableWithoutPlaceholder = '${' name:variableName '}' { } variableWithPlaceholder = '${' name:variableName ':' content:innerBodyContent '}' { - return { variable: name, content: content } + return { variable: name, content: content } } variableWithTransform = '${' name:variableName substitution:transform '}' { @@ -137,5 +137,5 @@ text = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char: } nonCloseBraceText = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:[^}] { return char })+ { - return text.join('') + return text.join('') } diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index e05deff9..094a6d53 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -14,21 +14,18 @@ module.exports = class SnippetExpansion { this.selections = [this.cursor.selection] const startPosition = this.cursor.selection.getBufferRange().start - let {body, tabStopList} = this.snippet.toString({ editor: this.editor, cursor: this.cursor, selectionRange: oldSelectionRange }) - this.tabStopList = tabStopList - - let tabStops = this.tabStopList.toArray() - - let indent = this.editor.lineTextForBufferRow(startPosition.row).match(/^\s*/)[0] - if (this.snippet.lineCount > 1 && indent) { - // Add proper leading indentation to the snippet - body = body.replace(/\n/g, `\n${indent}`) + let {body, tabStopList} = this.snippet.toString({ + editor: this.editor, + cursor: this.cursor, + indent: this.editor.lineTextForBufferRow(startPosition.row).match(/^\s*/)[0], + selectionRange: oldSelectionRange, // used by variable resolver + startPosition: startPosition + }) - // TODO: Remove concept of "body"; build on the fly each time to resolve variables and their transformations + this.tabStopList = tabStopList - tabStops = tabStops.map(tabStop => tabStop.copyWithIndent(indent)) - } + const tabStops = this.tabStopList.toArray() this.editor.transact(() => { this.ignoringBufferChanges(() => { @@ -37,7 +34,7 @@ module.exports = class SnippetExpansion { if (this.tabStopList.length > 0) { this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) - this.placeTabStopMarkers(startPosition, tabStops) + this.placeTabStopMarkers(tabStops) this.snippets.addExpansion(this.editor, this) this.editor.normalizeTabsInBufferRange(newRange) } @@ -119,7 +116,7 @@ module.exports = class SnippetExpansion { }) } - placeTabStopMarkers (startPosition, tabStops) { + placeTabStopMarkers (tabStops) { for (const tabStop of tabStops) { const {insertions} = tabStop const markers = [] @@ -127,12 +124,7 @@ module.exports = class SnippetExpansion { if (!tabStop.isValid()) { continue } for (const insertion of insertions) { - const {range} = insertion - const {start, end} = range - const marker = this.getMarkerLayer(this.editor).markBufferRange([ - startPosition.traverse(start), - startPosition.traverse(end) - ]) + const marker = this.getMarkerLayer(this.editor).markBufferRange(insertion.range) markers.push({ index: markers.length, marker, @@ -177,22 +169,17 @@ module.exports = class SnippetExpansion { this.tabStopIndex = tabStopIndex this.settingTabStop = true let markerSelected = false - let choices = [] const items = this.tabStopMarkers[this.tabStopIndex] if (items.length === 0) { return false } const ranges = [] - this.hasTransforms = false + let hasTransforms = false for (const item of items) { const {marker, insertion} = item - if (marker.isDestroyed()) { continue } - if (!marker.isValid()) { continue } - if (insertion.isChoices()) { - choices = insertion.choices - } + if (marker.isDestroyed() || !marker.isValid()) { continue } if (insertion.isTransformation()) { - this.hasTransforms = true + hasTransforms = true continue } ranges.push(marker.getBufferRange()) @@ -220,14 +207,7 @@ module.exports = class SnippetExpansion { this.settingTabStop = false // If this snippet has at least one transform, we need to observe changes // made to the editor so that we can update the transformed tab stops. - if (this.hasTransforms) { this.snippets.observeEditor(this.editor) } - - if (choices.length > 0) { - console.log("Activating choices") - this.snippets.snippetChoiceProvider.activate(choices) - } else { - this.snippets.snippetChoiceProvider.deactivate() - } + if (hasTransforms) { this.snippets.observeEditor(this.editor) } return markerSelected } diff --git a/lib/snippet.js b/lib/snippet.js index edb21482..59f901bc 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -1,4 +1,4 @@ -const {Range} = require('atom') +const {Point, Range} = require('atom') const TabStopList = require('./tab-stop-list') module.exports = class Snippet { @@ -16,69 +16,142 @@ module.exports = class Snippet { } toString (params) { - const tabStopList = new TabStopList(this) - const bodyText = [] - let row = 0 - let column = 0 - - // recursive helper function; mutates vars above - let extractTabStops = bodyTree => { - for (let segment of bodyTree) { - if (segment.index != null) { - let {index, content, substitution} = segment - if (index === 0) { index = Infinity; } - const start = [row, column] - extractTabStops(content) - const range = new Range(start, [row, column]) - const tabStop = tabStopList.findOrCreate({ - index, - snippet: this - }) - - let choices = [] - if (segment.choice && segment.choice.length > 0) { - choices = segment.choice - } - tabStop.addInsertion({ range, substitution, choices }) - } else { - if (segment.variable !== undefined) { - let value = this.variableResolver.resolve({ name: segment.variable, ...params }) - if (value === undefined) { - if (segment.content) { - extractTabStops(segment.content) - } - } else { - if (segment.substitution) { - try { - value = applyVariableTransformation(value, segment.substitution) - } catch (e) { - atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, { detail: e }) - } - } - segment = value - } - } - - if (typeof segment === 'string') { - bodyText.push(segment) - var segmentLines = segment.split('\n') - column += segmentLines.shift().length - let nextLine - while ((nextLine = segmentLines.shift()) != null) { - row += 1 - column = nextLine.length - } - } - } + // accumulator to keep track of constructed text, tabstops, and position + const acc = { + variableResolver: this.variableResolver, // TODO: Pass this in a more sensible way? (IDK; make all these functions methods?) + tabStopList: new TabStopList(this), + unknownVariables: new Map(), // name -> [range] + bodyText: '', + row: params.startPosition.row, + column: params.startPosition.column + } + + stringifyContent(this.bodyTree, params, acc) + + addTabstopsForUnknownVariables(acc.unknownVariables, acc.tabStopList) + + return { body: acc.bodyText, tabStopList: acc.tabStopList } + } +} + +function addTabstopsForUnknownVariables (unknowns, tabStopList) { + debugger + let index = tabStopList.getHighestIndex() + 1 + for (const ranges of unknowns.values()) { + const tabstop = tabStopList.findOrCreate({index, snippet: this}) + for (const range of ranges) { + tabstop.addInsertion({range}) + } + index++ + } +} + +function stringifyContent (content=[], params, acc) { + for (let node of content) { + if (node.index !== undefined) { // only tabstops and choices have an index + if (node.choice !== undefined) { + stringifyChoice(node, params, acc) + continue } + stringifyTabstop(node, params, acc) + continue + } + if (node.variable !== undefined) { + stringifyVariable(node, params, acc) + continue + } + stringifyText(node, params, acc) + } +} + +function stringifyTabstop (node, params, acc) { + const index = node.index === 0 ? Infinity : node.index + const start = new Point(acc.row, acc.column) + stringifyContent(node.content, params, acc) + const range = new Range(start, [acc.row, acc.column]) + acc.tabStopList.findOrCreate({index, snippet: this}).addInsertion({range, substitution: node.substitution}) +} + +function stringifyChoice (node, params, acc) { + // NOTE: will need to make sure all choices appear consistently + // VS Code treats first non-simple use as the true def. So + // `${1:foo} ${1|one,two|}` expands to `foo| foo|`, but reversing + // them expands to `one| one|` (with choice) + if (node.choice.length > 0) { + stringifyTabstop({...node, content: [node.choice[0]]}, params, acc) + } else { + stringifyTabstop(node, params, acc) + } +} + +// NOTE: VS Code does not apply the transformation in this case, so we won't either +function addUnknownVariable (variableName, acc) { + debugger + const {row, column} = acc + + acc.bodyText += variableName + acc.column += variableName.length + + const range = new Range([row, column], [row, acc.column]) + + const ranges = acc.unknownVariables.get(variableName) + if (ranges !== undefined) { + ranges.push(range) + return + } + + acc.unknownVariables.set(variableName, [range]) +} + +function stringifyVariable (node, params, acc) { + const {hasResolver, value} = acc.variableResolver.resolve({name: node.variable, ...params, row: acc.row, column: acc.column}) + + if (!hasResolver) { // variable unknown; convert to tabstop that goes at the end of all proper tabstops + addUnknownVariable(node.variable, acc) + return + } + + let resolvedValue + if (node.substitution) { + resolvedValue = applyVariableTransformation(value || '', node.substitution) + } else { + resolvedValue = value + } + + if (resolvedValue === undefined) { // variable known, but no value: use default contents or (implicitly) empty string + if (node.content) { + stringifyContent(node.content, params, acc) } + return + } + + // if we get to here, the variable is effectively a regular string now + stringifyText(resolvedValue, params, acc) +} + +// NOTE: Unlike the original version, this also applies +// the indent and uses the "true" row and columns +function stringifyText (text, params, acc) { + const origLength = text.length + const replacement = '\n' + params.indent - extractTabStops(this.bodyTree) - this.lineCount = row + 1 - this.insertions = tabStopList.getInsertions() + let rowDiff = 0 + let finalOffset = 0 - return { body: bodyText.join(''), tabStopList } + text = text.replace(/\n/g, (...arg) => { + rowDiff += 1 + finalOffset = arg[arg.length - 2] // this holds the current match offset relative to the original string + return replacement + }) + + if (rowDiff > 0) { + acc.row += rowDiff + acc.column = params.indent.length + (origLength - finalOffset - 1) + } else { + acc.column += origLength } + + acc.bodyText += text } function applyVariableTransformation (value, substitution) { diff --git a/lib/tab-stop-list.js b/lib/tab-stop-list.js index 0d3bd010..b18e2707 100644 --- a/lib/tab-stop-list.js +++ b/lib/tab-stop-list.js @@ -34,6 +34,10 @@ class TabStopList { return results } + getHighestIndex () { + return Math.max(Object.keys(this.list)) + } + toArray () { let results = [] this.forEachIndex(index => { diff --git a/lib/tab-stop.js b/lib/tab-stop.js index 3e42b69d..32e135a3 100644 --- a/lib/tab-stop.js +++ b/lib/tab-stop.js @@ -20,8 +20,8 @@ class TabStop { return !all } - addInsertion ({ range, substitution, choices=[] }) { - let insertion = new Insertion({ range, substitution, choices }) + addInsertion (insertionParams) { + let insertion = new Insertion(insertionParams) let insertions = this.insertions insertions.push(insertion) insertions = insertions.sort((i1, i2) => { @@ -34,28 +34,6 @@ class TabStop { } this.insertions = insertions } - - copyWithIndent (indent) { - let { snippet, index, insertions } = this - let newInsertions = insertions.map(insertion => { - let { range, substitution } = insertion - let newRange = Range.fromObject(range, true) - if (newRange.start.row) { - newRange.start.column += indent.length - newRange.end.column += indent.length - } - return new Insertion({ - range: newRange, - substitution - }) - }) - - return new TabStop({ - snippet, - index, - insertions: newInsertions - }) - } } module.exports = TabStop diff --git a/lib/variable-resolver.js b/lib/variable-resolver.js index 33b3ac2e..3204f943 100644 --- a/lib/variable-resolver.js +++ b/lib/variable-resolver.js @@ -39,14 +39,32 @@ module.exports = class VariableResolver { this.resolvers.set(variable, resolver) } + + /* + params = { + editor: the TextEditor we are expanding in + cursor: the cursor we are expanding from + indent: the indent of the original cursor line. Automatically applied to all text (post variable transformation) + selectionRange: the original selection range of the cursor. This has been modified on the actual cursor to now select the prefix + startPosition: the cursor selection start position, after being adjusted to select the prefix + row: the row the start of the variable will be inserted on (final) + column: the column the start of the variable will be inserted on (final; accounts for indent) + } + */ resolve (params) { const resolver = this.resolvers.get(params.name) if (resolver) { - return resolver(params) + return { + hasResolver: true, + value: resolver(params) + } } - return undefined + return { + hasResolver: false, + value: undefined + } } } diff --git a/package.json b/package.json index a7294f1b..55154da3 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "0.1.0": "provideSnippets" } }, - "autocomplete.provider": { + "autocomplete.provider.driver": { "versions": { "4.0.0": "provideAutocomplete" } From 443bd628d2d56c8f5f938ff01e9e72439bf06919 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Sun, 10 Feb 2019 22:22:16 +1000 Subject: [PATCH 25/77] remove debugger and pass entire acc to variable resolver --- lib/snippet.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/snippet.js b/lib/snippet.js index 59f901bc..fc131fad 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -35,7 +35,6 @@ module.exports = class Snippet { } function addTabstopsForUnknownVariables (unknowns, tabStopList) { - debugger let index = tabStopList.getHighestIndex() + 1 for (const ranges of unknowns.values()) { const tabstop = tabStopList.findOrCreate({index, snippet: this}) @@ -73,6 +72,7 @@ function stringifyTabstop (node, params, acc) { } function stringifyChoice (node, params, acc) { + // TODO: Support choices // NOTE: will need to make sure all choices appear consistently // VS Code treats first non-simple use as the true def. So // `${1:foo} ${1|one,two|}` expands to `foo| foo|`, but reversing @@ -86,12 +86,9 @@ function stringifyChoice (node, params, acc) { // NOTE: VS Code does not apply the transformation in this case, so we won't either function addUnknownVariable (variableName, acc) { - debugger const {row, column} = acc - acc.bodyText += variableName acc.column += variableName.length - const range = new Range([row, column], [row, acc.column]) const ranges = acc.unknownVariables.get(variableName) @@ -104,7 +101,7 @@ function addUnknownVariable (variableName, acc) { } function stringifyVariable (node, params, acc) { - const {hasResolver, value} = acc.variableResolver.resolve({name: node.variable, ...params, row: acc.row, column: acc.column}) + const {hasResolver, value} = acc.variableResolver.resolve({name: node.variable, ...params, ...acc}) if (!hasResolver) { // variable unknown; convert to tabstop that goes at the end of all proper tabstops addUnknownVariable(node.variable, acc) From 49f7e050029179c31cd7052d1240d0a907c7be4b Mon Sep 17 00:00:00 2001 From: Aerijo Date: Sun, 10 Feb 2019 22:28:44 +1000 Subject: [PATCH 26/77] Add default params fallback --- lib/snippet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/snippet.js b/lib/snippet.js index fc131fad..3af7647c 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -15,7 +15,7 @@ module.exports = class Snippet { this.variableResolver = variableResolver } - toString (params) { + toString (params = {startPosition: {row: 0, column: 0}, indent: ''}) { // accumulator to keep track of constructed text, tabstops, and position const acc = { variableResolver: this.variableResolver, // TODO: Pass this in a more sensible way? (IDK; make all these functions methods?) From 6542e769ab19175378430619d2cd9bd6ccd6e624 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Mon, 11 Feb 2019 00:07:29 +1000 Subject: [PATCH 27/77] Add in implicit end stop by default --- lib/snippet-expansion.js | 4 +++- lib/snippet.js | 8 +++++++- lib/tab-stop-list.js | 9 ++++++++- package.json | 7 +++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 094a6d53..ce8f97c8 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -117,6 +117,8 @@ module.exports = class SnippetExpansion { } placeTabStopMarkers (tabStops) { + const markerLayer = this.getMarkerLayer(this.editor) + for (const tabStop of tabStops) { const {insertions} = tabStop const markers = [] @@ -124,7 +126,7 @@ module.exports = class SnippetExpansion { if (!tabStop.isValid()) { continue } for (const insertion of insertions) { - const marker = this.getMarkerLayer(this.editor).markBufferRange(insertion.range) + const marker = markerLayer.markBufferRange(insertion.range) markers.push({ index: markers.length, marker, diff --git a/lib/snippet.js b/lib/snippet.js index 3af7647c..58cc5eb3 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -28,7 +28,12 @@ module.exports = class Snippet { stringifyContent(this.bodyTree, params, acc) - addTabstopsForUnknownVariables(acc.unknownVariables, acc.tabStopList) + const index = addTabstopsForUnknownVariables(acc.unknownVariables, acc.tabStopList) + + if (!acc.tabStopList.hasEndStop && atom.config.get("snippets.implicitEndTabstop")) { + const endRange = new Range([acc.row, acc.column], [acc.row, acc.column]) + acc.tabStopList.findOrCreate({index, snippet: this}).addInsertion({range: endRange}) + } return { body: acc.bodyText, tabStopList: acc.tabStopList } } @@ -43,6 +48,7 @@ function addTabstopsForUnknownVariables (unknowns, tabStopList) { } index++ } + return index } function stringifyContent (content=[], params, acc) { diff --git a/lib/tab-stop-list.js b/lib/tab-stop-list.js index b18e2707..4dfa576d 100644 --- a/lib/tab-stop-list.js +++ b/lib/tab-stop-list.js @@ -35,7 +35,14 @@ class TabStopList { } getHighestIndex () { - return Math.max(Object.keys(this.list)) + // the keys are strings... + return Object.keys(this.list).reduce((m, i) => { + const index = parseInt(i) + if (index > m) { // TODO: Does this handle $0? + return index + } + return m + }, 0) } toArray () { diff --git a/package.json b/package.json index 55154da3..61651dbe 100644 --- a/package.json +++ b/package.json @@ -42,5 +42,12 @@ }, "devDependencies": { "coffeelint": "^1.9.7" + }, + "configSchema": { + "implicitEndTabstop": { + "description": "Add a final tabstop at the end of the snippet when the $0 stop is not set", + "type": "boolean", + "default": true + } } } From e002906a0081cd2a5859ed1336350003bda899da Mon Sep 17 00:00:00 2001 From: Aerijo Date: Mon, 11 Feb 2019 11:52:37 +1000 Subject: [PATCH 28/77] Fix implicit end stops & catch transform errors --- lib/snippet.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/snippet.js b/lib/snippet.js index 58cc5eb3..1bb9fd0f 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -28,11 +28,11 @@ module.exports = class Snippet { stringifyContent(this.bodyTree, params, acc) - const index = addTabstopsForUnknownVariables(acc.unknownVariables, acc.tabStopList) + addTabstopsForUnknownVariables(acc.unknownVariables, acc.tabStopList) if (!acc.tabStopList.hasEndStop && atom.config.get("snippets.implicitEndTabstop")) { const endRange = new Range([acc.row, acc.column], [acc.row, acc.column]) - acc.tabStopList.findOrCreate({index, snippet: this}).addInsertion({range: endRange}) + acc.tabStopList.findOrCreate({index: Infinity, snippet: this}).addInsertion({range: endRange}) } return { body: acc.bodyText, tabStopList: acc.tabStopList } @@ -48,7 +48,6 @@ function addTabstopsForUnknownVariables (unknowns, tabStopList) { } index++ } - return index } function stringifyContent (content=[], params, acc) { @@ -116,7 +115,11 @@ function stringifyVariable (node, params, acc) { let resolvedValue if (node.substitution) { - resolvedValue = applyVariableTransformation(value || '', node.substitution) + try { + resolvedValue = applyVariableTransformation(value || '', node.substitution) + } catch (e) { + atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, { detail: e }) // TODO: add snippet location + } } else { resolvedValue = value } From 81dbaa01b0c721e79f6aae21ecae9343dd5e3959 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Mon, 11 Feb 2019 11:54:46 +1000 Subject: [PATCH 29/77] rename driver service --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 61651dbe..827af597 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "0.1.0": "provideSnippets" } }, - "autocomplete.provider.driver": { + "autocomplete.driver": { "versions": { "4.0.0": "provideAutocomplete" } From c64242eb2b1bf355bc9f8baff465cca366086f9d Mon Sep 17 00:00:00 2001 From: Aerijo Date: Mon, 11 Feb 2019 12:29:15 +1000 Subject: [PATCH 30/77] :art: --- lib/snippet-body.pegjs | 4 ++-- lib/snippet-expansion.js | 4 ++-- lib/snippet.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index 251e9e6a..c693a8a5 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -132,10 +132,10 @@ flags = flags:[a-z]* { return flags.join('') } -text = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:. { return char })+ { +text = text:(escaped / !tabstop !variable !choice char:. { return char })+ { return text.join('') } -nonCloseBraceText = text:(escaped / !tabstop !tabstopWithPlaceholder !variable !choice char:[^}] { return char })+ { +nonCloseBraceText = text:(escaped / !tabstop !variable !choice char:[^}] { return char })+ { return text.join('') } diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index ce8f97c8..6c47ab1e 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -83,11 +83,11 @@ module.exports = class SnippetExpansion { applyAllTransformations () { this.editor.transact(() => { this.tabStopMarkers.forEach((item, index) => - this.applyTransformations(index, true)) + this.applyTransformations(index)) }) } - applyTransformations (tabStop, initial = false) { + applyTransformations (tabStop) { const items = [...this.tabStopMarkers[tabStop]] if (items.length === 0) { return } diff --git a/lib/snippet.js b/lib/snippet.js index 1bb9fd0f..520bbd1f 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -144,9 +144,9 @@ function stringifyText (text, params, acc) { let rowDiff = 0 let finalOffset = 0 - text = text.replace(/\n/g, (...arg) => { + text = text.replace(/\n/g, (...match) => { rowDiff += 1 - finalOffset = arg[arg.length - 2] // this holds the current match offset relative to the original string + finalOffset = match[match.length - 2] // this holds the current match offset relative to the original string return replacement }) From b79780234436010b0a175b9883cd820643b4a787 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Mon, 11 Feb 2019 12:51:30 +1000 Subject: [PATCH 31/77] fix resolver service --- lib/variable-resolver.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/variable-resolver.js b/lib/variable-resolver.js index 3204f943..ab5d2f81 100644 --- a/lib/variable-resolver.js +++ b/lib/variable-resolver.js @@ -35,8 +35,11 @@ module.exports = class VariableResolver { ]) } - add (variable, resolver) { - this.resolvers.set(variable, resolver) + add (resolver) { + const varName = resolver.name + const resolve = resolver.resolve + if (typeof varName !== 'string' || typeof resolve !== 'function') return + this.resolvers.set(varName, resolve) } From 75a06417d99f7980cc8431808428a064f82eac05 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Mon, 11 Feb 2019 17:37:56 +1000 Subject: [PATCH 32/77] Consistent transform behaviour --- lib/insertion.js | 77 +----------------------- lib/snippet-expansion.js | 5 +- lib/snippet.js | 50 +--------------- lib/util.js | 124 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 123 deletions(-) create mode 100644 lib/util.js diff --git a/lib/insertion.js b/lib/insertion.js index 9024d97f..b7d27292 100644 --- a/lib/insertion.js +++ b/lib/insertion.js @@ -1,48 +1,4 @@ -const ESCAPES = { - u: (flags) => { - flags.lowercaseNext = false - flags.uppercaseNext = true - }, - l: (flags) => { - flags.uppercaseNext = false - flags.lowercaseNext = true - }, - U: (flags) => { - flags.lowercaseAll = false - flags.uppercaseAll = true - }, - L: (flags) => { - flags.uppercaseAll = false - flags.lowercaseAll = true - }, - E: (flags) => { - flags.uppercaseAll = false - flags.lowercaseAll = false - }, - r: (flags, result) => { - result.push('\\r') - }, - n: (flags, result) => { - result.push('\\n') - }, - $: (flags, result) => { - result.push('$') - } -} - -function transformText (str, flags) { - if (flags.uppercaseAll) { - return str.toUpperCase() - } else if (flags.lowercaseAll) { - return str.toLowerCase() - } else if (flags.uppercaseNext) { - flags.uppercaseNext = false - return str.replace(/^./, s => s.toUpperCase()) - } else if (flags.lowercaseNext) { - return str.replace(/^./, s => s.toLowerCase()) - } - return str -} +const { transformWithSubstitution } = require('./util') class Insertion { constructor ({ range, substitution, choices=[] }) { @@ -52,9 +8,7 @@ class Insertion { if (substitution.replace === undefined) { substitution.replace = '' } - this.replacer = this.makeReplacer(substitution.replace) } - this.choices = choices } @@ -66,35 +20,10 @@ class Insertion { return this.choices.length > 0 } - makeReplacer (replace) { - return function replacer (...match) { - let flags = { - uppercaseAll: false, - lowercaseAll: false, - uppercaseNext: false, - lowercaseNext: false - } - replace = [...replace] - let result = [] - replace.forEach(token => { - if (typeof token === 'string') { - result.push(transformText(token, flags)) - } else if (token.escape) { - ESCAPES[token.escape](flags, result) - } else if (token.backreference) { - let transformed = transformText(match[token.backreference], flags) - result.push(transformed) - } - }) - return result.join('') - } - } - transform (input) { - let { substitution } = this - if (!substitution) { return input } - return input.replace(substitution.find, this.replacer) + return transformWithSubstitution(input, this.substitution) } + } module.exports = Insertion diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 6c47ab1e..429b1e7e 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -1,4 +1,5 @@ const {CompositeDisposable, Range, Point} = require('atom') +const {getEndpointOfText} = require('./util') module.exports = class SnippetExpansion { constructor(snippet, editor, cursor, oldSelectionRange, snippets) { @@ -15,7 +16,7 @@ module.exports = class SnippetExpansion { const startPosition = this.cursor.selection.getBufferRange().start - let {body, tabStopList} = this.snippet.toString({ + const {body, tabStopList} = this.snippet.toString({ editor: this.editor, cursor: this.cursor, indent: this.editor.lineTextForBufferRow(startPosition.row).match(/^\s*/)[0], @@ -109,7 +110,7 @@ module.exports = class SnippetExpansion { this.editor.transact(() => this.editor.setTextInBufferRange(range, outputText)) const newRange = new Range( range.start, - range.start.traverse(new Point(0, outputText.length)) + range.start.traverse(getEndpointOfText(outputText)) ) marker.setBufferRange(newRange) } diff --git a/lib/snippet.js b/lib/snippet.js index 520bbd1f..fa50bb47 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -1,5 +1,6 @@ const {Point, Range} = require('atom') const TabStopList = require('./tab-stop-list') +const {transformWithSubstitution} = require('./util') module.exports = class Snippet { constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, bodyText, variableResolver}) { @@ -116,7 +117,7 @@ function stringifyVariable (node, params, acc) { let resolvedValue if (node.substitution) { try { - resolvedValue = applyVariableTransformation(value || '', node.substitution) + resolvedValue = transformWithSubstitution(value || '', node.substitution) } catch (e) { atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, { detail: e }) // TODO: add snippet location } @@ -139,7 +140,7 @@ function stringifyVariable (node, params, acc) { // the indent and uses the "true" row and columns function stringifyText (text, params, acc) { const origLength = text.length - const replacement = '\n' + params.indent + const replacement = '\n' + params.indent // TODO: proper line endings let rowDiff = 0 let finalOffset = 0 @@ -159,48 +160,3 @@ function stringifyText (text, params, acc) { acc.bodyText += text } - -function applyVariableTransformation (value, substitution) { - // TODO: Better bounds and type checking so errors aren't as cryptic - - const replace = substitution.replace - const result = value.replace(substitution.find, (...match) => { - let interimResult = '' - for (let i = 0; i < replace.length; i++) { - if (typeof replace[i] === "string") { - interimResult += replace[i] - continue - } - - const format = replace[i] - - const index = format.backreference - if (index >= match.length - 2) { throw new Error ("Index too high") } - - let capture = match[index] - if (capture === undefined) { continue } - - if (format.transform) { - // TODO: Support custom transforms as well? - switch (format.transform) { - case 'upcase': - capture = capture.toLocaleUpperCase() - break - case 'downcase': - capture = capture.toLocaleLowerCase() - break - case 'capitalize': - capture = capture ? capture[0].toLocaleUpperCase() + capture.substr(1) : '' - break - default: {} - } - } - - interimResult += capture - } - - return interimResult - }) - - return result -} diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 00000000..384064e0 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,124 @@ +const {Point} = require('atom') + +module.exports = { transformWithSubstitution, getEndpointOfText } + +const ESCAPES = { + u: (flags) => { + flags.lowercaseNext = false + flags.uppercaseNext = true + }, + l: (flags) => { + flags.uppercaseNext = false + flags.lowercaseNext = true + }, + U: (flags) => { + flags.lowercaseAll = false + flags.uppercaseAll = true + }, + L: (flags) => { + flags.uppercaseAll = false + flags.lowercaseAll = true + }, + E: (flags) => { + flags.uppercaseAll = false + flags.lowercaseAll = false + }, + // r: (flags, result) => { + // return result += '\\r' + // }, + // n: (flags, result) => { + // return result += '\\n' + // }, + // $: (flags, result) => { + // return result += '$' + // } +} + +function flagTransformText (str, flags) { + if (flags.uppercaseAll) { + return str.toUpperCase() + } else if (flags.lowercaseAll) { + return str.toLowerCase() + } else if (flags.uppercaseNext) { + flags.uppercaseNext = false + return str.replace(/^./, s => s.toUpperCase()) + } else if (flags.lowercaseNext) { + return str.replace(/^./, s => s.toLowerCase()) + } + return str +} + +function transformWithSubstitution (input, substitution) { + if (!substitution) { return input } + + return input.replace(substitution.find, (...match) => { + const flags = { + uppercaseAll: false, + lowercaseAll: false, + uppercaseNext: false, + lowercaseNext: false + } + + let result = '' + + substitution.replace.forEach(token => { + if (typeof token === 'string') { + result += flagTransformText(token, flags) + return + } + + if (token.escape !== undefined) { + switch (token.escape) { + case 'r': + result += '\\r' + break + case 'n': + result += '\\n' + break + case '$': + result += '$' + break + default: + ESCAPES[token.escape](flags) + } + } else if (token.backreference !== undefined) { + const original = match[token.backreference] + if (token.transform) { + switch (token.transform) { + case 'upcase': + result += original.toLocaleUpperCase() + break + case 'downcase': + result += original.toLocaleLowerCase() + break + case 'capitalize': + result += original ? original[0].toLocaleUpperCase() + original.substr(1) : '' + break + default: {} // TODO: Allow custom transformation handling (important for future proofing changes in the standard) + } + } else { + result += flagTransformText(original, flags) + } + } + }) + + return result + }) +} + +function getEndpointOfText (text) { + const newlineMatch = /\n/g + let rows = 0 + let lastIndex = 0 + + while (newlineMatch.exec(text) !== null) { + rows += 1 + lastIndex = newlineMatch.lastIndex + } + + if (rows === 0) { + return new Point(0, text.length) + } else { + return new Point(rows, text.length - lastIndex) + } +} From ce4d69eac953c8f77619bb7b2abf703399acde2f Mon Sep 17 00:00:00 2001 From: Aerijo Date: Mon, 11 Feb 2019 19:47:08 +1000 Subject: [PATCH 33/77] Start messing with history grouping --- lib/snippet-expansion.js | 19 ++++++++----------- lib/snippet.js | 2 +- lib/snippets.js | 20 ++++++++++---------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 429b1e7e..e96bcc42 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -27,19 +27,16 @@ module.exports = class SnippetExpansion { this.tabStopList = tabStopList const tabStops = this.tabStopList.toArray() - this.editor.transact(() => { this.ignoringBufferChanges(() => { - this.editor.transact(() => { - const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) - if (this.tabStopList.length > 0) { - this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) - this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) - this.placeTabStopMarkers(tabStops) - this.snippets.addExpansion(this.editor, this) - this.editor.normalizeTabsInBufferRange(newRange) - } - }) + const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) + if (this.tabStopList.length > 0) { + this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) + this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) + this.placeTabStopMarkers(tabStops) + this.snippets.addExpansion(this.editor, this) + this.editor.normalizeTabsInBufferRange(newRange) + } }) }) } diff --git a/lib/snippet.js b/lib/snippet.js index fa50bb47..77d7cba1 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -119,7 +119,7 @@ function stringifyVariable (node, params, acc) { try { resolvedValue = transformWithSubstitution(value || '', node.substitution) } catch (e) { - atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, { detail: e }) // TODO: add snippet location + atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, {detail: e}) // TODO: add snippet location } } else { resolvedValue = value diff --git a/lib/snippets.js b/lib/snippets.js index cccc2d3b..130fd62c 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -526,16 +526,16 @@ module.exports = { }) this.findOrCreateMarkerLayer(editor) - editor.transact(() => { - const cursors = editor.getCursors() - for (const cursor of cursors) { - const cursorPosition = cursor.getBufferPosition() - const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) - const oldSelectionRange = cursor.selection.getBufferRange() - cursor.selection.setBufferRange([startPoint, cursorPosition]) - this.insert(snippet, editor, cursor, oldSelectionRange) - } - }) + const checkpoint = editor.createCheckpoint() + const cursors = editor.getCursors() + for (const cursor of cursors) { + const cursorPosition = cursor.getBufferPosition() + const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) + const oldSelectionRange = cursor.selection.getBufferRange() + cursor.selection.setBufferRange([startPoint, cursorPosition]) + this.insert(snippet, editor, cursor, oldSelectionRange) + } + editor.groupChangesSinceCheckpoint(checkpoint) return true }, From 417dd936785393bfcfdfd0160bbc6ea1a6da3421 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 14:22:14 +1000 Subject: [PATCH 34/77] Hack in forced undo barrier --- lib/snippet-expansion.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index e96bcc42..9e965bb7 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -140,6 +140,8 @@ module.exports = class SnippetExpansion { } goToNextTabStop () { + this.editor.buffer.historyProvider.createCheckpoint({markers: undefined, isBarrier: false}) // HACK: We need `isBarrier`, but the normal methods enforce false + const nextIndex = this.tabStopIndex + 1 if (nextIndex < this.tabStopMarkers.length) { if (this.setTabStopIndex(nextIndex)) { From bea05d986962f67c75a2272efcedbd94d28fe029 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 14:37:28 +1000 Subject: [PATCH 35/77] extract to method --- lib/snippet-expansion.js | 43 ++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 9e965bb7..bc64ff32 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -93,24 +93,26 @@ module.exports = class SnippetExpansion { const primaryRange = primary.marker.getBufferRange() const inputText = this.editor.getTextInBufferRange(primaryRange) - this.ignoringBufferChanges(() => { - for (const item of items) { - const {marker, insertion} = item - var range = marker.getBufferRange() - - // Don't transform mirrored tab stops. They have their own cursors, so - // mirroring happens automatically. - if (!insertion.isTransformation()) { continue } - - var outputText = insertion.transform(inputText) - - this.editor.transact(() => this.editor.setTextInBufferRange(range, outputText)) - const newRange = new Range( - range.start, - range.start.traverse(getEndpointOfText(outputText)) - ) - marker.setBufferRange(newRange) - } + this.editor.transact(() => { + this.ignoringBufferChanges(() => { + for (const item of items) { + const {marker, insertion} = item + var range = marker.getBufferRange() + + // Don't transform mirrored tab stops. They have their own cursors, so + // mirroring happens automatically. + if (!insertion.isTransformation()) { continue } + + var outputText = insertion.transform(inputText) + + this.editor.setTextInBufferRange(range, outputText) + const newRange = new Range( + range.start, + range.start.traverse(getEndpointOfText(outputText)) + ) + marker.setBufferRange(newRange) + } + }) }) } @@ -139,9 +141,11 @@ module.exports = class SnippetExpansion { this.applyAllTransformations() } - goToNextTabStop () { + insertBarrierCheckpoint () { this.editor.buffer.historyProvider.createCheckpoint({markers: undefined, isBarrier: false}) // HACK: We need `isBarrier`, but the normal methods enforce false + } + goToNextTabStop () { const nextIndex = this.tabStopIndex + 1 if (nextIndex < this.tabStopMarkers.length) { if (this.setTabStopIndex(nextIndex)) { @@ -168,6 +172,7 @@ module.exports = class SnippetExpansion { } setTabStopIndex (tabStopIndex) { + this.insertBarrierCheckpoint() this.tabStopIndex = tabStopIndex this.settingTabStop = true let markerSelected = false From b156bb2898e204e55b2ff0e6b28d6397d6dfcf4b Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 14:59:41 +1000 Subject: [PATCH 36/77] Maybe this? --- lib/snippet-expansion.js | 39 +++++++++++++++++++-------------------- lib/snippets.js | 21 ++++++++++++--------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index bc64ff32..88e56816 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -27,6 +27,7 @@ module.exports = class SnippetExpansion { this.tabStopList = tabStopList const tabStops = this.tabStopList.toArray() + this.insertBarrierCheckpoint() this.editor.transact(() => { this.ignoringBufferChanges(() => { const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) @@ -93,26 +94,24 @@ module.exports = class SnippetExpansion { const primaryRange = primary.marker.getBufferRange() const inputText = this.editor.getTextInBufferRange(primaryRange) - this.editor.transact(() => { - this.ignoringBufferChanges(() => { - for (const item of items) { - const {marker, insertion} = item - var range = marker.getBufferRange() - - // Don't transform mirrored tab stops. They have their own cursors, so - // mirroring happens automatically. - if (!insertion.isTransformation()) { continue } - - var outputText = insertion.transform(inputText) - - this.editor.setTextInBufferRange(range, outputText) - const newRange = new Range( - range.start, - range.start.traverse(getEndpointOfText(outputText)) - ) - marker.setBufferRange(newRange) - } - }) + this.ignoringBufferChanges(() => { + for (const item of items) { + const {marker, insertion} = item + var range = marker.getBufferRange() + + // Don't transform mirrored tab stops. They have their own cursors, so + // mirroring happens automatically. + if (!insertion.isTransformation()) { continue } + + var outputText = insertion.transform(inputText) + + this.editor.setTextInBufferRange(range, outputText) + const newRange = new Range( + range.start, + range.start.traverse(getEndpointOfText(outputText)) + ) + marker.setBufferRange(newRange) + } }) } diff --git a/lib/snippets.js b/lib/snippets.js index 130fd62c..1f05f430 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -526,16 +526,19 @@ module.exports = { }) this.findOrCreateMarkerLayer(editor) - const checkpoint = editor.createCheckpoint() const cursors = editor.getCursors() - for (const cursor of cursors) { - const cursorPosition = cursor.getBufferPosition() - const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) - const oldSelectionRange = cursor.selection.getBufferRange() - cursor.selection.setBufferRange([startPoint, cursorPosition]) - this.insert(snippet, editor, cursor, oldSelectionRange) - } - editor.groupChangesSinceCheckpoint(checkpoint) + + editor.transact(() => { + for (const cursor of cursors) { + const cursorPosition = cursor.getBufferPosition() + const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) + + const oldSelectionRange = cursor.selection.getBufferRange() + cursor.selection.setBufferRange([startPoint, cursorPosition]) + this.insert(snippet, editor, cursor, oldSelectionRange) + } + }) + return true }, From 77e8bb1ac27e6545ed521571639ab518b4b436a7 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 17:05:46 +1000 Subject: [PATCH 37/77] Use checkpoint instead --- lib/snippets.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/snippets.js b/lib/snippets.js index 1f05f430..c7fbda5b 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -528,16 +528,15 @@ module.exports = { this.findOrCreateMarkerLayer(editor) const cursors = editor.getCursors() - editor.transact(() => { - for (const cursor of cursors) { - const cursorPosition = cursor.getBufferPosition() - const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) - - const oldSelectionRange = cursor.selection.getBufferRange() - cursor.selection.setBufferRange([startPoint, cursorPosition]) - this.insert(snippet, editor, cursor, oldSelectionRange) - } - }) + const checkpoint = editor.createCheckpoint() + for (const cursor of cursors) { + const cursorPosition = cursor.getBufferPosition() + const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) + const oldSelectionRange = cursor.selection.getBufferRange() + cursor.selection.setBufferRange([startPoint, cursorPosition]) + this.insert(snippet, editor, cursor, oldSelectionRange) + } + editor.groupChangesSinceCheckpoint(checkpoint, {deleteCheckpoint: true}) return true }, From 30cd9fb285c2b46ccc51b9748cba63cf36c90082 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 18:02:34 +1000 Subject: [PATCH 38/77] Try more stuff --- lib/snippet-expansion.js | 9 ++++++--- lib/snippets.js | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 88e56816..100876fe 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -6,6 +6,7 @@ module.exports = class SnippetExpansion { this.settingTabStop = false this.isIgnoringBufferChanges = false this.onUndoOrRedo = this.onUndoOrRedo.bind(this) + this.isUndoingOrRedoing = false this.snippet = snippet this.editor = editor this.cursor = cursor @@ -145,6 +146,7 @@ module.exports = class SnippetExpansion { } goToNextTabStop () { + this.insertBarrierCheckpoint() const nextIndex = this.tabStopIndex + 1 if (nextIndex < this.tabStopMarkers.length) { if (this.setTabStopIndex(nextIndex)) { @@ -167,7 +169,10 @@ module.exports = class SnippetExpansion { } goToPreviousTabStop () { - if (this.tabStopIndex > 0) { this.setTabStopIndex(this.tabStopIndex - 1) } + if (this.tabStopIndex > 0) { + this.insertBarrierCheckpoint() + this.setTabStopIndex(this.tabStopIndex - 1) + } } setTabStopIndex (tabStopIndex) { @@ -191,8 +196,6 @@ module.exports = class SnippetExpansion { ranges.push(marker.getBufferRange()) } - - if (ranges.length > 0) { for (const selection of this.selections.slice(ranges.length)) { selection.destroy() } this.selections = this.selections.slice(0, ranges.length) diff --git a/lib/snippets.js b/lib/snippets.js index c7fbda5b..9ad7f976 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -598,13 +598,14 @@ module.exports = { if ((activeExpansions.length === 0) || activeExpansions[0].isIgnoringBufferChanges) { return } this.ignoringTextChangesForEditor(editor, () => - editor.transact(() => + editor.transact(editor.undoGroupingInterval || 300, () => // HACK: relies on private editor property, and on one that will likely change in future (to non time based undo) activeExpansions.map(expansion => expansion.textChanged(event))) ) + // NOTE: Making the checkpoint appears to have contributed to the transformation undo issues // Create a checkpoint here to consolidate all the changes we just made into // the transaction that prompted them. - this.makeCheckpoint(editor) + // this.makeCheckpoint(editor) }, // Perform an action inside the editor without triggering our `textChanged` From 7cb2c8fc33494c2157b35ee4817c311933d27b14 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 19:23:24 +1000 Subject: [PATCH 39/77] Improvements --- lib/snippet-expansion.js | 17 +++++------------ lib/util.js | 23 +++++------------------ 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 100876fe..c017becd 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -148,23 +148,16 @@ module.exports = class SnippetExpansion { goToNextTabStop () { this.insertBarrierCheckpoint() const nextIndex = this.tabStopIndex + 1 - if (nextIndex < this.tabStopMarkers.length) { + if (nextIndex < this.tabStopMarkers.length - this.tabStopList.hasEndStop) { // this cracks me up for some reason :) it's so wrong, but it works 'cause true == 1 if (this.setTabStopIndex(nextIndex)) { return true } else { - return this.goToNextTabStop() + return this.goToNextTabStop() // recursively try the next one } } else { - // The user has tabbed past the last tab stop. If the last tab stop is a - // $0, we shouldn't move the cursor any further. - if (this.tabStopList.hasEndStop) { - this.destroy() - return false - } else { - const succeeded = this.goToEndOfLastTabStop() - this.destroy() - return succeeded - } + const succeeded = this.goToEndOfLastTabStop() + this.destroy() + return succeeded } } diff --git a/lib/util.js b/lib/util.js index 384064e0..b11461be 100644 --- a/lib/util.js +++ b/lib/util.js @@ -22,16 +22,7 @@ const ESCAPES = { E: (flags) => { flags.uppercaseAll = false flags.lowercaseAll = false - }, - // r: (flags, result) => { - // return result += '\\r' - // }, - // n: (flags, result) => { - // return result += '\\n' - // }, - // $: (flags, result) => { - // return result += '$' - // } + } } function flagTransformText (str, flags) { @@ -107,18 +98,14 @@ function transformWithSubstitution (input, substitution) { } function getEndpointOfText (text) { - const newlineMatch = /\n/g - let rows = 0 + const newlineMatch = /\n/g // NOTE: This is the same as used by TextBuffer, so should work even with \r + let row = 0 let lastIndex = 0 while (newlineMatch.exec(text) !== null) { - rows += 1 + row += 1 lastIndex = newlineMatch.lastIndex } - if (rows === 0) { - return new Point(0, text.length) - } else { - return new Point(rows, text.length - lastIndex) - } + return new Point(row, text.length - lastIndex) } From b8730af7bb07f1567fd59fe7099e1175e74f3950 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 19:38:52 +1000 Subject: [PATCH 40/77] clean end tabstop logic --- lib/snippet-expansion.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index c017becd..e245c376 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -148,17 +148,22 @@ module.exports = class SnippetExpansion { goToNextTabStop () { this.insertBarrierCheckpoint() const nextIndex = this.tabStopIndex + 1 - if (nextIndex < this.tabStopMarkers.length - this.tabStopList.hasEndStop) { // this cracks me up for some reason :) it's so wrong, but it works 'cause true == 1 - if (this.setTabStopIndex(nextIndex)) { - return true - } else { - return this.goToNextTabStop() // recursively try the next one - } - } else { + + // if we have an endstop (implicit ends have already been added) it will be the last one + if (nextIndex === this.tabStopMarkers.length - 1 && this.tabStopList.hasEndStop) { const succeeded = this.goToEndOfLastTabStop() this.destroy() return succeeded } + + // we are not at the end, and the next is not the endstop; just go to next stop + if (nextIndex < this.tabStopMarkers.length) { + return this.setTabStopIndex(nextIndex) || this.goToNextTabStop() + } + + // we have just tabbed past the final tabstop; silently clean up, and let an actual tab be inserted + this.destroy() + return false } goToPreviousTabStop () { From 0544e5135995a74eb0246daed986737fbe052e36 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 19:41:31 +1000 Subject: [PATCH 41/77] also make it correct -_- --- lib/snippet-expansion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index e245c376..f482c567 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -151,7 +151,7 @@ module.exports = class SnippetExpansion { // if we have an endstop (implicit ends have already been added) it will be the last one if (nextIndex === this.tabStopMarkers.length - 1 && this.tabStopList.hasEndStop) { - const succeeded = this.goToEndOfLastTabStop() + const succeeded = this.setTabStopIndex(nextIndex) this.destroy() return succeeded } From 231e617790a654d2354c64cbbe6d7cb4994aca39 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Wed, 13 Feb 2019 22:13:27 +1000 Subject: [PATCH 42/77] Support if/else syntax --- lib/snippet-body.pegjs | 25 ++++++++++++++++---- lib/snippet.js | 2 +- lib/util.js | 50 ++++++++++++++++++++++++++------------- spec/snippets-spec.coffee | 2 +- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index c693a8a5..c3cad0f9 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -53,8 +53,7 @@ regexString = regex:(escaped / [^/])* { replace = (format / replacetext)* -// TODO: Support conditionals -format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatEscape +format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatWithIf / formatWithIfElse / formatWithElse / formatEscape simpleFormat = '$' index:int { return { backreference: makeInteger(index) } @@ -64,15 +63,31 @@ formatWithoutPlaceholder = '${' index:int '}' { return { backreference: makeInteger(index) } } -formatWithCaseTransform = '${' index:int ':' casetransform:casetransform '}' { - return { backreference: makeInteger(index), transform: casetransform } +formatWithCaseTransform = '${' index:int ':' caseTransform:caseTransform '}' { + return { backreference: makeInteger(index), transform: caseTransform } +} + +formatWithIf = '${' index:int ':+' iftext:(nonCloseBraceText / '') '}' { + return { backreference: makeInteger(index), iftext: iftext} +} + +formatWithElse = '${' index:int (':-' / ':') elsetext:(nonCloseBraceText / '') '}' { + return { backreference: makeInteger(index), elsetext: elsetext } +} + +formatWithIfElse = '${' index:int ':?' iftext:nonColonText ':' elsetext:(nonCloseBraceText / '') '}' { + return { backreference: makeInteger(index), iftext: iftext, elsetext: elsetext } +} + +nonColonText = text:('\\:' / [^:])* { + return text.join('') } formatEscape = '\\' flag:[ULulErn$] { return { escape: flag } } -casetransform = '/' type:[a-zA-Z]* { +caseTransform = '/' type:[a-zA-Z]* { return type.join('') } diff --git a/lib/snippet.js b/lib/snippet.js index 77d7cba1..78e86387 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -140,7 +140,7 @@ function stringifyVariable (node, params, acc) { // the indent and uses the "true" row and columns function stringifyText (text, params, acc) { const origLength = text.length - const replacement = '\n' + params.indent // TODO: proper line endings + const replacement = '\n' + params.indent // NOTE: Line endings normalised by default for setTextInBufferRange let rowDiff = 0 let finalOffset = 0 diff --git a/lib/util.js b/lib/util.js index b11461be..cf608ab6 100644 --- a/lib/util.js +++ b/lib/util.js @@ -72,25 +72,41 @@ function transformWithSubstitution (input, substitution) { default: ESCAPES[token.escape](flags) } - } else if (token.backreference !== undefined) { - const original = match[token.backreference] - if (token.transform) { - switch (token.transform) { - case 'upcase': - result += original.toLocaleUpperCase() - break - case 'downcase': - result += original.toLocaleLowerCase() - break - case 'capitalize': - result += original ? original[0].toLocaleUpperCase() + original.substr(1) : '' - break - default: {} // TODO: Allow custom transformation handling (important for future proofing changes in the standard) - } - } else { - result += flagTransformText(original, flags) + return + } + + if (token.backreference === undefined) { return } // NOTE: this shouldn't trigger, but can safeguard against future grammar refactors + + let original = match[token.backreference] + + if (original === undefined) { + if (token.elsetext) { + result += flagTransformText(token.elsetext, flags) + } + return + } + + if (token.iftext !== undefined) { // NOTE: Should we treat the empty string as a match? + original = token.iftext + } + + if (token.transform) { + switch (token.transform) { + case 'upcase': + result += original.toLocaleUpperCase() + break + case 'downcase': + result += original.toLocaleLowerCase() + break + case 'capitalize': + result += original ? original[0].toLocaleUpperCase() + original.substr(1) : '' + break + default: {} // TODO: Allow custom transformation handling (important for future proofing changes in the standard) } + return } + + result += flagTransformText(original, flags) }) return result diff --git a/spec/snippets-spec.coffee b/spec/snippets-spec.coffee index 053e66c8..ca28b0be 100644 --- a/spec/snippets-spec.coffee +++ b/spec/snippets-spec.coffee @@ -707,7 +707,7 @@ describe "Snippets extension", -> expect(editor.getText()).toBe("[img src][/img]") editor.undo() - expect(editor.getText()).toBe("[i][/i]") + expect(editor.getText()).toBe("[i][/i]") # Would actually expect text to be empty, because undo intervals are time based editor.redo() expect(editor.getText()).toBe("[img src][/img]") From c86b63000930021a3944c6a3a32412fa6a065bde Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 14 Feb 2019 13:54:33 +1000 Subject: [PATCH 43/77] Fix tabstop position on undo/redo --- lib/snippet-expansion.js | 21 +++++++++++++++------ lib/snippets.js | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index f482c567..37897a90 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -47,14 +47,24 @@ module.exports = class SnippetExpansion { // They're already accounted for in the history. onUndoOrRedo (isUndo) { this.isUndoingOrRedoing = true + this.isUndo = isUndo } cursorMoved ({oldBufferPosition, newBufferPosition, textChanged}) { - if (this.settingTabStop || textChanged) { return } + if (this.settingTabStop || (textChanged && !this.isUndoingOrRedoing)) { return } const itemWithCursor = this.tabStopMarkers[this.tabStopIndex].find(item => item.marker.getBufferRange().containsPoint(newBufferPosition)) if (itemWithCursor && !itemWithCursor.insertion.isTransformation()) { return } + if (this.isUndoingOrRedoing) { + if (this.isUndo) { + this.goToPreviousTabStop(false) + } else { + this.goToNextTabStop(false) + } + return + } + this.destroy() } @@ -145,8 +155,8 @@ module.exports = class SnippetExpansion { this.editor.buffer.historyProvider.createCheckpoint({markers: undefined, isBarrier: false}) // HACK: We need `isBarrier`, but the normal methods enforce false } - goToNextTabStop () { - this.insertBarrierCheckpoint() + goToNextTabStop (breakUndo=true) { + if (breakUndo) this.insertBarrierCheckpoint() const nextIndex = this.tabStopIndex + 1 // if we have an endstop (implicit ends have already been added) it will be the last one @@ -166,15 +176,14 @@ module.exports = class SnippetExpansion { return false } - goToPreviousTabStop () { + goToPreviousTabStop (breakUndo=true) { if (this.tabStopIndex > 0) { - this.insertBarrierCheckpoint() + if (breakUndo) this.insertBarrierCheckpoint() this.setTabStopIndex(this.tabStopIndex - 1) } } setTabStopIndex (tabStopIndex) { - this.insertBarrierCheckpoint() this.tabStopIndex = tabStopIndex this.settingTabStop = true let markerSelected = false diff --git a/lib/snippets.js b/lib/snippets.js index 9ad7f976..ef757f36 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -668,7 +668,7 @@ module.exports = { } }, - onUndoOrRedo (editor, isUndo) { + onUndoOrRedo (editor, event, isUndo) { const activeExpansions = this.getExpansions(editor) activeExpansions.forEach(expansion => expansion.onUndoOrRedo(isUndo)) }, From 09d866d31462c0980ef9e9dc84acf27106db9b55 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 14 Feb 2019 13:56:53 +1000 Subject: [PATCH 44/77] comment the change --- lib/snippet-expansion.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 37897a90..cf55bb14 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -56,9 +56,10 @@ module.exports = class SnippetExpansion { if (itemWithCursor && !itemWithCursor.insertion.isTransformation()) { return } + // we get here if there is no item for the current index with the cursor if (this.isUndoingOrRedoing) { if (this.isUndo) { - this.goToPreviousTabStop(false) + this.goToPreviousTabStop(false) // don't set an undo break checkpoint } else { this.goToNextTabStop(false) } From cf1441f9caffe014d7cd42e462a012845878d133 Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 14 Feb 2019 15:21:59 +1000 Subject: [PATCH 45/77] Expand and pass around resolver classes --- lib/insertion.js | 5 +- lib/{variable-resolver.js => resolvers.js} | 113 ++++++++++++++------- lib/snippet-expansion.js | 48 ++++----- lib/snippet.js | 13 ++- lib/snippets.js | 50 ++++++--- lib/tab-stop-list.js | 2 +- lib/tab-stop.js | 5 +- lib/util.js | 19 ++-- package.json | 6 +- 9 files changed, 161 insertions(+), 100 deletions(-) rename lib/{variable-resolver.js => resolvers.js} (78%) diff --git a/lib/insertion.js b/lib/insertion.js index b7d27292..f2e185f7 100644 --- a/lib/insertion.js +++ b/lib/insertion.js @@ -1,7 +1,7 @@ const { transformWithSubstitution } = require('./util') class Insertion { - constructor ({ range, substitution, choices=[] }) { + constructor ({ range, substitution, choices=[], transformResolver }) { this.range = range this.substitution = substitution if (substitution) { @@ -10,6 +10,7 @@ class Insertion { } } this.choices = choices + this.transformResolver = transformResolver } isTransformation () { @@ -21,7 +22,7 @@ class Insertion { } transform (input) { - return transformWithSubstitution(input, this.substitution) + return transformWithSubstitution(input, this.substitution, this.transformResolver) } } diff --git a/lib/variable-resolver.js b/lib/resolvers.js similarity index 78% rename from lib/variable-resolver.js rename to lib/resolvers.js index ab5d2f81..cf77a3e3 100644 --- a/lib/variable-resolver.js +++ b/lib/resolvers.js @@ -1,8 +1,54 @@ const path = require("path") -module.exports = class VariableResolver { + +class ValueResolver { + constructor (resolvers = new Map) { + this.resolvers = resolvers + } + + add (varName, resolver) { + this.resolvers.set(varName, resolver) + } + + /* + + Params depend on context. VariableResolver can expect the following, but TransformResolver will likely get a restricted number + (VariableResolver) params = { + variable: the variable name this was called with + editor: the TextEditor we are expanding in + cursor: the cursor we are expanding from + indent: the indent of the original cursor line. Automatically applied to all text (post variable transformation) + selectionRange: the original selection range of the cursor. This has been modified on the actual cursor to now select the prefix + startPosition: the cursor selection start position, after being adjusted to select the prefix + row: the row the start of the variable will be inserted on (final) <- final for snippet body creation. Does not account for changes when the user starts typing + column: the column the start of the variable will be inserted on (final; accounts for indent) + } + + (TransformResolver) params = { + input: the text to be transformed + transform: the transform this was called with + } + */ + resolve (name, params) { + const resolver = this.resolvers.get(name) + + if (resolver) { + return { + hasResolver: true, + value: resolver(params) + } + } + + return { + hasResolver: false, + value: undefined + } + } +} + +class VariableResolver extends ValueResolver { constructor (resolvers = new Map) { - this.resolvers = new Map([ + super(new Map([ ["CLIPBOARD", resolveClipboard], ["TM_SELECTED_TEXT", resolveSelected], @@ -32,42 +78,7 @@ module.exports = class VariableResolver { ["LINE_COMMENT", resolveLineComment], ...resolvers - ]) - } - - add (resolver) { - const varName = resolver.name - const resolve = resolver.resolve - if (typeof varName !== 'string' || typeof resolve !== 'function') return - this.resolvers.set(varName, resolve) - } - - - /* - params = { - editor: the TextEditor we are expanding in - cursor: the cursor we are expanding from - indent: the indent of the original cursor line. Automatically applied to all text (post variable transformation) - selectionRange: the original selection range of the cursor. This has been modified on the actual cursor to now select the prefix - startPosition: the cursor selection start position, after being adjusted to select the prefix - row: the row the start of the variable will be inserted on (final) - column: the column the start of the variable will be inserted on (final; accounts for indent) - } - */ - resolve (params) { - const resolver = this.resolvers.get(params.name) - - if (resolver) { - return { - hasResolver: true, - value: resolver(params) - } - } - - return { - hasResolver: false, - value: undefined - } + ])) } } @@ -188,3 +199,29 @@ function resolveLineComment ({editor, cursor}) { const delims = getEditorCommentStringsForPoint(editor, cursor.getBufferPosition()) return delims.line } + + +class TransformResolver extends ValueResolver { + constructor (resolvers = new Map) { + super(new Map([ + ["upcase", transformUpcase], + ["downcase", transformDowncase], + ["capitalize", transformCapitalize], + ...resolvers + ])) + } +} + +function transformUpcase ({input}) { + return input.toLocaleUpperCase() +} + +function transformDowncase ({input}) { + return input.toLocaleLowerCase() +} + +function transformCapitalize ({input}) { + return input ? input[0].toLocaleUpperCase() + input.substr(1) : '' +} + +module.exports = { ValueResolver, VariableResolver, TransformResolver } diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index cf55bb14..7569a5ac 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -1,5 +1,5 @@ -const {CompositeDisposable, Range, Point} = require('atom') -const {getEndpointOfText} = require('./util') +const { CompositeDisposable, Range, Point } = require('atom') +const { getEndpointOfText } = require('./util') module.exports = class SnippetExpansion { constructor(snippet, editor, cursor, oldSelectionRange, snippets) { @@ -31,7 +31,7 @@ module.exports = class SnippetExpansion { this.insertBarrierCheckpoint() this.editor.transact(() => { this.ignoringBufferChanges(() => { - const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) + const newRange = this.cursor.selection.insertText(body, { autoIndent: false }) if (this.tabStopList.length > 0) { this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) @@ -45,12 +45,12 @@ module.exports = class SnippetExpansion { // Set a flag on undo or redo so that we know not to re-apply transforms. // They're already accounted for in the history. - onUndoOrRedo (isUndo) { + onUndoOrRedo(isUndo) { this.isUndoingOrRedoing = true this.isUndo = isUndo } - cursorMoved ({oldBufferPosition, newBufferPosition, textChanged}) { + cursorMoved({ oldBufferPosition, newBufferPosition, textChanged }) { if (this.settingTabStop || (textChanged && !this.isUndoingOrRedoing)) { return } const itemWithCursor = this.tabStopMarkers[this.tabStopIndex].find(item => item.marker.getBufferRange().containsPoint(newBufferPosition)) @@ -69,9 +69,9 @@ module.exports = class SnippetExpansion { this.destroy() } - cursorDestroyed () { if (!this.settingTabStop) { this.destroy() } } + cursorDestroyed() { if (!this.settingTabStop) { this.destroy() } } - textChanged (event) { + textChanged(event) { if (this.isIgnoringBufferChanges) { return } // Don't try to alter the buffer if all we're doing is restoring a @@ -84,21 +84,21 @@ module.exports = class SnippetExpansion { this.applyTransformations(this.tabStopIndex) } - ignoringBufferChanges (callback) { + ignoringBufferChanges(callback) { const wasIgnoringBufferChanges = this.isIgnoringBufferChanges this.isIgnoringBufferChanges = true callback() this.isIgnoringBufferChanges = wasIgnoringBufferChanges } - applyAllTransformations () { + applyAllTransformations() { this.editor.transact(() => { this.tabStopMarkers.forEach((item, index) => this.applyTransformations(index)) }) } - applyTransformations (tabStop) { + applyTransformations(tabStop) { const items = [...this.tabStopMarkers[tabStop]] if (items.length === 0) { return } @@ -108,7 +108,7 @@ module.exports = class SnippetExpansion { this.ignoringBufferChanges(() => { for (const item of items) { - const {marker, insertion} = item + const { marker, insertion } = item var range = marker.getBufferRange() // Don't transform mirrored tab stops. They have their own cursors, so @@ -127,11 +127,11 @@ module.exports = class SnippetExpansion { }) } - placeTabStopMarkers (tabStops) { + placeTabStopMarkers(tabStops) { const markerLayer = this.getMarkerLayer(this.editor) for (const tabStop of tabStops) { - const {insertions} = tabStop + const { insertions } = tabStop const markers = [] if (!tabStop.isValid()) { continue } @@ -152,11 +152,11 @@ module.exports = class SnippetExpansion { this.applyAllTransformations() } - insertBarrierCheckpoint () { - this.editor.buffer.historyProvider.createCheckpoint({markers: undefined, isBarrier: false}) // HACK: We need `isBarrier`, but the normal methods enforce false + insertBarrierCheckpoint() { + this.editor.buffer.createCheckpoint() } - goToNextTabStop (breakUndo=true) { + goToNextTabStop(breakUndo = true) { if (breakUndo) this.insertBarrierCheckpoint() const nextIndex = this.tabStopIndex + 1 @@ -177,14 +177,14 @@ module.exports = class SnippetExpansion { return false } - goToPreviousTabStop (breakUndo=true) { + goToPreviousTabStop(breakUndo = true) { if (this.tabStopIndex > 0) { if (breakUndo) this.insertBarrierCheckpoint() this.setTabStopIndex(this.tabStopIndex - 1) } } - setTabStopIndex (tabStopIndex) { + setTabStopIndex(tabStopIndex) { this.tabStopIndex = tabStopIndex this.settingTabStop = true let markerSelected = false @@ -195,7 +195,7 @@ module.exports = class SnippetExpansion { const ranges = [] let hasTransforms = false for (const item of items) { - const {marker, insertion} = item + const { marker, insertion } = item if (marker.isDestroyed() || !marker.isValid()) { continue } if (insertion.isTransformation()) { hasTransforms = true @@ -229,11 +229,11 @@ module.exports = class SnippetExpansion { return markerSelected } - goToEndOfLastTabStop () { + goToEndOfLastTabStop() { if (this.tabStopMarkers.length === 0) { return } const items = this.tabStopMarkers[this.tabStopMarkers.length - 1] if (items.length === 0) { return } - const {marker: lastMarker} = items[items.length - 1] + const { marker: lastMarker } = items[items.length - 1] if (lastMarker.isDestroyed()) { return false } else { @@ -242,7 +242,7 @@ module.exports = class SnippetExpansion { } } - destroy () { + destroy() { this.subscriptions.dispose() this.getMarkerLayer(this.editor).clear() this.tabStopMarkers = [] @@ -251,11 +251,11 @@ module.exports = class SnippetExpansion { this.snippets.snippetChoiceProvider.deactivate() // TODO: Move to clearExpansions? } - getMarkerLayer () { + getMarkerLayer() { return this.snippets.findOrCreateMarkerLayer(this.editor) } - restore (editor) { + restore(editor) { this.editor = editor this.snippets.addExpansion(this.editor, this) } diff --git a/lib/snippet.js b/lib/snippet.js index 78e86387..6dfe68c4 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -1,9 +1,10 @@ const {Point, Range} = require('atom') const TabStopList = require('./tab-stop-list') const {transformWithSubstitution} = require('./util') +const {VariableResolver, TransformResolver} = require('./resolvers') module.exports = class Snippet { - constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, bodyText, variableResolver}) { + constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, variableResolver, transformResolver}) { this.name = name this.prefix = prefix this.description = description @@ -12,14 +13,16 @@ module.exports = class Snippet { this.leftLabel = leftLabel this.leftLabelHTML = leftLabelHTML this.bodyTree = bodyTree - this.bodyText = bodyText this.variableResolver = variableResolver + this.transformResolver = transformResolver } toString (params = {startPosition: {row: 0, column: 0}, indent: ''}) { + params.variableResolver = this.variableResolver + params.transformResolver = this.transformResolver + // accumulator to keep track of constructed text, tabstops, and position const acc = { - variableResolver: this.variableResolver, // TODO: Pass this in a more sensible way? (IDK; make all these functions methods?) tabStopList: new TabStopList(this), unknownVariables: new Map(), // name -> [range] bodyText: '', @@ -107,7 +110,7 @@ function addUnknownVariable (variableName, acc) { } function stringifyVariable (node, params, acc) { - const {hasResolver, value} = acc.variableResolver.resolve({name: node.variable, ...params, ...acc}) + const {hasResolver, value} = params.variableResolver.resolve(node.variable, {variable: node.variable, ...params, ...acc}) if (!hasResolver) { // variable unknown; convert to tabstop that goes at the end of all proper tabstops addUnknownVariable(node.variable, acc) @@ -117,7 +120,7 @@ function stringifyVariable (node, params, acc) { let resolvedValue if (node.substitution) { try { - resolvedValue = transformWithSubstitution(value || '', node.substitution) + resolvedValue = transformWithSubstitution(value || '', node.substitution, params.transformResolver) } catch (e) { atom.notifications.addError(`Failed to transform snippet variable $${segment.variable}`, {detail: e}) // TODO: add snippet location } diff --git a/lib/snippets.js b/lib/snippets.js index ef757f36..e93b3396 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -9,7 +9,7 @@ const ScopedPropertyStore = require('scoped-property-store') const Snippet = require('./snippet') const SnippetExpansion = require('./snippet-expansion') const EditorStore = require('./editor-store') -const VariableResolver = require('./variable-resolver') +const {VariableResolver, TransformResolver} = require('./resolvers') const ChoiceProvider = require('./autocomplete-choice') const {getPackageRoot} = require('./helpers') @@ -22,6 +22,7 @@ module.exports = { this.parsedSnippetsById = new Map this.editorMarkerLayers = new WeakMap this.variableResolver = new VariableResolver + this.transformResolver = new TransformResolver this.snippetChoiceProvider = new ChoiceProvider this.scopedPropertyStore = new ScopedPropertyStore @@ -423,7 +424,7 @@ module.exports = { if (snippet == null) { let {id, prefix, name, body, bodyTree, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML} = attributes if (bodyTree == null) { bodyTree = this.getBodyParser().parse(body) } - snippet = new Snippet({id, name, prefix, bodyTree, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyText: body, variableResolver: this.variableResolver}) + snippet = new Snippet({id, name, prefix, bodyTree, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyText: body, variableResolver: this.variableResolver, transformResolver: this.transformResolver}) this.parsedSnippetsById.set(attributes.id, snippet) } return snippet @@ -597,15 +598,11 @@ module.exports = { if ((activeExpansions.length === 0) || activeExpansions[0].isIgnoringBufferChanges) { return } - this.ignoringTextChangesForEditor(editor, () => - editor.transact(editor.undoGroupingInterval || 300, () => // HACK: relies on private editor property, and on one that will likely change in future (to non time based undo) + this.ignoringTextChangesForEditor(editor, () => { + let interval = editor.undoGroupingInterval === undefined ? 300 : editor.undoGroupingInterval // HACK: relies on private editor property, and on one that will likely change in future (to non time based undo) + editor.transact(interval, () => activeExpansions.map(expansion => expansion.textChanged(event))) - ) - - // NOTE: Making the checkpoint appears to have contributed to the transformation undo issues - // Create a checkpoint here to consolidate all the changes we just made into - // the transaction that prompted them. - // this.makeCheckpoint(editor) + }) }, // Perform an action inside the editor without triggering our `textChanged` @@ -633,7 +630,7 @@ module.exports = { if (cursor == null) { cursor = editor.getLastCursor() } if (typeof snippet === 'string') { const bodyTree = this.getBodyParser().parse(snippet) - snippet = new Snippet({name: '__anonymous', prefix: '', bodyTree, bodyText: snippet, variableResolver: this.variableResolver}) + snippet = new Snippet({name: '__anonymous', prefix: '', bodyTree, bodyText: snippet, variableResolver: this.variableResolver, transformResolver: this.transformResolver}) } return new SnippetExpansion(snippet, editor, cursor, oldSelectionRange, this) }, @@ -673,8 +670,35 @@ module.exports = { activeExpansions.forEach(expansion => expansion.onUndoOrRedo(isUndo)) }, - consumeVariableResolver (resolver) { - this.variableResolver.add(resolver) + consumeResolver (payload) { + if (payload === null || typeof payload !== 'object') return + + const variableResolvers = payload.variableResolvers + const transformResolvers = payload.transformResolvers + + if (variableResolvers) { + this.addResolvers(this.variableResolver, variableResolvers) + } + + if (transformResolvers) { + this.addResolvers(this.transformResolver, transformResolvers) + } + }, + + addResolvers (resolverObject, nameResolverPairs) { + let itr + if (nameResolverPairs instanceof Map) { + itr = nameResolverPairs.entries() + } else if (typeof nameResolverPairs === 'object') { + itr = Object.entries(nameResolverPairs) + } else { + return + } + + for (const [varName, resolver] of itr) { + if (typeof varName !== 'string' || typeof resolve !== 'function') continue + resolverObject.add(varName, resolver) + } }, provideAutocomplete () { diff --git a/lib/tab-stop-list.js b/lib/tab-stop-list.js index 4dfa576d..5b4496a0 100644 --- a/lib/tab-stop-list.js +++ b/lib/tab-stop-list.js @@ -16,7 +16,7 @@ class TabStopList { findOrCreate ({ index, snippet }) { if (!this.list[index]) { - this.list[index] = new TabStop({ index, snippet }) + this.list[index] = new TabStop({ index, snippet, transformResolver: this.snippet.transformResolver }) } return this.list[index] } diff --git a/lib/tab-stop.js b/lib/tab-stop.js index 32e135a3..b7e69faa 100644 --- a/lib/tab-stop.js +++ b/lib/tab-stop.js @@ -6,8 +6,9 @@ const Insertion = require('./insertion') // * has an index (one tab stop per index) // * has multiple Insertions class TabStop { - constructor ({ snippet, index, insertions }) { + constructor ({ snippet, index, insertions, transformResolver }) { this.insertions = insertions || [] + this.transformResolver = transformResolver Object.assign(this, { snippet, index }) } @@ -21,7 +22,7 @@ class TabStop { } addInsertion (insertionParams) { - let insertion = new Insertion(insertionParams) + let insertion = new Insertion({...insertionParams, transformResolver: this.transformResolver}) let insertions = this.insertions insertions.push(insertion) insertions = insertions.sort((i1, i2) => { diff --git a/lib/util.js b/lib/util.js index cf608ab6..9d458a4a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -39,7 +39,7 @@ function flagTransformText (str, flags) { return str } -function transformWithSubstitution (input, substitution) { +function transformWithSubstitution (input, substitution, transformResolver) { if (!substitution) { return input } return input.replace(substitution.find, (...match) => { @@ -91,17 +91,12 @@ function transformWithSubstitution (input, substitution) { } if (token.transform) { - switch (token.transform) { - case 'upcase': - result += original.toLocaleUpperCase() - break - case 'downcase': - result += original.toLocaleLowerCase() - break - case 'capitalize': - result += original ? original[0].toLocaleUpperCase() + original.substr(1) : '' - break - default: {} // TODO: Allow custom transformation handling (important for future proofing changes in the standard) + debugger + if (transformResolver === undefined) return + + const { hasResolver, value } = transformResolver.resolve(token.transform, {transform: token.transform, input: original}) + if (hasResolver && value) { + result += value } return } diff --git a/package.json b/package.json index 827af597..c5a8b540 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ } }, "consumedServices": { - "snippetsVariableResolver": { - "description": "Provide custom functions to resolve snippet variables.", + "snippetsResolver": { + "description": "Provide custom functions to resolve snippet variables and transforms.", "versions": { - "0.0.0": "consumeVariableResolver" + "0.0.0": "consumeResolver" } } }, From b6bbe9f4c986810a3a881c543953dbdd0567db6d Mon Sep 17 00:00:00 2001 From: Aerijo Date: Thu, 14 Feb 2019 15:22:23 +1000 Subject: [PATCH 46/77] remove debugger --- lib/util.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/util.js b/lib/util.js index 9d458a4a..c52761c2 100644 --- a/lib/util.js +++ b/lib/util.js @@ -91,7 +91,6 @@ function transformWithSubstitution (input, substitution, transformResolver) { } if (token.transform) { - debugger if (transformResolver === undefined) return const { hasResolver, value } = transformResolver.resolve(token.transform, {transform: token.transform, input: original}) From 80fb5ca08f56db464135232d0dc38392afd08ee7 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sun, 17 Feb 2019 16:30:55 +1100 Subject: [PATCH 47/77] fix typo --- lib/snippets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/snippets.js b/lib/snippets.js index e93b3396..8a6937ba 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -696,7 +696,7 @@ module.exports = { } for (const [varName, resolver] of itr) { - if (typeof varName !== 'string' || typeof resolve !== 'function') continue + if (typeof varName !== 'string' || typeof resolver !== 'function') continue resolverObject.add(varName, resolver) } }, From 3c721d77e73dd692e3192ea79fa8c61f7dae84fd Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Tue, 16 Apr 2019 19:01:06 +1000 Subject: [PATCH 48/77] implicit returns strike again --- lib/autocomplete-choice.js | 40 -------------------------------------- lib/snippet-expansion.js | 3 ++- lib/snippets.js | 7 ------- package.json | 5 ----- 4 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 lib/autocomplete-choice.js diff --git a/lib/autocomplete-choice.js b/lib/autocomplete-choice.js deleted file mode 100644 index 8cc46fbe..00000000 --- a/lib/autocomplete-choice.js +++ /dev/null @@ -1,40 +0,0 @@ -// NOTE: This provider is not currently in use. - -module.exports = class ChoiceProvider { - constructor () { - this.selector = '*' - this.inclusionPriority = -Infinity - this.suggestionPriority = 100 - this.filterSuggestions = false - this.excludeLowerPriority = false - this.active = false - this.choices = [] - } - - getSuggestions () { - // TODO: Show all initially and when no prefix, show filtered and sorted when started typing - if (!this.active) { return undefined } - return this.choices.map(c => { - return { - text: c, - type: "constant" - } - }) - } - - activate (choices) { - this.active = true - this.inclusionPriority = 1000 - this.suggestionPriority = 1000 - this.excludeLowerPriority = true - this.choices = choices - } - - deactivate () { - this.active = false - this.inclusionPriority = -Infinity - this.suggestionPriority = -Infinity - this.excludeLowerPriority = false - this.choices = [] - } -} diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 7569a5ac..f42e58ed 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -180,8 +180,9 @@ module.exports = class SnippetExpansion { goToPreviousTabStop(breakUndo = true) { if (this.tabStopIndex > 0) { if (breakUndo) this.insertBarrierCheckpoint() - this.setTabStopIndex(this.tabStopIndex - 1) + return this.setTabStopIndex(this.tabStopIndex - 1) } + return false } setTabStopIndex(tabStopIndex) { diff --git a/lib/snippets.js b/lib/snippets.js index 8a6937ba..8e83a930 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -10,7 +10,6 @@ const Snippet = require('./snippet') const SnippetExpansion = require('./snippet-expansion') const EditorStore = require('./editor-store') const {VariableResolver, TransformResolver} = require('./resolvers') -const ChoiceProvider = require('./autocomplete-choice') const {getPackageRoot} = require('./helpers') module.exports = { @@ -23,7 +22,6 @@ module.exports = { this.editorMarkerLayers = new WeakMap this.variableResolver = new VariableResolver this.transformResolver = new TransformResolver - this.snippetChoiceProvider = new ChoiceProvider this.scopedPropertyStore = new ScopedPropertyStore // The above ScopedPropertyStore will store the main registry of snippets. @@ -88,7 +86,6 @@ module.exports = { } this.emitter = null this.editorSnippetExpansions = null - this.snippetChoiceProvider.deactivate() atom.config.transact(() => this.subscriptions.dispose()) }, @@ -699,9 +696,5 @@ module.exports = { if (typeof varName !== 'string' || typeof resolver !== 'function') continue resolverObject.add(varName, resolver) } - }, - - provideAutocomplete () { - return this.snippetChoiceProvider } } diff --git a/package.json b/package.json index 61f0b55f..6d4805eb 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,6 @@ "versions": { "0.1.0": "provideSnippets" } - }, - "autocomplete.driver": { - "versions": { - "4.0.0": "provideAutocomplete" - } } }, "consumedServices": { From e7432ed7251e8221472c7421457dc91bbc37a0b1 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Tue, 16 Apr 2019 23:17:41 +1000 Subject: [PATCH 49/77] Style & logic tweaks --- lib/editor-store.js | 9 ---- lib/helpers.js | 8 ++-- lib/insertion.js | 11 ++--- lib/resolvers.js | 90 +++++++++++++++++++-------------------- lib/snippet-body.pegjs | 34 +++++++-------- lib/snippet-expansion.js | 55 ++++++++++++------------ lib/snippet.js | 32 +++++++------- lib/snippets-available.js | 8 ++-- lib/snippets.js | 15 +++---- lib/tab-stop-list.js | 24 +++++------ lib/tab-stop.js | 14 +++--- lib/util.js | 4 +- spec/insertion-spec.js | 2 +- 13 files changed, 143 insertions(+), 163 deletions(-) diff --git a/lib/editor-store.js b/lib/editor-store.js index c57cb7ad..f338a3b8 100644 --- a/lib/editor-store.js +++ b/lib/editor-store.js @@ -5,7 +5,6 @@ class EditorStore { this.editor = editor this.buffer = this.editor.getBuffer() this.observer = null - this.checkpoint = null this.expansions = [] this.existingHistoryProvider = null } @@ -52,14 +51,6 @@ class EditorStore { this.observer = null return true } - - makeCheckpoint () { - const existing = this.checkpoint - if (existing) { - this.buffer.groupChangesSinceCheckpoint(existing) - } - this.checkpoint = this.buffer.createCheckpoint() - } } EditorStore.store = new WeakMap() diff --git a/lib/helpers.js b/lib/helpers.js index 0814a3df..647b54f5 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,8 +1,6 @@ -/** @babel */ +const path = require('path') -import path from 'path' - -export function getPackageRoot() { +function getPackageRoot() { const {resourcePath} = atom.getLoadSettings() const currentFileWasRequiredFromSnapshot = !path.isAbsolute(__dirname) if (currentFileWasRequiredFromSnapshot) { @@ -11,3 +9,5 @@ export function getPackageRoot() { return path.resolve(__dirname, '..') } } + +module.exports = {getPackageRoot} diff --git a/lib/insertion.js b/lib/insertion.js index f2e185f7..b80d2ea6 100644 --- a/lib/insertion.js +++ b/lib/insertion.js @@ -1,13 +1,11 @@ -const { transformWithSubstitution } = require('./util') +const {transformWithSubstitution} = require('./util') class Insertion { - constructor ({ range, substitution, choices=[], transformResolver }) { + constructor ({range, substitution, choices=[], transformResolver}) { this.range = range this.substitution = substitution - if (substitution) { - if (substitution.replace === undefined) { - substitution.replace = '' - } + if (substitution && substitution.replace === undefined) { + substitution.replace = '' } this.choices = choices this.transformResolver = transformResolver @@ -24,7 +22,6 @@ class Insertion { transform (input) { return transformWithSubstitution(input, this.substitution, this.transformResolver) } - } module.exports = Insertion diff --git a/lib/resolvers.js b/lib/resolvers.js index cf77a3e3..4556f45d 100644 --- a/lib/resolvers.js +++ b/lib/resolvers.js @@ -1,4 +1,4 @@ -const path = require("path") +const path = require('path') class ValueResolver { @@ -11,7 +11,6 @@ class ValueResolver { } /* - Params depend on context. VariableResolver can expect the following, but TransformResolver will likely get a restricted number (VariableResolver) params = { variable: the variable name this was called with @@ -31,14 +30,12 @@ class ValueResolver { */ resolve (name, params) { const resolver = this.resolvers.get(name) - if (resolver) { return { hasResolver: true, value: resolver(params) } } - return { hasResolver: false, value: undefined @@ -49,33 +46,33 @@ class ValueResolver { class VariableResolver extends ValueResolver { constructor (resolvers = new Map) { super(new Map([ - ["CLIPBOARD", resolveClipboard], - - ["TM_SELECTED_TEXT", resolveSelected], - ["TM_CURRENT_LINE", resolveCurrentLine], - ["TM_CURRENT_WORD", resolveCurrentWord], - ["TM_LINE_INDEX", resolveLineIndex], - ["TM_LINE_NUMBER", resolveLineNumber], - ["TM_FILENAME", resolveFileName], - ["TM_FILENAME_BASE", resolveFileNameBase], - ["TM_DIRECTORY", resolveFileDirectory], - ["TM_FILEPATH", resolveFilePath], - - ["CURRENT_YEAR", resolveYear], - ["CURRENT_YEAR_SHORT", resolveYearShort], - ["CURRENT_MONTH", resolveMonth], - ["CURRENT_MONTH_NAME", resolveMonthName], - ["CURRENT_MONTH_NAME_SHORT", resolveMonthNameShort], - ["CURRENT_DATE", resolveDate], - ["CURRENT_DAY_NAME", resolveDayName], - ["CURRENT_DAY_NAME_SHORT", resolveDayNameShort], - ["CURRENT_HOUR", resolveHour], - ["CURRENT_MINUTE", resolveMinute], - ["CURRENT_SECOND", resolveSecond], - - ["BLOCK_COMMENT_START", resolveBlockCommentStart], - ["BLOCK_COMMENT_END", resolveBlockCommentEnd], - ["LINE_COMMENT", resolveLineComment], + ['CLIPBOARD', resolveClipboard], + + ['TM_SELECTED_TEXT', resolveSelected], + ['TM_CURRENT_LINE', resolveCurrentLine], + ['TM_CURRENT_WORD', resolveCurrentWord], + ['TM_LINE_INDEX', resolveLineIndex], + ['TM_LINE_NUMBER', resolveLineNumber], + ['TM_FILENAME', resolveFileName], + ['TM_FILENAME_BASE', resolveFileNameBase], + ['TM_DIRECTORY', resolveFileDirectory], + ['TM_FILEPATH', resolveFilePath], + + ['CURRENT_YEAR', resolveYear], + ['CURRENT_YEAR_SHORT', resolveYearShort], + ['CURRENT_MONTH', resolveMonth], + ['CURRENT_MONTH_NAME', resolveMonthName], + ['CURRENT_MONTH_NAME_SHORT', resolveMonthNameShort], + ['CURRENT_DATE', resolveDate], + ['CURRENT_DAY_NAME', resolveDayName], + ['CURRENT_DAY_NAME_SHORT', resolveDayNameShort], + ['CURRENT_HOUR', resolveHour], + ['CURRENT_MINUTE', resolveMinute], + ['CURRENT_SECOND', resolveSecond], + + ['BLOCK_COMMENT_START', resolveBlockCommentStart], + ['BLOCK_COMMENT_END', resolveBlockCommentEnd], + ['LINE_COMMENT', resolveLineComment], ...resolvers ])) @@ -136,53 +133,53 @@ function resolveFilePath ({editor}) { // TODO: Use correct locale function resolveYear () { - return new Date().toLocaleString('en-us', { year: 'numeric' }) + return new Date().toLocaleString('en-us', {year: 'numeric'}) } function resolveYearShort () { // last two digits of year - return new Date().toLocaleString('en-us', { year: '2-digit' }) + return new Date().toLocaleString('en-us', {year: '2-digit'}) } function resolveMonth () { - return new Date().toLocaleString('en-us', { month: '2-digit' }) + return new Date().toLocaleString('en-us', {month: '2-digit'}) } function resolveMonthName () { - return new Date().toLocaleString('en-us', { month: 'long' }) + return new Date().toLocaleString('en-us', {month: 'long'}) } function resolveMonthNameShort () { - return new Date().toLocaleString('en-us', { month: 'short' }) + return new Date().toLocaleString('en-us', {month: 'short'}) } function resolveDate () { - return new Date().toLocaleString('en-us', { day: '2-digit' }) + return new Date().toLocaleString('en-us', {day: '2-digit'}) } function resolveDayName () { - return new Date().toLocaleString('en-us', { weekday: 'long' }) + return new Date().toLocaleString('en-us', {weekday: 'long'}) } function resolveDayNameShort () { - return new Date().toLocaleString('en-us', { weekday: 'short' }) + return new Date().toLocaleString('en-us', {weekday: 'short'}) } function resolveHour () { - return new Date().toLocaleString('en-us', { hour12: false, hour: '2-digit' }) + return new Date().toLocaleString('en-us', {hour12: false, hour: '2-digit'}) } function resolveMinute () { - return new Date().toLocaleString('en-us', { minute: '2-digit' }) + return new Date().toLocaleString('en-us', {minute: '2-digit'}) } function resolveSecond () { - return new Date().toLocaleString('en-us', { second: '2-digit' }) + return new Date().toLocaleString('en-us', {second: '2-digit'}) } // TODO: wait for https://github.com/atom/atom/issues/18812 // Could make a start with what we have; one of the two should be available function getEditorCommentStringsForPoint (_editor, _point) { - return { line: '//', start: '/*', end: '*/' } + return {line: '//', start: '/*', end: '*/'} } function resolveBlockCommentStart ({editor, cursor}) { @@ -200,13 +197,12 @@ function resolveLineComment ({editor, cursor}) { return delims.line } - class TransformResolver extends ValueResolver { constructor (resolvers = new Map) { super(new Map([ - ["upcase", transformUpcase], - ["downcase", transformDowncase], - ["capitalize", transformCapitalize], + ['upcase', transformUpcase], + ['downcase', transformDowncase], + ['capitalize', transformCapitalize], ...resolvers ])) } diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index c3cad0f9..62b4d257 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -1,6 +1,6 @@ { function makeInteger(i) { - return parseInt(i.join(""), 10); + return parseInt(i.join(''), 10); } } @@ -11,15 +11,15 @@ innerBodyContent = content:(tabstop / choice / variable / nonCloseBraceText)* { tabstop = simpleTabstop / tabstopWithoutPlaceholder / tabstopWithPlaceholder / tabstopWithTransform simpleTabstop = '$' index:int { - return { index: makeInteger(index), content: [] } + return {index: makeInteger(index), content: []} } tabstopWithoutPlaceholder = '${' index:int '}' { - return { index: makeInteger(index), content: [] } + return {index: makeInteger(index), content: []} } tabstopWithPlaceholder = '${' index:int ':' content:innerBodyContent '}' { - return { index: makeInteger(index), content: content } + return {index: makeInteger(index), content: content} } tabstopWithTransform = '${' index:int substitution:transform '}' { @@ -32,7 +32,7 @@ tabstopWithTransform = '${' index:int substitution:transform '}' { choice = '${' index:int '|' choice:choicecontents '|}' { const content = choice.length > 0 ? [choice[0]] : [] - return { index: makeInteger(index), choice: choice, content: content } + return {index: makeInteger(index), choice: choice, content: content} } choicecontents = elem:choicetext rest:(',' val:choicetext { return val } )* { @@ -44,7 +44,7 @@ choicetext = choicetext:(choiceEscaped / [^|,] / barred:('|' &[^}]) { return bar } transform = '/' regex:regexString '/' replace:replace '/' flags:flags { - return { find: new RegExp(regex, flags), replace: replace } + return {find: new RegExp(regex, flags), replace: replace} } regexString = regex:(escaped / [^/])* { @@ -56,27 +56,27 @@ replace = (format / replacetext)* format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatWithIf / formatWithIfElse / formatWithElse / formatEscape simpleFormat = '$' index:int { - return { backreference: makeInteger(index) } + return {backreference: makeInteger(index)} } formatWithoutPlaceholder = '${' index:int '}' { - return { backreference: makeInteger(index) } + return {backreference: makeInteger(index)} } formatWithCaseTransform = '${' index:int ':' caseTransform:caseTransform '}' { - return { backreference: makeInteger(index), transform: caseTransform } + return {backreference: makeInteger(index), transform: caseTransform} } formatWithIf = '${' index:int ':+' iftext:(nonCloseBraceText / '') '}' { - return { backreference: makeInteger(index), iftext: iftext} + return {backreference: makeInteger(index), iftext: iftext} } formatWithElse = '${' index:int (':-' / ':') elsetext:(nonCloseBraceText / '') '}' { - return { backreference: makeInteger(index), elsetext: elsetext } + return {backreference: makeInteger(index), elsetext: elsetext} } formatWithIfElse = '${' index:int ':?' iftext:nonColonText ':' elsetext:(nonCloseBraceText / '') '}' { - return { backreference: makeInteger(index), iftext: iftext, elsetext: elsetext } + return {backreference: makeInteger(index), iftext: iftext, elsetext: elsetext} } nonColonText = text:('\\:' / [^:])* { @@ -84,7 +84,7 @@ nonColonText = text:('\\:' / [^:])* { } formatEscape = '\\' flag:[ULulErn$] { - return { escape: flag } + return {escape: flag} } caseTransform = '/' type:[a-zA-Z]* { @@ -98,19 +98,19 @@ replacetext = replacetext:(!formatEscape escaped / !format char:[^/] { return ch variable = simpleVariable / variableWithoutPlaceholder / variableWithPlaceholder / variableWithTransform simpleVariable = '$' name:variableName { - return { variable: name } + return {variable: name} } variableWithoutPlaceholder = '${' name:variableName '}' { - return { variable: name } + return {variable: name} } variableWithPlaceholder = '${' name:variableName ':' content:innerBodyContent '}' { - return { variable: name, content: content } + return {variable: name, content: content} } variableWithTransform = '${' name:variableName substitution:transform '}' { - return { variable: name, substitution: substitution } + return {variable: name, substitution: substitution} } variableName = first:[a-zA-Z_] rest:[a-zA-Z_0-9]* { diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index f42e58ed..3404b1f3 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -1,8 +1,8 @@ -const { CompositeDisposable, Range, Point } = require('atom') -const { getEndpointOfText } = require('./util') +const {CompositeDisposable, Range, Point} = require('atom') +const {getEndpointOfText} = require('./util') module.exports = class SnippetExpansion { - constructor(snippet, editor, cursor, oldSelectionRange, snippets) { + constructor (snippet, editor, cursor, oldSelectionRange, snippets) { this.settingTabStop = false this.isIgnoringBufferChanges = false this.onUndoOrRedo = this.onUndoOrRedo.bind(this) @@ -28,10 +28,10 @@ module.exports = class SnippetExpansion { this.tabStopList = tabStopList const tabStops = this.tabStopList.toArray() - this.insertBarrierCheckpoint() + this.insertCheckpoint() this.editor.transact(() => { this.ignoringBufferChanges(() => { - const newRange = this.cursor.selection.insertText(body, { autoIndent: false }) + const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) if (this.tabStopList.length > 0) { this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) @@ -45,12 +45,12 @@ module.exports = class SnippetExpansion { // Set a flag on undo or redo so that we know not to re-apply transforms. // They're already accounted for in the history. - onUndoOrRedo(isUndo) { + onUndoOrRedo (isUndo) { this.isUndoingOrRedoing = true this.isUndo = isUndo } - cursorMoved({ oldBufferPosition, newBufferPosition, textChanged }) { + cursorMoved ({oldBufferPosition, newBufferPosition, textChanged}) { if (this.settingTabStop || (textChanged && !this.isUndoingOrRedoing)) { return } const itemWithCursor = this.tabStopMarkers[this.tabStopIndex].find(item => item.marker.getBufferRange().containsPoint(newBufferPosition)) @@ -69,9 +69,9 @@ module.exports = class SnippetExpansion { this.destroy() } - cursorDestroyed() { if (!this.settingTabStop) { this.destroy() } } + cursorDestroyed () { if (!this.settingTabStop) { this.destroy() } } - textChanged(event) { + textChanged (event) { if (this.isIgnoringBufferChanges) { return } // Don't try to alter the buffer if all we're doing is restoring a @@ -84,21 +84,21 @@ module.exports = class SnippetExpansion { this.applyTransformations(this.tabStopIndex) } - ignoringBufferChanges(callback) { + ignoringBufferChanges (callback) { const wasIgnoringBufferChanges = this.isIgnoringBufferChanges this.isIgnoringBufferChanges = true callback() this.isIgnoringBufferChanges = wasIgnoringBufferChanges } - applyAllTransformations() { + applyAllTransformations () { this.editor.transact(() => { this.tabStopMarkers.forEach((item, index) => this.applyTransformations(index)) }) } - applyTransformations(tabStop) { + applyTransformations (tabStop) { const items = [...this.tabStopMarkers[tabStop]] if (items.length === 0) { return } @@ -108,7 +108,7 @@ module.exports = class SnippetExpansion { this.ignoringBufferChanges(() => { for (const item of items) { - const { marker, insertion } = item + const {marker, insertion} = item var range = marker.getBufferRange() // Don't transform mirrored tab stops. They have their own cursors, so @@ -127,11 +127,11 @@ module.exports = class SnippetExpansion { }) } - placeTabStopMarkers(tabStops) { + placeTabStopMarkers (tabStops) { const markerLayer = this.getMarkerLayer(this.editor) for (const tabStop of tabStops) { - const { insertions } = tabStop + const {insertions} = tabStop const markers = [] if (!tabStop.isValid()) { continue } @@ -152,12 +152,12 @@ module.exports = class SnippetExpansion { this.applyAllTransformations() } - insertBarrierCheckpoint() { + insertCheckpoint () { this.editor.buffer.createCheckpoint() } - goToNextTabStop(breakUndo = true) { - if (breakUndo) this.insertBarrierCheckpoint() + goToNextTabStop (breakUndo = true) { + if (breakUndo) this.insertCheckpoint() const nextIndex = this.tabStopIndex + 1 // if we have an endstop (implicit ends have already been added) it will be the last one @@ -177,15 +177,15 @@ module.exports = class SnippetExpansion { return false } - goToPreviousTabStop(breakUndo = true) { + goToPreviousTabStop (breakUndo = true) { if (this.tabStopIndex > 0) { - if (breakUndo) this.insertBarrierCheckpoint() + if (breakUndo) this.insertCheckpoint() return this.setTabStopIndex(this.tabStopIndex - 1) } return false } - setTabStopIndex(tabStopIndex) { + setTabStopIndex (tabStopIndex) { this.tabStopIndex = tabStopIndex this.settingTabStop = true let markerSelected = false @@ -196,7 +196,7 @@ module.exports = class SnippetExpansion { const ranges = [] let hasTransforms = false for (const item of items) { - const { marker, insertion } = item + const {marker, insertion} = item if (marker.isDestroyed() || !marker.isValid()) { continue } if (insertion.isTransformation()) { hasTransforms = true @@ -230,11 +230,11 @@ module.exports = class SnippetExpansion { return markerSelected } - goToEndOfLastTabStop() { + goToEndOfLastTabStop () { if (this.tabStopMarkers.length === 0) { return } const items = this.tabStopMarkers[this.tabStopMarkers.length - 1] if (items.length === 0) { return } - const { marker: lastMarker } = items[items.length - 1] + const {marker: lastMarker} = items[items.length - 1] if (lastMarker.isDestroyed()) { return false } else { @@ -243,20 +243,19 @@ module.exports = class SnippetExpansion { } } - destroy() { + destroy () { this.subscriptions.dispose() this.getMarkerLayer(this.editor).clear() this.tabStopMarkers = [] this.snippets.stopObservingEditor(this.editor) this.snippets.clearExpansions(this.editor) - this.snippets.snippetChoiceProvider.deactivate() // TODO: Move to clearExpansions? } - getMarkerLayer() { + getMarkerLayer () { return this.snippets.findOrCreateMarkerLayer(this.editor) } - restore(editor) { + restore (editor) { this.editor = editor this.snippets.addExpansion(this.editor, this) } diff --git a/lib/snippet.js b/lib/snippet.js index 6dfe68c4..fd98f516 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -4,17 +4,17 @@ const {transformWithSubstitution} = require('./util') const {VariableResolver, TransformResolver} = require('./resolvers') module.exports = class Snippet { - constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, variableResolver, transformResolver}) { - this.name = name - this.prefix = prefix - this.description = description - this.descriptionMoreURL = descriptionMoreURL - this.rightLabelHTML = rightLabelHTML - this.leftLabel = leftLabel - this.leftLabelHTML = leftLabelHTML - this.bodyTree = bodyTree - this.variableResolver = variableResolver - this.transformResolver = transformResolver + constructor(params) { + this.name = params.name + this.prefix = params.prefix + this.description = params.description + this.descriptionMoreURL = params.descriptionMoreURL + this.rightLabelHTML = params.rightLabelHTML + this.leftLabel = params.leftLabel + this.leftLabelHTML = params.leftLabelHTML + this.bodyTree = params.bodyTree + this.variableResolver = params.variableResolver + this.transformResolver = params.transformResolver } toString (params = {startPosition: {row: 0, column: 0}, indent: ''}) { @@ -34,12 +34,12 @@ module.exports = class Snippet { addTabstopsForUnknownVariables(acc.unknownVariables, acc.tabStopList) - if (!acc.tabStopList.hasEndStop && atom.config.get("snippets.implicitEndTabstop")) { + if (!acc.tabStopList.hasEndStop && atom.config.get('snippets.implicitEndTabstop')) { const endRange = new Range([acc.row, acc.column], [acc.row, acc.column]) acc.tabStopList.findOrCreate({index: Infinity, snippet: this}).addInsertion({range: endRange}) } - return { body: acc.bodyText, tabStopList: acc.tabStopList } + return {body: acc.bodyText, tabStopList: acc.tabStopList} } } @@ -54,7 +54,7 @@ function addTabstopsForUnknownVariables (unknowns, tabStopList) { } } -function stringifyContent (content=[], params, acc) { +function stringifyContent (content = [], params, acc) { for (let node of content) { if (node.index !== undefined) { // only tabstops and choices have an index if (node.choice !== undefined) { @@ -85,7 +85,7 @@ function stringifyChoice (node, params, acc) { // NOTE: will need to make sure all choices appear consistently // VS Code treats first non-simple use as the true def. So // `${1:foo} ${1|one,two|}` expands to `foo| foo|`, but reversing - // them expands to `one| one|` (with choice) + // them expands to `one| one|` (with choice) if (node.choice.length > 0) { stringifyTabstop({...node, content: [node.choice[0]]}, params, acc) } else { @@ -140,7 +140,7 @@ function stringifyVariable (node, params, acc) { } // NOTE: Unlike the original version, this also applies -// the indent and uses the "true" row and columns +// the indent and uses the 'true' row and columns function stringifyText (text, params, acc) { const origLength = text.length const replacement = '\n' + params.indent // NOTE: Line endings normalised by default for setTextInBufferRange diff --git a/lib/snippets-available.js b/lib/snippets-available.js index 659618f1..e57488ce 100644 --- a/lib/snippets-available.js +++ b/lib/snippets-available.js @@ -1,9 +1,7 @@ -/** @babel */ +const _ = require('underscore-plus') +const SelectListView = require('atom-select-list') -import _ from 'underscore-plus' -import SelectListView from 'atom-select-list' - -export default class SnippetsAvailable { +module.exports = class SnippetsAvailable { constructor (snippets) { this.panel = null this.snippets = snippets diff --git a/lib/snippets.js b/lib/snippets.js index 8e83a930..f8be6964 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -358,7 +358,7 @@ module.exports = { // prefix for expansion, but both stores have their contents exported when // the settings view asks for all available snippets. const unparsedSnippets = {} - unparsedSnippets[selector] = {"snippets": value} + unparsedSnippets[selector] = {'snippets': value} const store = isDisabled ? this.disabledSnippetsScopedPropertyStore : this.scopedPropertyStore store.addProperties(path, unparsedSnippets, {priority: this.priorityForSource(path)}) }, @@ -380,7 +380,7 @@ module.exports = { const unparsedSnippetsByPrefix = this.scopedPropertyStore.getPropertyValue( this.getScopeChain(scopeDescriptor), - "snippets" + 'snippets' ) const legacyScopeDescriptor = atom.config.getLegacyScopeDescriptorForNewScopeDescriptor @@ -390,7 +390,7 @@ module.exports = { if (legacyScopeDescriptor) { unparsedLegacySnippetsByPrefix = this.scopedPropertyStore.getPropertyValue( this.getScopeChain(legacyScopeDescriptor), - "snippets" + 'snippets' ) } @@ -512,6 +512,7 @@ module.exports = { if (prefixData) { return this.snippetForPrefix(snippets, prefixData.snippetPrefix, prefixData.wordPrefix) } + return false }, expandSnippetsUnderCursors (editor) { @@ -596,7 +597,8 @@ module.exports = { if ((activeExpansions.length === 0) || activeExpansions[0].isIgnoringBufferChanges) { return } this.ignoringTextChangesForEditor(editor, () => { - let interval = editor.undoGroupingInterval === undefined ? 300 : editor.undoGroupingInterval // HACK: relies on private editor property, and on one that will likely change in future (to non time based undo) + // HACK: relies on private editor property, and on one that will likely change in future (to non time based undo) + let interval = editor.undoGroupingInterval == undefined ? 300 : editor.undoGroupingInterval editor.transact(interval, () => activeExpansions.map(expansion => expansion.textChanged(event))) }) @@ -618,12 +620,9 @@ module.exports = { this.getStore(editor).stopObserving() }, - makeCheckpoint (editor) { - this.getStore(editor).makeCheckpoint() - }, - insert (snippet, editor, cursor, oldSelectionRange) { if (editor == null) { editor = atom.workspace.getActiveTextEditor() } + if (editor == null) { return } if (cursor == null) { cursor = editor.getLastCursor() } if (typeof snippet === 'string') { const bodyTree = this.getBodyParser().parse(snippet) diff --git a/lib/tab-stop-list.js b/lib/tab-stop-list.js index 5b4496a0..211dd2b6 100644 --- a/lib/tab-stop-list.js +++ b/lib/tab-stop-list.js @@ -14,20 +14,20 @@ class TabStopList { return !!this.list[Infinity] } - findOrCreate ({ index, snippet }) { + findOrCreate ({index, snippet}) { if (!this.list[index]) { - this.list[index] = new TabStop({ index, snippet, transformResolver: this.snippet.transformResolver }) + this.list[index] = new TabStop({index, snippet, transformResolver: this.snippet.transformResolver}) } return this.list[index] } forEachIndex (iterator) { - let indices = Object.keys(this.list).sort((a1, a2) => a1 - a2) + const indices = Object.keys(this.list).sort((a1, a2) => a1 - a2) indices.forEach(iterator) } getInsertions () { - let results = [] + const results = [] this.forEachIndex(index => { results.push(...this.list[index].insertions) }) @@ -38,19 +38,19 @@ class TabStopList { // the keys are strings... return Object.keys(this.list).reduce((m, i) => { const index = parseInt(i) - if (index > m) { // TODO: Does this handle $0? - return index - } - return m + return index > m + ? index + : m }, 0) } toArray () { - let results = [] + const results = [] this.forEachIndex(index => { - let tabStop = this.list[index] - if (!tabStop.isValid()) return - results.push(tabStop) + const tabStop = this.list[index] + if (tabStop.isValid()) { + results.push(tabStop) + } }) return results } diff --git a/lib/tab-stop.js b/lib/tab-stop.js index b7e69faa..833bbc5f 100644 --- a/lib/tab-stop.js +++ b/lib/tab-stop.js @@ -6,29 +6,29 @@ const Insertion = require('./insertion') // * has an index (one tab stop per index) // * has multiple Insertions class TabStop { - constructor ({ snippet, index, insertions, transformResolver }) { + constructor ({snippet, index, insertions, transformResolver}) { this.insertions = insertions || [] this.transformResolver = transformResolver - Object.assign(this, { snippet, index }) + Object.assign(this, {snippet, index}) } isValid () { let any = this.insertions.some(insertion => insertion.isTransformation()) if (!any) return true - let all = this.insertions.every(insertion => insertion.isTransformation()) + const all = this.insertions.every(insertion => insertion.isTransformation()) // If there are any transforming insertions, there must be at least one // non-transforming insertion to act as the primary. return !all } addInsertion (insertionParams) { - let insertion = new Insertion({...insertionParams, transformResolver: this.transformResolver}) - let insertions = this.insertions + const insertion = new Insertion({...insertionParams, transformResolver: this.transformResolver}) + const insertions = this.insertions insertions.push(insertion) - insertions = insertions.sort((i1, i2) => { + insertions.sort((i1, i2) => { return i1.range.start.compare(i2.range.start) }) - let initial = insertions.find(insertion => !insertion.isTransformation()) + const initial = insertions.find(insertion => !insertion.isTransformation()) if (initial) { insertions.splice(insertions.indexOf(initial), 1) insertions.unshift(initial) diff --git a/lib/util.js b/lib/util.js index c52761c2..dfdfc412 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,6 +1,6 @@ const {Point} = require('atom') -module.exports = { transformWithSubstitution, getEndpointOfText } +module.exports = {transformWithSubstitution, getEndpointOfText} const ESCAPES = { u: (flags) => { @@ -93,7 +93,7 @@ function transformWithSubstitution (input, substitution, transformResolver) { if (token.transform) { if (transformResolver === undefined) return - const { hasResolver, value } = transformResolver.resolve(token.transform, {transform: token.transform, input: original}) + const {hasResolver, value} = transformResolver.resolve(token.transform, {transform: token.transform, input: original}) if (hasResolver && value) { result += value } diff --git a/spec/insertion-spec.js b/spec/insertion-spec.js index 83fac925..7e6d5056 100644 --- a/spec/insertion-spec.js +++ b/spec/insertion-spec.js @@ -1,5 +1,5 @@ const Insertion = require('../lib/insertion') -const { Range } = require('atom') +const {Range} = require('atom') const range = new Range(0, 0) From dae7f48ab52036891243e642902ca305a4989d81 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Tue, 16 Apr 2019 23:32:52 +1000 Subject: [PATCH 50/77] use single quotes --- spec/snippet-loading-spec.coffee | 134 +++++++++++++++---------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/spec/snippet-loading-spec.coffee b/spec/snippet-loading-spec.coffee index c0bbf178..7165fd4e 100644 --- a/spec/snippet-loading-spec.coffee +++ b/spec/snippet-loading-spec.coffee @@ -2,7 +2,7 @@ path = require 'path' fs = require 'fs-plus' temp = require('temp').track() -describe "Snippet Loading", -> +describe 'Snippet Loading', -> [configDirPath, snippetsService] = [] beforeEach -> @@ -25,14 +25,14 @@ describe "Snippet Loading", -> activateSnippetsPackage = -> waitsForPromise -> - atom.packages.activatePackage("snippets").then ({mainModule}) -> + atom.packages.activatePackage('snippets').then ({mainModule}) -> snippetsService = mainModule.provideSnippets() mainModule.loaded = false - waitsFor "all snippets to load", 3000, -> + waitsFor 'all snippets to load', 3000, -> snippetsService.bundledSnippetsLoaded() - it "loads the bundled snippet template snippets", -> + it 'loads the bundled snippet template snippets', -> activateSnippetsPackage() runs -> @@ -50,7 +50,7 @@ describe "Snippet Loading", -> expect(csonSnippet.toString().body).toContain "'body':" expect(csonSnippet.toString().tabStopList.length).toBeGreaterThan(0) - it "loads non-hidden snippet files from atom packages with snippets directories", -> + it 'loads non-hidden snippet files from atom packages with snippets directories', -> activateSnippetsPackage() runs -> @@ -72,10 +72,10 @@ describe "Snippet Loading", -> snippet = snippetsService.snippetsForScopes(['.test'])['testhtmllabels'] expect(snippet.prefix).toBe 'testhtmllabels' expect(snippet.toString().body).toBe 'testing 456' - expect(snippet.leftLabelHTML).toBe 'Label' - expect(snippet.rightLabelHTML).toBe 'Label' + expect(snippet.leftLabelHTML).toBe 'Label' + expect(snippet.rightLabelHTML).toBe 'Label' - it "logs a warning if package snippets files cannot be parsed", -> + it 'logs a warning if package snippets files cannot be parsed', -> activateSnippetsPackage() runs -> @@ -83,7 +83,7 @@ describe "Snippet Loading", -> expect(console.warn.calls.length).toBeGreaterThan 0 expect(console.warn.mostRecentCall.args[0]).toMatch(/Error reading.*package-with-broken-snippets/) - describe "::loadPackageSnippets(callback)", -> + describe '::loadPackageSnippets(callback)', -> beforeEach -> # simulate a list of packages where the javascript core package is returned at the end atom.packages.getLoadedPackages.andReturn [ @@ -93,40 +93,40 @@ describe "Snippet Loading", -> it "allows other packages to override core packages' snippets", -> waitsForPromise -> - atom.packages.activatePackage("language-javascript") + atom.packages.activatePackage('language-javascript') activateSnippetsPackage() runs -> snippet = snippetsService.snippetsForScopes(['.source.js'])['log'] - expect(snippet.toString().body).toBe "from-a-community-package" + expect(snippet.toString().body).toBe 'from-a-community-package' - describe "::onDidLoadSnippets(callback)", -> - it "invokes listeners when all snippets are loaded", -> + describe '::onDidLoadSnippets(callback)', -> + it 'invokes listeners when all snippets are loaded', -> loadedCallback = null - waitsFor "package to activate", (done) -> - atom.packages.activatePackage("snippets").then ({mainModule}) -> + waitsFor 'package to activate', (done) -> + atom.packages.activatePackage('snippets').then ({mainModule}) -> mainModule.onDidLoadSnippets(loadedCallback = jasmine.createSpy('onDidLoadSnippets callback')) done() - waitsFor "onDidLoad callback to be called", -> loadedCallback.callCount > 0 + waitsFor 'onDidLoad callback to be called', -> loadedCallback.callCount > 0 - describe "when ~/.atom/snippets.json exists", -> + describe 'when ~/.atom/snippets.json exists', -> beforeEach -> - fs.writeFileSync path.join(configDirPath, 'snippets.json'), """ + fs.writeFileSync path.join(configDirPath, 'snippets.json'), ''' { - ".foo": { - "foo snippet": { - "prefix": "foo", - "body": "bar1" + '.foo': { + 'foo snippet': { + 'prefix': 'foo', + 'body': 'bar1' } } } - """ + ''' activateSnippetsPackage() - it "loads the snippets from that file", -> + it 'loads the snippets from that file', -> snippet = null waitsFor -> @@ -134,43 +134,43 @@ describe "Snippet Loading", -> runs -> expect(snippet.name).toBe 'foo snippet' - expect(snippet.prefix).toBe "foo" - expect(snippet.toString().body).toBe "bar1" + expect(snippet.prefix).toBe 'foo' + expect(snippet.toString().body).toBe 'bar1' - describe "when that file changes", -> - it "reloads the snippets", -> - fs.writeFileSync path.join(configDirPath, 'snippets.json'), """ + describe 'when that file changes', -> + it 'reloads the snippets', -> + fs.writeFileSync path.join(configDirPath, 'snippets.json'), ''' { - ".foo": { - "foo snippet": { - "prefix": "foo", - "body": "bar2" + '.foo': { + 'foo snippet': { + 'prefix': 'foo', + 'body': 'bar2' } } } - """ + ''' - waitsFor "snippets to be changed", -> + waitsFor 'snippets to be changed', -> snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] snippet?.toString().body is 'bar2' runs -> - fs.writeFileSync path.join(configDirPath, 'snippets.json'), "" + fs.writeFileSync path.join(configDirPath, 'snippets.json'), '' - waitsFor "snippets to be removed", -> + waitsFor 'snippets to be removed', -> not snippetsService.snippetsForScopes(['.foo'])['foo'] - describe "when ~/.atom/snippets.cson exists", -> + describe 'when ~/.atom/snippets.cson exists', -> beforeEach -> - fs.writeFileSync path.join(configDirPath, 'snippets.cson'), """ - ".foo": - "foo snippet": - "prefix": "foo" - "body": "bar1" - """ + fs.writeFileSync path.join(configDirPath, 'snippets.cson'), ''' + '.foo': + 'foo snippet': + 'prefix': 'foo' + 'body': 'bar1' + ''' activateSnippetsPackage() - it "loads the snippets from that file", -> + it 'loads the snippets from that file', -> snippet = null waitsFor -> @@ -178,33 +178,33 @@ describe "Snippet Loading", -> runs -> expect(snippet.name).toBe 'foo snippet' - expect(snippet.prefix).toBe "foo" - expect(snippet.toString().body).toBe "bar1" - - describe "when that file changes", -> - it "reloads the snippets", -> - fs.writeFileSync path.join(configDirPath, 'snippets.cson'), """ - ".foo": - "foo snippet": - "prefix": "foo" - "body": "bar2" - """ - - waitsFor "snippets to be changed", -> + expect(snippet.prefix).toBe 'foo' + expect(snippet.toString().body).toBe 'bar1' + + describe 'when that file changes', -> + it 'reloads the snippets', -> + fs.writeFileSync path.join(configDirPath, 'snippets.cson'), ''' + '.foo': + 'foo snippet': + 'prefix': 'foo' + 'body': 'bar2' + ''' + + waitsFor 'snippets to be changed', -> snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] snippet?.toString().body is 'bar2' runs -> - fs.writeFileSync path.join(configDirPath, 'snippets.cson'), "" + fs.writeFileSync path.join(configDirPath, 'snippets.cson'), '' - waitsFor "snippets to be removed", -> + waitsFor 'snippets to be removed', -> snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] not snippet? - it "notifies the user when the user snippets file cannot be loaded", -> - fs.writeFileSync path.join(configDirPath, 'snippets.cson'), """ - ".junk"::: - """ + it 'notifies the user when the user snippets file cannot be loaded', -> + fs.writeFileSync path.join(configDirPath, 'snippets.cson'), ''' + '.junk'::: + ''' activateSnippetsPackage() @@ -212,8 +212,8 @@ describe "Snippet Loading", -> expect(console.warn).toHaveBeenCalled() expect(atom.notifications.addError).toHaveBeenCalled() if atom.notifications? - describe "packages-with-snippets-disabled feature", -> - it "disables no snippets if the config option is empty", -> + describe 'packages-with-snippets-disabled feature', -> + it 'disables no snippets if the config option is empty', -> originalConfig = atom.config.get('core.packagesWithSnippetsDisabled') atom.config.set('core.packagesWithSnippetsDisabled', []) @@ -246,7 +246,7 @@ describe "Snippet Loading", -> expect(Object.keys(snippets).length).toBe 0 atom.config.set('core.packagesWithSnippetsDisabled', originalConfig) - it "unloads and/or reloads snippets from a package if the config option is changed after activation", -> + it 'unloads and/or reloads snippets from a package if the config option is changed after activation', -> originalConfig = atom.config.get('core.packagesWithSnippetsDisabled') atom.config.set('core.packagesWithSnippetsDisabled', []) From df13d064ddaeefda18bb19dd32ac3a43889c04a6 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Tue, 16 Apr 2019 23:39:26 +1000 Subject: [PATCH 51/77] but not for JSON files --- spec/snippet-loading-spec.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/snippet-loading-spec.coffee b/spec/snippet-loading-spec.coffee index 7165fd4e..c87227a3 100644 --- a/spec/snippet-loading-spec.coffee +++ b/spec/snippet-loading-spec.coffee @@ -116,10 +116,10 @@ describe 'Snippet Loading', -> beforeEach -> fs.writeFileSync path.join(configDirPath, 'snippets.json'), ''' { - '.foo': { - 'foo snippet': { - 'prefix': 'foo', - 'body': 'bar1' + ".foo": { + "foo snippet": { + "prefix": "foo", + "body": "bar1" } } } @@ -141,10 +141,10 @@ describe 'Snippet Loading', -> it 'reloads the snippets', -> fs.writeFileSync path.join(configDirPath, 'snippets.json'), ''' { - '.foo': { - 'foo snippet': { - 'prefix': 'foo', - 'body': 'bar2' + ".foo": { + "foo snippet": { + "prefix": "foo", + "body": "bar2" } } } From 0d8d6409177244d0b8d9de2aa3461ffcd12176f4 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Thu, 18 Apr 2019 00:07:47 +1000 Subject: [PATCH 52/77] translate snippet tests --- lib/snippet-history-provider.js | 3 +- lib/snippets-available.js | 2 +- spec/fixtures/test-snippets.cson | 145 +++ ...s-spec.coffee => snippets-spec-old.coffee} | 4 +- spec/snippets-spec.js | 976 ++++++++++++++++++ 5 files changed, 1125 insertions(+), 5 deletions(-) create mode 100644 spec/fixtures/test-snippets.cson rename spec/{snippets-spec.coffee => snippets-spec-old.coffee} (99%) create mode 100644 spec/snippets-spec.js diff --git a/lib/snippet-history-provider.js b/lib/snippet-history-provider.js index b1b3e57c..4bba42ff 100644 --- a/lib/snippet-history-provider.js +++ b/lib/snippet-history-provider.js @@ -1,11 +1,10 @@ function wrap (manager, callbacks) { - let klass = new SnippetHistoryProvider(manager) return new Proxy(manager, { get (target, name) { if (name in callbacks) { callbacks[name]() } - return name in klass ? klass[name] : target[name] + return target[name] } }) } diff --git a/lib/snippets-available.js b/lib/snippets-available.js index e57488ce..2d99fb61 100644 --- a/lib/snippets-available.js +++ b/lib/snippets-available.js @@ -26,7 +26,7 @@ module.exports = class SnippetsAvailable { }, didConfirmSelection: (snippet) => { for (const cursor of this.editor.getCursors()) { - this.snippets.insert(snippet.bodyText, this.editor, cursor, cursor.selection.getBufferRange()) + this.snippets.insert(snippet, this.editor, cursor, cursor.selection.getBufferRange()) } this.cancel() }, diff --git a/spec/fixtures/test-snippets.cson b/spec/fixtures/test-snippets.cson new file mode 100644 index 00000000..72bfe318 --- /dev/null +++ b/spec/fixtures/test-snippets.cson @@ -0,0 +1,145 @@ +'*': + 'without tab stops': + prefix: 't1' + body: 'this is a test' + + 'with only an end tab stop': + prefix: 't1a' + body: 'something $0 strange' + + 'overlapping prefix': + prefix: 'tt1' + body: 'this is another test' + + 'special chars': + prefix: '!@#$%^&*()-_=+[]{}54|\\;:?.,unique' + body: '@unique see' + + 'tab stops': + prefix: 't2' + body: ''' + go here next:($2) and finally go here:($0) + go here first:($1) + ''' + + 'indented second line': + prefix: 't3' + body: ''' + line 1 + \tline 2$1 + $2 + ''' + + 'multiline with indented placeholder tabstop': + prefix: 't4' + body: ''' + line ${1:1} + ${2:body...} + ''' + + 'multiline starting with tabstop': + prefix: 't4b' + body: ''' + $1 = line 1 { + line 2 + } + ''' + + 'nested tab stops': + prefix: 't5' + body: "${1:'${2:key}'}: ${3:value}" + + 'caused problems with undo': + prefix: 't6' + body: ''' + first line$1 + ${2:placeholder ending second line} + ''' + + 'contains empty lines': + prefix: 't7' + body: ''' + first line $1 + + + fourth line after blanks $2 + ''' + 'with/without placeholder': + prefix: 't8' + body: ''' + with placeholder ${1:test} + without placeholder ${2} + ''' + + 'multi-caret': + prefix: 't9' + body: ''' + with placeholder ${1:test} + without placeholder $1 + ''' + + 'multi-caret-multi-tabstop': + prefix: 't9b' + body: ''' + with placeholder ${1:test} + without placeholder $1 + second tabstop $2 + third tabstop $3 + ''' + + 'large indices': + prefix: 't10' + body: ''' + hello${10} ${11:large} indices${1} + ''' + + 'no body': + prefix: 'bad1' + + 'number body': + prefix: 'bad2' + body: 100 + + 'many tabstops': + prefix: 't11' + body: ''' + $0one${1} ${2:two} three${3} + ''' + + 'simple transform': + prefix: 't12' + body: ''' + [${1:b}][/${1/[ ]+.*$//}] + ''' + + 'transform with non-transforming mirrors': + prefix: 't13' + body: ''' + ${1:placeholder}\n${1/(.)/\\u$1/g}\n$1 + ''' + + 'multiple tab stops, some with transforms and some without': + prefix: 't14' + body: ''' + ${1:placeholder} ${1/(.)/\\u$1/g} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2 + ''' + + 'has a transformed tab stop without a corresponding ordinary tab stop': + prefix: 't15' + body: ''' + ${1/(.)/\\u$1/g} & $2 + ''' + + 'has a transformed tab stop that occurs before the corresponding ordinary tab stop': + prefix: 't16' + body: ''' + & ${1/(.)/\\u$1/g} & ${1:q} + ''' + + "has a placeholder that mirrors another tab stop's content": + prefix: 't17' + body: "$4console.${3:log}('${2:uh $1}', $1);$0" + + 'has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step': + prefix: 't18' + body: '// $1\n// ${1/./=/g}' diff --git a/spec/snippets-spec.coffee b/spec/snippets-spec-old.coffee similarity index 99% rename from spec/snippets-spec.coffee rename to spec/snippets-spec-old.coffee index 423826d6..75c6f6be 100644 --- a/spec/snippets-spec.coffee +++ b/spec/snippets-spec-old.coffee @@ -327,8 +327,8 @@ describe "Snippets extension", -> expect(editor.getSelectedBufferRange()).toEqual [[2, 14], [2, 14]] editor.insertText 'abc' - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[2, 40], [2, 40]] + # simulateTabKeyEvent() + # expect(editor.getSelectedBufferRange()).toEqual [[2, 40], [2, 40]] # tab backwards simulateTabKeyEvent(shift: true) diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js new file mode 100644 index 00000000..19d63041 --- /dev/null +++ b/spec/snippets-spec.js @@ -0,0 +1,976 @@ +const path = require('path') +const fs = require('fs') +const temp = require('temp').track() +const CSON = require('season') +const Snippets = require('../lib/snippets') +const {TextEditor} = require('atom') + +describe('Snippets extension', () => { + let editorElement + let editor + + function simulateTabKeyEvent ({shift}={}) { + const event = atom.keymaps.constructor.buildKeydownEvent('tab', {shift, target: editorElement}) + atom.keymaps.handleKeyboardEvent(event) + } + + function expandSnippetUnderCursor () { + atom.commands.dispatch(editorElement, 'snippets:expand') + } + + function gotoNextTabstop () { + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop') + } + + function gotoPreviousTabstop () { + atom.commands.dispatch(editorElement, 'snippets:previous-tab-stop') + } + + beforeEach(() => { + spyOn(Snippets, 'loadAll') + spyOn(Snippets, 'getUserSnippetsPath').andReturn('') + + waitsForPromise(() => atom.workspace.open()) // NOTE: Loading `sample.js` was a large time sink + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + waitsForPromise(() => atom.packages.activatePackage('snippets')) + + runs(() => { + editor = atom.workspace.getActiveTextEditor() + editorElement = atom.views.getView(editor) + }) + }) + + afterEach(() => { + waitsForPromise(() => atom.packages.deactivatePackage('snippets')) + }) + + describe('provideSnippets interface', () => { + let snippetsInterface = null + + beforeEach(() => { + snippetsInterface = Snippets.provideSnippets() + }) + + describe('bundledSnippetsLoaded', () => { + it('indicates the loaded state of the bundled snippets', () => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false) + Snippets.doneLoading() + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true) + }) + + it('resets the loaded state after snippets is deactivated', () => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false) + Snippets.doneLoading() + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true) + + waitsForPromise(() => atom.packages.deactivatePackage('snippets')) + waitsForPromise(() => atom.packages.activatePackage('snippets')) + + runs(() => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false) + Snippets.doneLoading() + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true) + }) + }) + }) + + describe('insertSnippet', () => { + it('can insert a snippet', () => { + editor.setText('var quicksort = function () {') + editor.setSelectedBufferRange([[0, 4], [0, 13]]) + snippetsInterface.insertSnippet("hello world", editor) + expect(editor.lineTextForBufferRow(0)).toBe("var hello world = function () {") + }) + }) + }) + + it('returns false for snippetToExpandUnderCursor if getSnippets returns {}', () => { + snippets = atom.packages.getActivePackage('snippets').mainModule + expect(snippets.snippetToExpandUnderCursor(editor)).toEqual(false) + }) + + it('ignores invalid snippets in the config', () => { + snippets = atom.packages.getActivePackage('snippets').mainModule + + invalidSnippets = null + spyOn(snippets.scopedPropertyStore, 'getPropertyValue').andCallFake(() => invalidSnippets) + expect(snippets.getSnippets(editor)).toEqual({}) + + invalidSnippets = 'test' + expect(snippets.getSnippets(editor)).toEqual({}) + + invalidSnippets = [] + expect(snippets.getSnippets(editor)).toEqual({}) + + invalidSnippets = 3 + expect(snippets.getSnippets(editor)).toEqual({}) + + invalidSnippets = {a: null} + expect(snippets.getSnippets(editor)).toEqual({}) + }) + + describe('when null snippets are present', () => { + beforeEach(() => { + Snippets.add(__filename, { + '.source.js': { + 'some snippet': { + prefix: 't1', + body: 'this is a test' + } + }, + '.source.js .nope': { + 'some snippet': { + prefix: 't1', + body: null + } + } + }) + }) + + it('overrides the less-specific defined snippet', () => { + snippets = Snippets.provideSnippets() + expect(snippets.snippetsForScopes(['.source.js'])['t1']).toBeTruthy() + expect(snippets.snippetsForScopes(['.source.js .nope.not-today'])['t1']).toBeFalsy() + }) + }) + + describe('when "tab" is triggered on the editor', () => { + const testSnippets = CSON.readFileSync(path.join(__dirname, 'fixtures', 'test-snippets.cson')) + + beforeEach(() => { + Snippets.add(__filename, testSnippets) + editor.setSoftTabs(false) // hard tabs are easier to reason with + editor.setText('') + }) + + it('parses snippets once, reusing cached ones on subsequent queries', () => { + spyOn(Snippets, 'getBodyParser').andCallThrough() + editor.setText('var quicksort = function () {') + editor.setCursorBufferPosition([0, 0]) + editor.insertText('t1') + simulateTabKeyEvent() + + expect(Snippets.getBodyParser).toHaveBeenCalled() + expect(editor.lineTextForBufferRow(0)).toBe('this is a testvar quicksort = function () {') + expect(editor.getCursorScreenPosition()).toEqual([0, 14]) + + Snippets.getBodyParser.reset() + + editor.setText('') + editor.insertText('t1') + simulateTabKeyEvent() + + expect(Snippets.getBodyParser).not.toHaveBeenCalled() + expect(editor.lineTextForBufferRow(0)).toBe('this is a test') + expect(editor.getCursorScreenPosition()).toEqual([0, 14]) + + Snippets.getBodyParser.reset() + + Snippets.add(__filename, { + '*': { + 'invalidate previous snippet': { + prefix: 't1', + body: 'new snippet' + } + } + }) + + editor.setText('') + editor.insertText('t1') + simulateTabKeyEvent() + + expect(Snippets.getBodyParser).toHaveBeenCalled() + expect(editor.lineTextForBufferRow(0)).toBe('new snippet') + expect(editor.getCursorScreenPosition()).toEqual([0, 11]) + }) + + describe('when the snippet body is invalid or missing', () => { + it('does not register the snippet', () => { + editor.insertText('bad1') + expandSnippetUnderCursor() + expect(editor.getText()).toBe('bad1') + + editor.setText('') + editor.setText('bad2') + expandSnippetUnderCursor() + expect(editor.getText()).toBe('bad2') + }) + }) + + describe('when the letters preceding the cursor trigger a snippet', () => { + describe('when the snippet contains no tab stops', () => { + it('replaces the prefix with the snippet text and places the cursor at its end', () => { + editor.setText('hello world') + editor.setCursorBufferPosition([0, 6]) + editor.insertText('t1') + expect(editor.getCursorScreenPosition()).toEqual([0, 8]) + + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('hello this is a testworld') + expect(editor.getCursorScreenPosition()).toEqual([0, 20]) + }) + + it('inserts a real tab the next time a tab is pressed after the snippet is expanded', () => { + editor.insertText('t1') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('this is a test') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('this is a test\t') + }) + }) + + describe('when the snippet contains tab stops', () => { + it('places the cursor at the first tab-stop, and moves the cursor in response to "next-tab-stop" events', () => { + markerCountBefore = editor.getMarkerCount() + editor.insertText('t2') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('go here next:() and finally go here:()') + expect(editor.lineTextForBufferRow(1)).toBe('go here first:()') + expect(editor.getSelectedBufferRange()).toEqual([[1, 15], [1, 15]]) + editor.insertText('abc') + + simulateTabKeyEvent() + expect(editor.getSelectedBufferRange()).toEqual([[0, 14], [0, 14]]) + + // tab backwards + simulateTabKeyEvent({shift: true}) + expect(editor.getSelectedBufferRange()).toEqual([[1, 15], [1, 18]]) // should highlight text typed at tab stop + + // shift-tab on first tab-stop does nothing + simulateTabKeyEvent({shift: true}) + expect(editor.getSelectedBufferRange()).toEqual([[1, 15], [1, 18]]) + + // jump to second tab-stop + simulateTabKeyEvent() + expect(editor.getSelectedBufferRange()).toEqual([[0, 14], [0, 14]]) + + // jump to end tab-stop + simulateTabKeyEvent() + expect(editor.getSelectedBufferRange()).toEqual([[0, 37], [0, 37]]) + + expect(editor.lineTextForBufferRow(0)).toBe('go here next:() and finally go here:()') + expect(editor.lineTextForBufferRow(1)).toBe('go here first:(abc)') + expect(editor.getMarkerCount()).toBe(markerCountBefore) + + // We have reached $0, so the next tab press should be an actual tab + simulateTabKeyEvent() + const firstLine = 'go here next:() and finally go here:(\t)'; + expect(editor.lineTextForBufferRow(0)).toBe(firstLine) + expect(editor.getSelectedBufferRange()).toEqual([[0, firstLine.length - 1], [0, firstLine.length - 1]]) + }) + + describe('when tab stops are nested', () => { + it('destroys the inner tab stop if the outer tab stop is modified', () => { + editor.insertText('t5') + expandSnippetUnderCursor() + expect(editor.lineTextForBufferRow(0)).toBe("'key': value") + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 5]]) + editor.insertText('foo') + simulateTabKeyEvent() + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 10]]) + }) + }) + + describe('when the only tab stop is an end stop', () => { + it('terminates the snippet immediately after moving the cursor to the end stop', () => { + editor.insertText('t1a') + simulateTabKeyEvent() + + expect(editor.lineTextForBufferRow(0)).toBe('something strange') + expect(editor.getCursorBufferPosition()).toEqual([0, 10]) + + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('something \t strange') + expect(editor.getCursorBufferPosition()).toEqual([0, 11]) + }) + }) + + describe('when tab stops are separated by blank lines', () => { + it('correctly places the tab stops (regression)', () => { + editor.insertText('t7') + expandSnippetUnderCursor() + gotoNextTabstop() + expect(editor.getCursorBufferPosition()).toEqual([3, 25]) + }) + }) + + describe('when the cursor is moved beyond the bounds of the current tab stop', () => { + it('terminates the snippet', () => { + editor.insertText('t2') + simulateTabKeyEvent() + + editor.moveUp() + editor.moveLeft() + simulateTabKeyEvent() + + expect(editor.lineTextForBufferRow(0)).toBe('go here next:(\t) and finally go here:()') + expect(editor.getCursorBufferPosition()).toEqual([0, 15]) + + // test we can terminate with shift-tab + // TODO: Not sure what this was doing / is for + }) + }) + + describe('when the cursor is moved within the bounds of the current tab stop', () => { + it('should not terminate the snippet', () => { + editor.insertText('t8') + simulateTabKeyEvent() + + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder test') + editor.moveRight() + editor.moveLeft() + editor.insertText('foo') + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder tesfoot') + + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder ') + editor.insertText('test') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder test') + editor.moveLeft() + editor.insertText('foo') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder tesfoot') + + simulateTabKeyEvent({shift: true}) + expect(editor.getSelectedBufferRange()).toEqual([[0, 17], [0, 24]]) + }) + }) + + describe('when the backspace is press within the bounds of the current tab stop', () => { + it('should not terminate the snippet', () => { + editor.insertText('t8') + simulateTabKeyEvent() + + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder test') + editor.moveRight() + editor.backspace() + editor.insertText('foo') + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder tesfoo') + + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder ') + editor.insertText('test') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder test') + editor.backspace() + editor.insertText('foo') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder tesfoo') + }) + }) + + }) + + describe('when the snippet contains hard tabs', () => { + describe('when the edit session is in soft-tabs mode', () => { + beforeEach(() => editor.setSoftTabs(true)) + + it('translates hard tabs in the snippet to the appropriate number of spaces', () => { + expect(editor.getSoftTabs()).toBeTruthy() + editor.insertText('t3') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(1)).toBe(' line 2') + expect(editor.getCursorBufferPosition()).toEqual([1, 8]) + }) + }) + + describe('when the edit session is in hard-tabs mode', () => { + beforeEach(() => editor.setSoftTabs(false)) + + it('inserts hard tabs in the snippet directly', () => { + expect(editor.getSoftTabs()).toBeFalsy() + editor.insertText('t3') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(1)).toBe("\tline 2") + expect(editor.getCursorBufferPosition()).toEqual([1, 7]) + }) + }) + }) + + describe('when the snippet prefix is indented', () => { + describe('when the snippet spans a single line', () => { + it('does not indent the next line', () => { + editor.setText('first line\n\t\nthird line') + editor.setCursorScreenPosition([1, Infinity]) + editor.insertText('t1') + expect(editor.lineTextForBufferRow(1)).toBe('\tt1') + expandSnippetUnderCursor() + expect(editor.lineTextForBufferRow(2)).toBe('third line') + }) + }) + + describe('when the snippet spans multiple lines', () => { + it('indents the subsequent lines of the snippet to be even with the start of the first line', () => { + editor.setSoftTabs(true) + const tabSpace = editor.getTabText() + editor.setText(tabSpace + 't3') + expandSnippetUnderCursor() + expect(editor.lineTextForBufferRow(0)).toBe(tabSpace + 'line 1') + expect(editor.lineTextForBufferRow(1)).toBe(tabSpace + tabSpace + 'line 2') + gotoNextTabstop() + expect(editor.getCursorBufferPosition()).toEqual([2, tabSpace.length]) + }) + }) + }) + + describe('when the snippet spans multiple lines', () => { + beforeEach(() => { + // editor.update() returns a Promise that never gets resolved, so we + // need to return undefined to avoid a timeout in the spec. + // TODO: Figure out why `editor.update({autoIndent: true})` never gets resolved. + editor.update({autoIndent: true}) + }) + + it('places tab stops correctly', () => { + editor.insertText('t3') + expandSnippetUnderCursor() + expect(editor.getCursorBufferPosition()).toEqual([1, 7]) + gotoNextTabstop() + expect(editor.getCursorBufferPosition()).toEqual([2, 0]) + }) + + it('indents the subsequent lines of the snippet based on the indent level before the snippet is inserted', () => { + editor.insertText('\tt4b') + expandSnippetUnderCursor() + + expect(editor.lineTextForBufferRow(0)).toBe('\t = line 1 {') + expect(editor.lineTextForBufferRow(1)).toBe('\t line 2') + expect(editor.lineTextForBufferRow(2)).toBe('\t}') + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('does not change the relative positioning of the tab stops when inserted multiple times', () => { + editor.insertText('t4') + expandSnippetUnderCursor() + + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 6]]) + gotoNextTabstop() + expect(editor.getSelectedBufferRange()).toEqual([[1, 2], [1, 9]]) + + editor.insertText('t4') + expandSnippetUnderCursor() + + expect(editor.getSelectedBufferRange()).toEqual([[1, 7], [1, 8]]) + gotoNextTabstop() + expect(editor.getSelectedBufferRange()).toEqual([[2, 4], [2, 11]]) // prefix was on line indented by 2 spaces + + editor.setText('') + editor.insertText('t4') + expandSnippetUnderCursor() + + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 6]]) + gotoNextTabstop() + expect(editor.getSelectedBufferRange()).toEqual([[1, 2], [1, 9]]) + }) + }) + + describe('when multiple snippets match the prefix', () => { + it('expands the snippet that is the longest match for the prefix', () => { + editor.setText('t113') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('t113\t') + expect(editor.getCursorBufferPosition()).toEqual([0, 5]) + + editor.setText('tt1') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('this is another test') + expect(editor.getCursorBufferPosition()).toEqual([0, 20]) + + editor.setText('@t1') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('@this is a test') + expect(editor.getCursorBufferPosition()).toEqual([0, 15]) + }) + }) + }) + + describe('when the word preceding the cursor ends with a snippet prefix', () => { + it('inserts a tab as normal', () => { + editor.setText('t1t1t1') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('t1t1t1\t') + }) + }) + + describe("when the letters preceding the cursor don't match a snippet", () => { + it('inserts a tab as normal', () => { + editor.setText('xxte') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('xxte\t') + expect(editor.getCursorBufferPosition()).toEqual([0, 5]) + }) + }) + + describe('when text is selected', () => { + it('inserts a tab as normal', () => { + editor.setText('t1') + editor.setSelectedBufferRange([[0, 0], [0, 2]]) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('\tt1') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when a previous snippet expansion has just been undone', () => { + it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { + editor.setText('t6\n') + editor.setCursorBufferPosition([0, 2]) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('first line') + expect(editor.lineTextForBufferRow(1)).toBe(' placeholder ending second line') + + editor.undo() + expect(editor.lineTextForBufferRow(0)).toBe('t6') + expect(editor.lineTextForBufferRow(1)).toBe('') + + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('first line') + expect(editor.lineTextForBufferRow(1)).toBe(' placeholder ending second line') + }) + }) + + describe('when the prefix contains non-word characters', () => { + it('selects the non-word characters as part of the prefix', () => { + editor.setText("!@#$%^&*()-_=+[]{}54|\\;:?.,unique") + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe("@unique see") + expect(editor.getCursorScreenPosition()).toEqual([0, 11]) + + editor.setText("'!@#$%^&*()-_=+[]{}54|\\;:?.,unique") // has ' at start (this char is not in any loaded snippet prefix) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe("'@unique see") + expect(editor.getCursorBufferPosition()).toEqual([0, 12]) + }) + + it('does not select the whitespace before the prefix', () => { + editor.setText('a; !@#$%^&*()-_=+[]{}54|\\;:?.,unique') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('a; @unique see') + expect(editor.getCursorBufferPosition()).toEqual([0, 14]) + }) + }) + + describe('when snippet contains tabstops with and without placeholder', () => { + it('should create two markers', () => { + editor.setText('t8') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder test') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder ') + expect(editor.getSelectedBufferRange()).toEqual([[0, 17], [0, 21]]) + + simulateTabKeyEvent() + expect(editor.getSelectedBufferRange()).toEqual([[1, 20], [1, 20]]) + }) + }) + + describe('when snippet contains multi-caret tabstops with and without placeholder', () => { + it('should create two markers', () => { + editor.setText('t9') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder test') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder ') + editor.insertText('hello') + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder hello') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder hello') + }) + + it('terminates the snippet when cursors are destroyed', () => { + editor.setText('t9b') + simulateTabKeyEvent() + editor.getCursors()[0].destroy() + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toEqual("with placeholder test") + expect(editor.lineTextForBufferRow(1)).toEqual("without placeholder \t") + }) + + it('terminates the snippet expansion if a new cursor moves outside the bounds of the tab stops', () => { + editor.setCursorScreenPosition([0, 0]) + editor.insertText('t9b') + simulateTabKeyEvent() + editor.insertText('test') + + editor.getCursors()[0].destroy() + editor.moveDown() // this should destroy the previous expansion + editor.moveToBeginningOfLine() + + // this should insert whitespace instead of going through tabstops of the previous destroyed snippet + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(2).indexOf("\tsecond")).toBe(0) + }) + + it('moves to the second tabstop after a multi-caret tabstop', () => { + editor.setText('t9b') + simulateTabKeyEvent() + editor.insertText('line 1') + + simulateTabKeyEvent() + editor.insertText('line 2') + + simulateTabKeyEvent() + editor.insertText('line 3') + + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder line 1') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder line 1') + expect(editor.lineTextForBufferRow(2)).toBe('second tabstop line 2') + expect(editor.lineTextForBufferRow(3)).toBe('third tabstop line 3') + }) + + it("mirrors input properly when a tabstop's placeholder refers to another tabstop", () => { + editor.setText('t17') + simulateTabKeyEvent() + editor.insertText('foo') + expect(editor.getText()).toBe("console.log('uh foo', foo);") + + simulateTabKeyEvent() + editor.insertText('bar') + expect(editor.getText()).toBe("console.log('bar', foo);") + }) + }) + + describe('when the snippet contains tab stops with transformations', () => { + it('transforms the text typed into the first tab stop before setting it in the transformed tab stop', () => { + editor.setText('t12') + simulateTabKeyEvent() + expect(editor.getText()).toBe('[b][/b]') + editor.insertText('img src') + expect(editor.getText()).toBe('[img src][/img]') + }) + + it('bundles the transform mutations along with the original manual mutation for the purposes of undo and redo', async () => { + // TODO + // editor.setText('t12') + // simulateTabKeyEvent() + // editor.insertText('i') + // expect(editor.getText()).toBe("[i][/i]") + // + // editor.insertText('mg src') + // expect(editor.getText()).toBe("[img src][/img]") + // debugger + // editor.undo() + // + // expect(editor.getText()).toBe("[b][/b]") + // // let now = 500 + // // while (now) { now--; console.log(now) } + // // expect(editor.getText()).toBe("[i][/i]") // Would actually expect text to be empty, because undo intervals are time based + // // + // // editor.redo() + // // expect(editor.getText()).toBe("[img src][/img]") + }) + + it('can pick the right insertion to use as the primary even if a transformed insertion occurs first in the snippet', () => { + editor.setText('t16') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('& Q & q') + expect(editor.getCursorBufferPosition()).toEqual([0, 7]) + + editor.insertText('rst') + expect(editor.lineTextForBufferRow(0)).toBe('& RST & rst') + }) + + it('silently ignores a tab stop without a non-transformed insertion to use as the primary', () => { + editor.setText('t15') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe(' & ') + + editor.insertText('a') + expect(editor.lineTextForBufferRow(0)).toBe(' & a') + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + }) + + describe('when the snippet contains mirrored tab stops and tab stops with transformations', () => { + it('adds cursors for the mirrors but not the transformations', () => { + editor.setText('t13') + simulateTabKeyEvent() + expect(editor.getCursors().length).toBe(2) + expect(editor.getText()).toBe('placeholder\nPLACEHOLDER\n') + + editor.insertText('foo') + expect(editor.getText()).toBe('foo\nFOO\nfoo') + }) + }) + + describe('when the snippet contains multiple tab stops, some with transformations and some without', () => { + it('does not get confused', () => { + editor.setText('t14') + simulateTabKeyEvent() + expect(editor.getCursors().length).toBe(2) + expect(editor.getText()).toBe('placeholder PLACEHOLDER ANOTHER another ') + + simulateTabKeyEvent() + expect(editor.getCursors().length).toBe(2) + + editor.insertText('FOO') + expect(editor.getText()).toBe('placeholder PLACEHOLDER FOO foo FOO') + }) + }) + + describe('when the snippet has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step', () => { + it('terminates the snippet upon such a cursor move', () => { + editor.setText('t18') + simulateTabKeyEvent() + expect(editor.getText()).toBe('// \n// ') + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.insertText('wat') + expect(editor.getText()).toBe('// wat\n// ===') + // Move the cursor down one line, then up one line. This puts the cursor + // back in its previous position, but the snippet should no longer be + // active, so when we type more text, it should not be mirrored. + editor.moveDown() + editor.moveUp() + editor.insertText('wat') + expect(editor.getText()).toBe('// watwat\n// ===') + }) + }) + + describe('when the snippet contains tab stops with an index >= 10', () => { + it('parses and orders the indices correctly', () => { + editor.setText('t10') + simulateTabKeyEvent() + expect(editor.getText()).toBe('hello large indices') + expect(editor.getCursorBufferPosition()).toEqual([0, 19]) + + simulateTabKeyEvent() + expect(editor.getCursorBufferPosition()).toEqual([0, 5]) + + simulateTabKeyEvent() + expect(editor.getSelectedBufferRange()).toEqual([[0, 6], [0, 11]]) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when the cursors share a common snippet prefix', () => { + it('expands the snippet for all cursors and allows simultaneous editing', () => { + editor.setText('t9\nt9') + editor.setCursorBufferPosition([0, 2]) + editor.addCursorAtBufferPosition([1, 2]) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder test') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder ') + expect(editor.lineTextForBufferRow(2)).toBe('with placeholder test') + expect(editor.lineTextForBufferRow(3)).toBe('without placeholder ') + + editor.insertText('hello') + expect(editor.lineTextForBufferRow(0)).toBe('with placeholder hello') + expect(editor.lineTextForBufferRow(1)).toBe('without placeholder hello') + expect(editor.lineTextForBufferRow(2)).toBe('with placeholder hello') + expect(editor.lineTextForBufferRow(3)).toBe('without placeholder hello') + }) + + it('applies transformations identically to single-expansion mode', () => { + editor.setText('t14\nt14') + editor.setCursorBufferPosition([1, 3]) + editor.addCursorAtBufferPosition([0, 3]) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('placeholder PLACEHOLDER ANOTHER another ') + expect(editor.lineTextForBufferRow(1)).toBe('placeholder PLACEHOLDER ANOTHER another ') + + editor.insertText('testing') + expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing ANOTHER another ') + expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing ANOTHER another ') + + simulateTabKeyEvent() + editor.insertText('AGAIN') + expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing AGAIN again AGAIN') + expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing AGAIN again AGAIN') + }) + + it('bundles transform-induced mutations into a single history entry along with their triggering edit, even across multiple snippets', () => { + editor.setText('t14\nt14') + editor.setCursorBufferPosition([1, 3]) + editor.addCursorAtBufferPosition([0, 3]) + simulateTabKeyEvent() + editor.insertText('testing') + simulateTabKeyEvent() + editor.insertText('AGAIN') + + // TODO + // editor.undo() + // expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing ANOTHER another ') + // expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing ANOTHER another ') + // + // editor.undo() + // expect(editor.lineTextForBufferRow(0)).toBe('placeholder PLACEHOLDER ANOTHER another ') + // expect(editor.lineTextForBufferRow(1)).toBe('placeholder PLACEHOLDER ANOTHER another ') + // + // editor.redo() + // expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing ANOTHER another ') + // expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing ANOTHER another ') + // + // editor.redo() + // expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing AGAIN again AGAIN') + // expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing AGAIN again AGAIN') + }) + }) + + describe('when there are many tabstops', () => { + it('moves the cursors between the tab stops for their corresponding snippet when tab and shift-tab are pressed', () => { + editor.setText('t11\nt11\nt11') + editor.setCursorBufferPosition([0, 3]) + editor.addCursorAtBufferPosition([1, 3]) + editor.addCursorAtBufferPosition([2, 3]) + simulateTabKeyEvent() + const cursors = editor.getCursors() + expect(cursors.length).toEqual(3) + + expect(cursors[0].getBufferPosition()).toEqual([0, 3]) + expect(cursors[1].getBufferPosition()).toEqual([1, 3]) + expect(cursors[2].getBufferPosition()).toEqual([2, 3]) + expect(cursors[0].selection.isEmpty()).toBe(true) + expect(cursors[1].selection.isEmpty()).toBe(true) + expect(cursors[2].selection.isEmpty()).toBe(true) + + simulateTabKeyEvent() + expect(cursors[0].selection.getBufferRange()).toEqual([[0, 4], [0, 7]]) + expect(cursors[1].selection.getBufferRange()).toEqual([[1, 4], [1, 7]]) + expect(cursors[2].selection.getBufferRange()).toEqual([[2, 4], [2, 7]]) + + simulateTabKeyEvent() + expect(cursors[0].getBufferPosition()).toEqual([0, 13]) + expect(cursors[1].getBufferPosition()).toEqual([1, 13]) + expect(cursors[2].getBufferPosition()).toEqual([2, 13]) + expect(cursors[0].selection.isEmpty()).toBe(true) + expect(cursors[1].selection.isEmpty()).toBe(true) + expect(cursors[2].selection.isEmpty()).toBe(true) + + // TODO + // simulateTabKeyEvent() + // expect(cursors[0].getBufferPosition()).toEqual([0, 0]) + // expect(cursors[1].getBufferPosition()).toEqual([1, 0]) + // expect(cursors[2].getBufferPosition()).toEqual([2, 0]) + // expect(cursors[0].selection.isEmpty()).toBe(true) + // expect(cursors[1].selection.isEmpty()).toBe(true) + // expect(cursors[2].selection.isEmpty()).toBe(true) + }) + }) + + describe('when the cursors do not share common snippet prefixes', () => { + it('inserts tabs as normal', () => { + editor.setText('t8\nt9') + editor.setCursorBufferPosition([0, 2]) + editor.addCursorAtBufferPosition([1, 2]) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('t8\t') + expect(editor.lineTextForBufferRow(1)).toBe('t9\t') + }) + }) + + describe('when a snippet is triggered within an existing snippet expansion', () => { + it ('ignores the snippet expansion and goes to the next tab stop', () => { + // NOTE: The snippet will actually expand if triggered by expandSnippetUnderCursor() + // So the title should be 'when a snippet is triggered with TAB', or the spec is wrong + + editor.setText('t11') + simulateTabKeyEvent() + simulateTabKeyEvent() + + editor.insertText('t1') + expect(editor.getText()).toEqual('one t1 three') + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + simulateTabKeyEvent() + expect(editor.getText()).toEqual('one t1 three') + expect(editor.getCursorBufferPosition()).toEqual([0, 12]) + }) + }) + }) + + describe('when the editor is not a pane item (regression)', () => { + it('handles tab stops correctly', () => { + editor = new TextEditor() + editorElement = editor.getElement() + + editor.insertText('t2') + simulateTabKeyEvent() + editor.insertText('ABC') + expect(editor.lineTextForBufferRow(1)).toEqual('go here first:(ABC)') + + // TODO + // editor.undo() + // editor.undo() + // expect(editor.getText()).toBe('t2') + // simulateTabKeyEvent() + // editor.insertText('ABC') + // expect(editor.getText()).toContain('go here first:(ABC)') + }) + }) + }) + + describe('when atom://.atom/snippets is opened', () => { + it('opens ~/.atom/snippets.cson', () => { + jasmine.unspy(Snippets, 'getUserSnippetsPath') + atom.workspace.destroyActivePaneItem() + const configDirPath = temp.mkdirSync('atom-config-dir-') + spyOn(atom, 'getConfigDirPath').andReturn(configDirPath) + atom.workspace.open('atom://.atom/snippets') + + waitsFor(() => atom.workspace.getActiveTextEditor()) // NOTE: CS had a trailing ? + + runs(() => { + expect(atom.workspace.getActiveTextEditor().getURI()).toBe(path.join(configDirPath, 'snippets.cson')) + }) + }) + }) + + describe('snippet insertion API', () => { + it('will automatically parse snippet definition and replace selection', () => { + editor.setText('var quicksort = function () {') + editor.setSelectedBufferRange([[0, 4], [0, 13]]) + Snippets.insert('hello ${1:world}', editor) + + expect(editor.lineTextForBufferRow(0)).toBe('var hello world = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 10], [0, 15]]) + }) + }) + + describe('when the "snippets:available" command is triggered', () => { + let availableSnippetsView = null + + beforeEach(() => { + Snippets.add(__filename, { + "*": { + "test": { + prefix: "test", + body: "${1:Test pass you will}, young " + }, + "challenge": { + prefix: "chal", + body: "$1: ${2:To pass this challenge}" + } + } + }) + + delete Snippets.availableSnippetsView + + atom.commands.dispatch(editorElement, "snippets:available") + + waitsFor(() => atom.workspace.getModalPanels().length === 1) + + runs(() => { + availableSnippetsView = atom.workspace.getModalPanels()[0].getItem() + }) + }) + + it('renders a select list of all available snippets', () => { + expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe('test') + expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe('test') + expect(availableSnippetsView.selectListView.getSelectedItem().toString().body).toBe('Test pass you will, young ') + + availableSnippetsView.selectListView.selectNext() + + expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe('chal') + expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe('challenge') + expect(availableSnippetsView.selectListView.getSelectedItem().toString().body).toBe(': To pass this challenge') + }) + + it('writes the selected snippet to the editor as snippet', () => { + availableSnippetsView.selectListView.confirmSelection() + expect(editor.getCursorBufferPosition()).toEqual([0, 18]) + expect(editor.getSelectedText()).toBe('Test pass you will') + expect(editor.getText()).toBe('Test pass you will, young ') + }) + + it('closes the dialog when triggered again', () => { + atom.commands.dispatch(availableSnippetsView.selectListView.refs.queryEditor.element, 'snippets:available') + expect(atom.workspace.getModalPanels().length).toBe(0) + }) + }) +}) From ba1bc7477b43f6d8e90e142164ae988948aa4ad5 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Thu, 18 Apr 2019 00:13:28 +1000 Subject: [PATCH 53/77] Don't load language-javascript --- spec/snippets-spec.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 19d63041..20daf4f2 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -30,8 +30,7 @@ describe('Snippets extension', () => { spyOn(Snippets, 'loadAll') spyOn(Snippets, 'getUserSnippetsPath').andReturn('') - waitsForPromise(() => atom.workspace.open()) // NOTE: Loading `sample.js` was a large time sink - waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + waitsForPromise(() => atom.workspace.open()) waitsForPromise(() => atom.packages.activatePackage('snippets')) runs(() => { @@ -926,14 +925,14 @@ describe('Snippets extension', () => { beforeEach(() => { Snippets.add(__filename, { - "*": { - "test": { - prefix: "test", - body: "${1:Test pass you will}, young " + '*': { + 'test': { + prefix: 'test', + body: '${1:Test pass you will}, young ' }, - "challenge": { - prefix: "chal", - body: "$1: ${2:To pass this challenge}" + 'challenge': { + prefix: 'chal', + body: '$1: ${2:To pass this challenge}' } } }) From cea1151f5339833a048f683c967d9f81c54e7f00 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Thu, 18 Apr 2019 23:11:58 +1000 Subject: [PATCH 54/77] vaguely better undo/redo behaviour --- lib/snippet-expansion.js | 77 ++++++++++++++++++++-------------------- lib/snippets.js | 55 ++++++++++++++++++++-------- 2 files changed, 79 insertions(+), 53 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 3404b1f3..f850ba7a 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -28,18 +28,16 @@ module.exports = class SnippetExpansion { this.tabStopList = tabStopList const tabStops = this.tabStopList.toArray() - this.insertCheckpoint() - this.editor.transact(() => { - this.ignoringBufferChanges(() => { - const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) - if (this.tabStopList.length > 0) { - this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) - this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) - this.placeTabStopMarkers(tabStops) - this.snippets.addExpansion(this.editor, this) - this.editor.normalizeTabsInBufferRange(newRange) - } - }) + this.ignoringBufferChanges(() => { + const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) + // this.editor.buffer.groupLastChanges() + if (this.tabStopList.length > 0) { + this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) + this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) + this.placeTabStopMarkers(tabStops) + this.snippets.addExpansion(this.editor, this) + this.editor.normalizeTabsInBufferRange(newRange) + } }) } @@ -59,14 +57,15 @@ module.exports = class SnippetExpansion { // we get here if there is no item for the current index with the cursor if (this.isUndoingOrRedoing) { if (this.isUndo) { - this.goToPreviousTabStop(false) // don't set an undo break checkpoint + this.goToPreviousTabStop() } else { - this.goToNextTabStop(false) + this.goToNextTabStop() } return } - this.destroy() + console.log('cursor out of bounds') + this.destroy(true) } cursorDestroyed () { if (!this.settingTabStop) { this.destroy() } } @@ -92,10 +91,7 @@ module.exports = class SnippetExpansion { } applyAllTransformations () { - this.editor.transact(() => { - this.tabStopMarkers.forEach((item, index) => - this.applyTransformations(index)) - }) + this.tabStopMarkers.forEach((item, index) => this.applyTransformations(index)) } applyTransformations (tabStop) { @@ -118,6 +114,8 @@ module.exports = class SnippetExpansion { var outputText = insertion.transform(inputText) this.editor.setTextInBufferRange(range, outputText) + // this.editor.buffer.groupLastChanges() + const newRange = new Range( range.start, range.start.traverse(getEndpointOfText(outputText)) @@ -152,37 +150,38 @@ module.exports = class SnippetExpansion { this.applyAllTransformations() } - insertCheckpoint () { - this.editor.buffer.createCheckpoint() - } - goToNextTabStop (breakUndo = true) { - if (breakUndo) this.insertCheckpoint() const nextIndex = this.tabStopIndex + 1 // if we have an endstop (implicit ends have already been added) it will be the last one if (nextIndex === this.tabStopMarkers.length - 1 && this.tabStopList.hasEndStop) { - const succeeded = this.setTabStopIndex(nextIndex) - this.destroy() - return succeeded + return { + succeeded: this.setTabStopIndex(nextIndex), + end: true + } } // we are not at the end, and the next is not the endstop; just go to next stop if (nextIndex < this.tabStopMarkers.length) { - return this.setTabStopIndex(nextIndex) || this.goToNextTabStop() + return { + succeeded: this.setTabStopIndex(nextIndex) || this.goToNextTabStop(), + end: false + } } // we have just tabbed past the final tabstop; silently clean up, and let an actual tab be inserted this.destroy() - return false + return {succeeded: false, end: true} } - goToPreviousTabStop (breakUndo = true) { + goToPreviousTabStop () { if (this.tabStopIndex > 0) { - if (breakUndo) this.insertCheckpoint() - return this.setTabStopIndex(this.tabStopIndex - 1) + return { + succeeded: this.setTabStopIndex(this.tabStopIndex - 1), + end: false + } } - return false + return {succeeded: false, end: true} } setTabStopIndex (tabStopIndex) { @@ -243,12 +242,14 @@ module.exports = class SnippetExpansion { } } - destroy () { + destroy (all = false) { this.subscriptions.dispose() - this.getMarkerLayer(this.editor).clear() - this.tabStopMarkers = [] - this.snippets.stopObservingEditor(this.editor) - this.snippets.clearExpansions(this.editor) + if (all) { + this.getMarkerLayer(this.editor).clear() + this.tabStopMarkers = [] + this.snippets.stopObservingEditor(this.editor) + this.snippets.clearExpansions(this.editor) + } } getMarkerLayer () { diff --git a/lib/snippets.js b/lib/snippets.js index f8be6964..d7ceba68 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -52,10 +52,12 @@ module.exports = { this.subscriptions.add(atom.commands.add('atom-text-editor', { 'snippets:expand'(event) { + console.log('expanding snippet') const editor = this.getModel() - if (snippets.snippetToExpandUnderCursor(editor)) { + const snippet = snippets.snippetToExpandUnderCursor(editor) + if (snippet) { snippets.clearExpansions(editor) - snippets.expandSnippetsUnderCursors(editor) + snippets.expandSnippetsUnderCursors(editor, snippet) } else { event.abortKeyBinding() } @@ -515,8 +517,11 @@ module.exports = { return false }, - expandSnippetsUnderCursors (editor) { - const snippet = this.snippetToExpandUnderCursor(editor) + expandSnippetsUnderCursors (editor, snippet) { + if (!snippet) { + snippet = this.snippetToExpandUnderCursor(editor) + } + if (!snippet) { return false } this.getStore(editor).observeHistory({ @@ -525,37 +530,56 @@ module.exports = { }) this.findOrCreateMarkerLayer(editor) - const cursors = editor.getCursors() - const checkpoint = editor.createCheckpoint() + const cursors = editor.getCursors() for (const cursor of cursors) { const cursorPosition = cursor.getBufferPosition() - const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0]) + const startPoint = cursorPosition.translate([0, -snippet.prefix.length]) const oldSelectionRange = cursor.selection.getBufferRange() cursor.selection.setBufferRange([startPoint, cursorPosition]) this.insert(snippet, editor, cursor, oldSelectionRange) } - editor.groupChangesSinceCheckpoint(checkpoint, {deleteCheckpoint: true}) + editor.groupChangesSinceCheckpoint(checkpoint) return true }, goToNextTabStop (editor) { let nextTabStopVisited = false + let isEnd = true for (const expansion of this.getExpansions(editor)) { - if (expansion && expansion.goToNextTabStop()) { - nextTabStopVisited = true - } + if (!expansion) { continue } + const {succeeded, end} = expansion.goToNextTabStop() + if (isEnd) { isEnd = end } + if (succeeded) { nextTabStopVisited = true } + } + if (isEnd) { + this.findOrCreateMarkerLayer(editor).clear() + this.stopObservingEditor(editor) + this.clearExpansions(editor) + } + if (nextTabStopVisited) { + editor.buffer.createCheckpoint() } return nextTabStopVisited }, goToPreviousTabStop (editor) { let previousTabStopVisited = false + let isEnd = true for (const expansion of this.getExpansions(editor)) { - if (expansion && expansion.goToPreviousTabStop()) { - previousTabStopVisited = true - } + if (!expansion) { continue; } + const {succeeded, end} = expansion.goToPreviousTabStop(!previousTabStopVisited) + if (isEnd) { isEnd = end } + if (succeeded) { previousTabStopVisited = true } + } + if (isEnd) { + this.findOrCreateMarkerLayer(editor).clear() + this.stopObservingEditor(editor) + this.clearExpansions(editor) + } + if (previousTabStopVisited) { + editor.buffer.createCheckpoint() } return previousTabStopVisited }, @@ -568,6 +592,7 @@ module.exports = { let layer = this.editorMarkerLayers.get(editor) if (layer === undefined) { layer = editor.addMarkerLayer({maintainHistory: true}) + console.log(layer.id, ) this.editorMarkerLayers.set(editor, layer) } return layer @@ -598,7 +623,7 @@ module.exports = { this.ignoringTextChangesForEditor(editor, () => { // HACK: relies on private editor property, and on one that will likely change in future (to non time based undo) - let interval = editor.undoGroupingInterval == undefined ? 300 : editor.undoGroupingInterval + const interval = editor.undoGroupingInterval == undefined ? 300 : editor.undoGroupingInterval editor.transact(interval, () => activeExpansions.map(expansion => expansion.textChanged(event))) }) From c77cf00d628f95fa429a2a514c74a3cc4c1906cb Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Fri, 19 Apr 2019 11:17:30 +1000 Subject: [PATCH 55/77] fix --- lib/snippet-expansion.js | 12 ++++++------ lib/snippets.js | 16 ++++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index f850ba7a..e51d6574 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -150,7 +150,8 @@ module.exports = class SnippetExpansion { this.applyAllTransformations() } - goToNextTabStop (breakUndo = true) { + goToNextTabStop () { + debugger const nextIndex = this.tabStopIndex + 1 // if we have an endstop (implicit ends have already been added) it will be the last one @@ -163,10 +164,9 @@ module.exports = class SnippetExpansion { // we are not at the end, and the next is not the endstop; just go to next stop if (nextIndex < this.tabStopMarkers.length) { - return { - succeeded: this.setTabStopIndex(nextIndex) || this.goToNextTabStop(), - end: false - } + const succeeded = this.setTabStopIndex(nextIndex) + if (succeeded) { return {succeeded, end: false} } + return this.goToNextTabStop() } // we have just tabbed past the final tabstop; silently clean up, and let an actual tab be inserted @@ -244,9 +244,9 @@ module.exports = class SnippetExpansion { destroy (all = false) { this.subscriptions.dispose() + this.tabStopMarkers = [] if (all) { this.getMarkerLayer(this.editor).clear() - this.tabStopMarkers = [] this.snippets.stopObservingEditor(this.editor) this.snippets.clearExpansions(this.editor) } diff --git a/lib/snippets.js b/lib/snippets.js index d7ceba68..ca6ce4b8 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -545,6 +545,7 @@ module.exports = { }, goToNextTabStop (editor) { + debugger let nextTabStopVisited = false let isEnd = true for (const expansion of this.getExpansions(editor)) { @@ -561,6 +562,9 @@ module.exports = { if (nextTabStopVisited) { editor.buffer.createCheckpoint() } + + console.log('end:', isEnd, 'succ:', nextTabStopVisited) + return nextTabStopVisited }, @@ -570,14 +574,14 @@ module.exports = { for (const expansion of this.getExpansions(editor)) { if (!expansion) { continue; } const {succeeded, end} = expansion.goToPreviousTabStop(!previousTabStopVisited) - if (isEnd) { isEnd = end } + // if (isEnd) { isEnd = end } if (succeeded) { previousTabStopVisited = true } } - if (isEnd) { - this.findOrCreateMarkerLayer(editor).clear() - this.stopObservingEditor(editor) - this.clearExpansions(editor) - } + // if (isEnd) { + // this.findOrCreateMarkerLayer(editor).clear() + // this.stopObservingEditor(editor) + // this.clearExpansions(editor) + // } if (previousTabStopVisited) { editor.buffer.createCheckpoint() } From 01db36041f7b782980801acc18da39b71481d982 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Fri, 19 Apr 2019 11:23:06 +1000 Subject: [PATCH 56/77] Remove debugger & tweak gotoPreviousStop --- lib/snippet-expansion.js | 3 +-- lib/snippets.js | 1 - spec/snippets-spec.js | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index e51d6574..f0467f26 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -151,7 +151,6 @@ module.exports = class SnippetExpansion { } goToNextTabStop () { - debugger const nextIndex = this.tabStopIndex + 1 // if we have an endstop (implicit ends have already been added) it will be the last one @@ -181,7 +180,7 @@ module.exports = class SnippetExpansion { end: false } } - return {succeeded: false, end: true} + return {succeeded: true, end: false} } setTabStopIndex (tabStopIndex) { diff --git a/lib/snippets.js b/lib/snippets.js index ca6ce4b8..dc82528f 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -545,7 +545,6 @@ module.exports = { }, goToNextTabStop (editor) { - debugger let nextTabStopVisited = false let isEnd = true for (const expansion of this.getExpansions(editor)) { diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 20daf4f2..3cb2cb20 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -641,7 +641,6 @@ describe('Snippets extension', () => { // // editor.insertText('mg src') // expect(editor.getText()).toBe("[img src][/img]") - // debugger // editor.undo() // // expect(editor.getText()).toBe("[b][/b]") From ac06db58adcd9cfb02d144424580ba76aae8c337 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Fri, 19 Apr 2019 17:23:25 +1000 Subject: [PATCH 57/77] bug fixes --- lib/snippet-body.pegjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index 62b4d257..ffe11579 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -83,7 +83,7 @@ nonColonText = text:('\\:' / [^:])* { return text.join('') } -formatEscape = '\\' flag:[ULulErn$] { +formatEscape = '\\' flag:[ULulErn] { return {escape: flag} } @@ -91,7 +91,7 @@ caseTransform = '/' type:[a-zA-Z]* { return type.join('') } -replacetext = replacetext:(!formatEscape escaped / !format char:[^/] { return char })+ { +replacetext = replacetext:(!formatEscape char:escaped { return char } / !format char:[^/] { return char })+ { return replacetext.join('') } From b847db11095c445dbe23012d19ae2e1e0fd1c53d Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Fri, 19 Apr 2019 20:23:33 +1000 Subject: [PATCH 58/77] Specs and bug fixes --- lib/snippet-body.pegjs | 14 +- ...pec.coffee => body-parser-spec-old.coffee} | 0 spec/body-parser-spec.js | 338 ++++++ spec/snippets-spec-old.coffee | 1039 ----------------- 4 files changed, 347 insertions(+), 1044 deletions(-) rename spec/{body-parser-spec.coffee => body-parser-spec-old.coffee} (100%) create mode 100644 spec/body-parser-spec.js delete mode 100644 spec/snippets-spec-old.coffee diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index ffe11579..04b5e5b1 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -67,20 +67,20 @@ formatWithCaseTransform = '${' index:int ':' caseTransform:caseTransform '}' { return {backreference: makeInteger(index), transform: caseTransform} } -formatWithIf = '${' index:int ':+' iftext:(nonCloseBraceText / '') '}' { +formatWithIf = '${' index:int ':+' iftext:(ifElseText / '') '}' { return {backreference: makeInteger(index), iftext: iftext} } -formatWithElse = '${' index:int (':-' / ':') elsetext:(nonCloseBraceText / '') '}' { +formatWithElse = '${' index:int (':-' / ':') elsetext:(ifElseText / '') '}' { return {backreference: makeInteger(index), elsetext: elsetext} } -formatWithIfElse = '${' index:int ':?' iftext:nonColonText ':' elsetext:(nonCloseBraceText / '') '}' { +formatWithIfElse = '${' index:int ':?' iftext:nonColonText ':' elsetext:(ifElseText / '') '}' { return {backreference: makeInteger(index), iftext: iftext, elsetext: elsetext} } -nonColonText = text:('\\:' / [^:])* { - return text.join('') +nonColonText = text:('\\:' { return ':' } / escaped / [^:])* { + return text.join('') } formatEscape = '\\' flag:[ULulErn] { @@ -154,3 +154,7 @@ text = text:(escaped / !tabstop !variable !choice char:. { return char })+ { nonCloseBraceText = text:(escaped / !tabstop !variable !choice char:[^}] { return char })+ { return text.join('') } + +ifElseText = text:(escaped / char:[^}] { return char })+ { + return text.join('') +} diff --git a/spec/body-parser-spec.coffee b/spec/body-parser-spec-old.coffee similarity index 100% rename from spec/body-parser-spec.coffee rename to spec/body-parser-spec-old.coffee diff --git a/spec/body-parser-spec.js b/spec/body-parser-spec.js new file mode 100644 index 00000000..3b8ef568 --- /dev/null +++ b/spec/body-parser-spec.js @@ -0,0 +1,338 @@ +const BodyParser = require('../lib/snippet-body-parser') + +function expectMatch(input, tree) { + expect(BodyParser.parse(input)).toEqual(tree) +} + +describe('Snippet Body Parser', () => { + it('parses a snippet with no special behaviour', () => { + expectMatch('${} $ n $}1} ${/upcase/} \n world ${||}', [ + '${} $ n $}1} ${/upcase/} \n world ${||}' + ]) + }) + + describe('for snippets with tabstops', () => { + it('parses simple tabstops', () => { + expectMatch('hello$1world$2', [ + 'hello', + {index: 1, content: []}, + 'world', + {index: 2, content: []} + ]) + }) + + it('parses verbose tabstops', () => { + expectMatch('hello${1}world${2}', [ + 'hello', + {index: 1, content: []}, + 'world', + {index: 2, content: []} + ]) + }) + + it('skips escaped tabstops', () => { + expectMatch('$1 \\$2 $3 \\\\$4 \\\\\\$5 $6', [ + {index: 1, content: []}, + ' $2 ', + {index: 3, content: []}, + ' \\', + {index: 4, content: []}, + ' \\$5 ', + {index: 6, content: []} + ]) + }) + + describe('for tabstops with placeholders', () => { + it('parses them', () => { + expectMatch('hello${1:placeholder}world', [ + 'hello', + {index: 1, content: ['placeholder']}, + 'world' + ]) + }) + + it('allows escaped back braces', () => { + expectMatch('${1:{}}', [ + {index: 1, content: ['{']}, + '}' + ]) + expectMatch('${1:{\\}}', [ + {index: 1, content: ['{}']} + ]) + }) + }) + + it('parses tabstops with transforms', () => { + expectMatch('${1/.*/$0/}', [ + { + index: 1, + content: [], + substitution: { + find: /.*/, + replace: [{backreference: 0}] + } + } + ]) + }) + + it('parses tabstops with choices', () => { + expectMatch('${1|on}e,t\\|wo,th\\,ree|}', [ + {index: 1, content: ['on}e'], choice: ['on}e', 't|wo', 'th,ree']} + ]) + }) + + it('parses nested tabstops', () => { + expectMatch('${1:place${2:hol${3:der}}}', [ + { + index: 1, + content: [ + 'place', + {index: 2, content: [ + 'hol', + {index: 3, content: ['der']} + ]} + ] + } + ]) + }) + }) + + describe('for snippets with variables', () => { + it('parses simple variables', () => { + expectMatch('$foo', [{variable: 'foo'}]) + expectMatch('$FOO', [{variable: 'FOO'}]) + }) + + it('parses verbose variables', () => { + expectMatch('${foo}', [{variable: 'foo'}]) + expectMatch('${FOO}', [{variable: 'FOO'}]) + }) + + it('parses variables with placeholders', () => { + expectMatch('${f:placeholder}', [{variable: 'f', content: ['placeholder']}]) + expectMatch('${f:foo$1 $VAR}', [ + { + variable: 'f', + content: [ + 'foo', + {index: 1, content: []}, + ' ', + {variable: 'VAR'} + ] + } + ]) + }) + + it('parses variables with transforms', () => { + expectMatch('${f/.*/$0/}', [ + { + variable: 'f', + substitution: { + find: /.*/, + replace: [ + {backreference: 0} + ] + } + } + ]) + }) + }) + + describe('for escaped characters', () => { + it('treats a selection of escaped characters specially', () => { + expectMatch('\\$ \\\\ \\}', [ + '$ \\ }' + ]) + }) + + it('returns the literal slash and character otherwise', () => { + expectMatch('\\ \\. \\# \\n \\r \\', [ + '\\ \\. \\# \\n \\r \\' + ]) + }) + }) + + describe('for transforms', () => { + it('allows an empty transform', () => { + expectMatch('${a///}', [ + { + variable: 'a', + substitution: { + find: new RegExp(), + replace: [] + } + } + ]) + }) + + it('appends the declared flags', () => { + expectMatch('${a/.//g}', [ + { + variable: 'a', + substitution: { + find: /./g, + replace: [] + } + } + ]) + expectMatch('${a/.//gimuy}', [ // s flag not available apparently + { + variable: 'a', + substitution: { + find: /./gimuy, + replace: [] + } + } + ]) + // NOTE: We do not try to filter out invalid flags. This + // helps protect against future flag changes, such as when + // 's' is introduced + }) + + it('allows searching with an escaped forwards slash', () => { + expectMatch('${a/^\\/5/bar/}', [ + { + variable: 'a', + substitution: { + find: /^\/5/, + replace: ['bar'] + } + } + ]) + }) + + it('allows an escaped back brace, removing the backslash', () => { + expectMatch('${a/^\\}5//}', [ + { + variable: 'a', + substitution: { + find: /^}5/, + replace: [] + } + } + ]) + }) + + it('supports worded transformations', () => { + expectMatch('${a/./foo${0:/Bar}/}', [ + { + variable: 'a', + substitution: { + find: /./, + replace: [ + 'foo', + { + backreference: 0, + transform: 'Bar' + } + ] + } + } + ]) + }) + + it('supports flag transformations', () => { + expectMatch('${a/./foo\\ubar\\n\\r\\U\\L\\l\\E\\$0/}', [ + { + variable: 'a', + substitution: { + find: /./, + replace: [ + 'foo', + {escape: 'u'}, + 'bar', + {escape: 'n'}, + {escape: 'r'}, + {escape: 'U'}, + {escape: 'L'}, + {escape: 'l'}, + {escape: 'E'}, + '$0' + ] + } + } + ]) + }) + + it('treats invalid flag transforms as literal', () => { + expectMatch('${a/./foo\\p5/}', [ + { + variable: 'a', + substitution: { + find: /./, + replace: [ + 'foo\\p5' + ] + } + } + ]) + }) + + it('supports if replacements', () => { + // NOTE: the '+' cannot be escaped. If you want it to be part of + // a placeholder (else only), use ':-' + expectMatch('${a/./${1:+foo$0bar\\}baz}/}', [ + { + variable: 'a', + substitution: { + find: /./, + replace: [ + { + backreference: 1, + iftext: 'foo$0bar}baz' + } + ] + } + } + ]) + + expectMatch('${a/./${1:-foo$0bar\\}baz}/}', [ + { + variable: 'a', + substitution: { + find: /./, + replace: [ + { + backreference: 1, + elsetext: 'foo$0bar}baz' + } + ] + } + } + ]) + + expectMatch('${a/./${1:foo$0bar\\}baz}/}', [ + { + variable: 'a', + substitution: { + find: /./, + replace: [ + { + backreference: 1, + elsetext: 'foo$0bar}baz' + } + ] + } + } + ]) + + // NOTE: colon can be escaped in if text, but not in else text as it is + // unnecessary + expectMatch('${a/./${1:?foo$0bar\\}baz\\:hux\\\\:foo$0bar\\}baz\\:hux\\\\}/}', [ + { + variable: 'a', + substitution: { + find: /./, + replace: [ + { + backreference: 1, + iftext: 'foo$0bar}baz:hux\\', + elsetext: 'foo$0bar}baz\\:hux\\' + } + ] + } + } + ]) + }) + }) +}) diff --git a/spec/snippets-spec-old.coffee b/spec/snippets-spec-old.coffee deleted file mode 100644 index 75c6f6be..00000000 --- a/spec/snippets-spec-old.coffee +++ /dev/null @@ -1,1039 +0,0 @@ -path = require 'path' -temp = require('temp').track() -Snippets = require '../lib/snippets' -{TextEditor} = require 'atom' - -describe "Snippets extension", -> - [editorElement, editor] = [] - - simulateTabKeyEvent = ({shift}={}) -> - event = atom.keymaps.constructor.buildKeydownEvent('tab', {shift, target: editorElement}) - atom.keymaps.handleKeyboardEvent(event) - - beforeEach -> - spyOn(Snippets, 'loadAll') - spyOn(Snippets, 'getUserSnippetsPath').andReturn('') - - waitsForPromise -> - atom.workspace.open('sample.js') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - atom.packages.activatePackage('snippets') - - runs -> - editor = atom.workspace.getActiveTextEditor() - editorElement = atom.views.getView(editor) - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackage('snippets') - - describe "provideSnippets interface", -> - snippetsInterface = null - - beforeEach -> - snippetsInterface = Snippets.provideSnippets() - - describe "bundledSnippetsLoaded", -> - it "indicates the loaded state of the bundled snippets", -> - expect(snippetsInterface.bundledSnippetsLoaded()).toBe false - Snippets.doneLoading() - expect(snippetsInterface.bundledSnippetsLoaded()).toBe true - - it "resets the loaded state after snippets is deactivated", -> - expect(snippetsInterface.bundledSnippetsLoaded()).toBe false - Snippets.doneLoading() - expect(snippetsInterface.bundledSnippetsLoaded()).toBe true - - waitsForPromise -> atom.packages.deactivatePackage('snippets') - waitsForPromise -> atom.packages.activatePackage('snippets') - - runs -> - expect(snippetsInterface.bundledSnippetsLoaded()).toBe false - Snippets.doneLoading() - expect(snippetsInterface.bundledSnippetsLoaded()).toBe true - - describe "insertSnippet", -> - it "can insert a snippet", -> - editor.setSelectedBufferRange([[0, 4], [0, 13]]) - snippetsInterface.insertSnippet("hello ${1:world}", editor) - expect(editor.lineTextForBufferRow(0)).toBe "var hello world = function () {" - - it "returns false for snippetToExpandUnderCursor if getSnippets returns {}", -> - snippets = atom.packages.getActivePackage('snippets').mainModule - expect(snippets.snippetToExpandUnderCursor(editor)).toEqual false - - it "ignores invalid snippets in the config", -> - snippets = atom.packages.getActivePackage('snippets').mainModule - - invalidSnippets = null - spyOn(snippets.scopedPropertyStore, 'getPropertyValue').andCallFake -> invalidSnippets - expect(snippets.getSnippets(editor)).toEqual {} - - invalidSnippets = 'test' - expect(snippets.getSnippets(editor)).toEqual {} - - invalidSnippets = [] - expect(snippets.getSnippets(editor)).toEqual {} - - invalidSnippets = 3 - expect(snippets.getSnippets(editor)).toEqual {} - - invalidSnippets = {a: null} - expect(snippets.getSnippets(editor)).toEqual {} - - describe "when null snippets are present", -> - beforeEach -> - Snippets.add __filename, - '.source.js': - "some snippet": - prefix: "t1" - body: "this is a test" - - '.source.js .nope': - "some snippet": - prefix: "t1" - body: null - - it "overrides the less-specific defined snippet", -> - snippets = Snippets.provideSnippets() - expect(snippets.snippetsForScopes(['.source.js'])['t1']).toBeTruthy() - expect(snippets.snippetsForScopes(['.source.js .nope.not-today'])['t1']).toBeFalsy() - - describe "when 'tab' is triggered on the editor", -> - beforeEach -> - Snippets.add __filename, - ".source.js": - "without tab stops": - prefix: "t1" - body: "this is a test" - - "with only an end tab stop": - prefix: "t1a" - body: "something $0 strange" - - "overlapping prefix": - prefix: "tt1" - body: "this is another test" - - "special chars": - prefix: "@unique" - body: "@unique see" - - "tab stops": - prefix: "t2" - body: """ - go here next:($2) and finally go here:($0) - go here first:($1) - - """ - - "indented second line": - prefix: "t3" - body: """ - line 1 - \tline 2$1 - $2 - """ - - "multiline with indented placeholder tabstop": - prefix: "t4" - body: """ - line ${1:1} - ${2:body...} - """ - - "multiline starting with tabstop": - prefix: "t4b" - body: """ - $1 = line 1 { - line 2 - } - """ - - "nested tab stops": - prefix: "t5" - body: '${1:"${2:key}"}: ${3:value}' - - "caused problems with undo": - prefix: "t6" - body: """ - first line$1 - ${2:placeholder ending second line} - """ - - "contains empty lines": - prefix: "t7" - body: """ - first line $1 - - - fourth line after blanks $2 - """ - "with/without placeholder": - prefix: "t8" - body: """ - with placeholder ${1:test} - without placeholder ${2} - """ - - "multi-caret": - prefix: "t9" - body: """ - with placeholder ${1:test} - without placeholder $1 - """ - - "multi-caret-multi-tabstop": - prefix: "t9b" - body: """ - with placeholder ${1:test} - without placeholder $1 - second tabstop $2 - third tabstop $3 - """ - - "large indices": - prefix: "t10" - body: """ - hello${10} ${11:large} indices${1} - """ - - "no body": - prefix: "bad1" - - "number body": - prefix: "bad2" - body: 100 - - "many tabstops": - prefix: "t11" - body: """ - $0one${1} ${2:two} three${3} - """ - - "simple transform": - prefix: "t12" - body: """ - [${1:b}][/${1/[ ]+.*$//}] - """ - "transform with non-transforming mirrors": - prefix: "t13" - body: """ - ${1:placeholder}\n${1/(.)/\\u$1/g}\n$1 - """ - "multiple tab stops, some with transforms and some without": - prefix: "t14" - body: """ - ${1:placeholder} ${1/(.)/\\u$1/g} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2 - """ - "has a transformed tab stop without a corresponding ordinary tab stop": - prefix: 't15' - body: """ - ${1/(.)/\\u$1/g} & $2 - """ - "has a transformed tab stop that occurs before the corresponding ordinary tab stop": - prefix: 't16' - body: """ - & ${1/(.)/\\u$1/g} & ${1:q} - """ - "has a placeholder that mirrors another tab stop's content": - prefix: 't17' - body: "$4console.${3:log}('${2:uh $1}', $1);$0" - "has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step": - prefix: 't18' - body: '// $1\n// ${1/./=/g}' - - it "parses snippets once, reusing cached ones on subsequent queries", -> - spyOn(Snippets, "getBodyParser").andCallThrough() - - editor.insertText("t1") - simulateTabKeyEvent() - - expect(Snippets.getBodyParser).toHaveBeenCalled() - expect(editor.lineTextForBufferRow(0)).toBe "this is a testvar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 14] - - Snippets.getBodyParser.reset() - - editor.setText("") - editor.insertText("t1") - simulateTabKeyEvent() - - expect(Snippets.getBodyParser).not.toHaveBeenCalled() - expect(editor.lineTextForBufferRow(0)).toBe "this is a test" - expect(editor.getCursorScreenPosition()).toEqual [0, 14] - - Snippets.getBodyParser.reset() - - Snippets.add __filename, - ".source.js": - "invalidate previous snippet": - prefix: "t1" - body: "new snippet" - - editor.setText("") - editor.insertText("t1") - simulateTabKeyEvent() - - expect(Snippets.getBodyParser).toHaveBeenCalled() - expect(editor.lineTextForBufferRow(0)).toBe "new snippet" - expect(editor.getCursorScreenPosition()).toEqual [0, 11] - - describe "when the snippet body is invalid or missing", -> - it "does not register the snippet", -> - editor.setText('') - editor.insertText('bad1') - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.getText()).toBe 'bad1' - - editor.setText('') - editor.setText('bad2') - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.getText()).toBe 'bad2' - - describe "when the letters preceding the cursor trigger a snippet", -> - describe "when the snippet contains no tab stops", -> - it "replaces the prefix with the snippet text and places the cursor at its end", -> - editor.insertText("t1") - expect(editor.getCursorScreenPosition()).toEqual [0, 2] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "this is a testvar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 14] - - it "inserts a real tab the next time a tab is pressed after the snippet is expanded", -> - editor.insertText("t1") - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "this is a testvar quicksort = function () {" - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "this is a test var quicksort = function () {" - - describe "when the snippet contains tab stops", -> - it "places the cursor at the first tab-stop, and moves the cursor in response to 'next-tab-stop' events", -> - markerCountBefore = editor.getMarkerCount() - editor.setCursorScreenPosition([2, 0]) - editor.insertText('t2') - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(2)).toBe "go here next:() and finally go here:()" - expect(editor.lineTextForBufferRow(3)).toBe "go here first:()" - expect(editor.lineTextForBufferRow(4)).toBe " if (items.length <= 1) return items;" - expect(editor.getSelectedBufferRange()).toEqual [[3, 15], [3, 15]] - - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[2, 14], [2, 14]] - editor.insertText 'abc' - - # simulateTabKeyEvent() - # expect(editor.getSelectedBufferRange()).toEqual [[2, 40], [2, 40]] - - # tab backwards - simulateTabKeyEvent(shift: true) - expect(editor.getSelectedBufferRange()).toEqual [[2, 14], [2, 17]] # should highlight text typed at tab stop - - simulateTabKeyEvent(shift: true) - expect(editor.getSelectedBufferRange()).toEqual [[3, 15], [3, 15]] - - # shift-tab on first tab-stop does nothing - simulateTabKeyEvent(shift: true) - expect(editor.getCursorScreenPosition()).toEqual [3, 15] - - # tab through all tab stops, then tab on last stop to terminate snippet - simulateTabKeyEvent() - simulateTabKeyEvent() - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(2)).toBe "go here next:(abc) and finally go here:( )" - expect(editor.getMarkerCount()).toBe markerCountBefore - - describe "when tab stops are nested", -> - it "destroys the inner tab stop if the outer tab stop is modified", -> - editor.setText('') - editor.insertText 't5' - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.lineTextForBufferRow(0)).toBe '"key": value' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 5]] - editor.insertText("foo") - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 10]] - - describe "when the only tab stop is an end stop", -> - it "terminates the snippet immediately after moving the cursor to the end stop", -> - editor.setText('') - editor.insertText 't1a' - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "something strange" - expect(editor.getCursorBufferPosition()).toEqual [0, 10] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "something strange" - expect(editor.getCursorBufferPosition()).toEqual [0, 12] - - describe "when tab stops are separated by blank lines", -> - it "correctly places the tab stops (regression)", -> - editor.setText('') - editor.insertText 't7' - atom.commands.dispatch editorElement, 'snippets:expand' - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getCursorBufferPosition()).toEqual [3, 25] - - describe "when the cursor is moved beyond the bounds of the current tab stop", -> - it "terminates the snippet", -> - editor.setCursorScreenPosition([2, 0]) - editor.insertText('t2') - simulateTabKeyEvent() - - editor.moveUp() - editor.moveLeft() - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(2)).toBe "go here next:( ) and finally go here:()" - expect(editor.getCursorBufferPosition()).toEqual [2, 16] - - # test we can terminate with shift-tab - editor.setCursorScreenPosition([4, 0]) - editor.insertText('t2') - simulateTabKeyEvent() - simulateTabKeyEvent() - - editor.moveRight() - simulateTabKeyEvent(shift: true) - expect(editor.getCursorBufferPosition()).toEqual [4, 15] - - describe "when the cursor is moved within the bounds of the current tab stop", -> - it "should not terminate the snippet", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t8') - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - editor.moveRight() - editor.moveLeft() - editor.insertText("foo") - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder tesfoot" - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - editor.insertText("test") - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder testvar quicksort = function () {" - editor.moveLeft() - editor.insertText("foo") - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder tesfootvar quicksort = function () {" - - describe "when the backspace is press within the bounds of the current tab stop", -> - it "should not terminate the snippet", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t8') - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - editor.moveRight() - editor.backspace() - editor.insertText("foo") - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder tesfoo" - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - editor.insertText("test") - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder testvar quicksort = function () {" - editor.backspace() - editor.insertText("foo") - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder tesfoovar quicksort = function () {" - - describe "when the snippet contains hard tabs", -> - describe "when the edit session is in soft-tabs mode", -> - it "translates hard tabs in the snippet to the appropriate number of spaces", -> - expect(editor.getSoftTabs()).toBeTruthy() - editor.insertText("t3") - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(1)).toBe " line 2" - expect(editor.getCursorBufferPosition()).toEqual [1, 8] - - describe "when the edit session is in hard-tabs mode", -> - it "inserts hard tabs in the snippet directly", -> - editor.setSoftTabs(false) - editor.insertText("t3") - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(1)).toBe "\tline 2" - expect(editor.getCursorBufferPosition()).toEqual [1, 7] - - describe "when the snippet prefix is indented", -> - describe "when the snippet spans a single line", -> - it "does not indent the next line", -> - editor.setCursorScreenPosition([2, Infinity]) - editor.insertText ' t1' - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when the snippet spans multiple lines", -> - it "indents the subsequent lines of the snippet to be even with the start of the first line", -> - expect(editor.getSoftTabs()).toBeTruthy() - editor.setCursorScreenPosition([2, Infinity]) - editor.insertText ' t3' - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items; line 1" - expect(editor.lineTextForBufferRow(3)).toBe " line 2" - expect(editor.getCursorBufferPosition()).toEqual [3, 12] - - describe "when the snippet spans multiple lines", -> - beforeEach -> - editor.update({autoIndent: true}) - # editor.update() returns a Promise that never gets resolved, so we - # need to return undefined to avoid a timeout in the spec. - # TODO: Figure out why `editor.update({autoIndent: true})` never gets resolved. - return - - it "places tab stops correctly", -> - expect(editor.getSoftTabs()).toBeTruthy() - editor.setCursorScreenPosition([2, Infinity]) - editor.insertText ' t3' - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.getCursorBufferPosition()).toEqual [3, 12] - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getCursorBufferPosition()).toEqual [4, 4] - - it "indents the subsequent lines of the snippet based on the indent level before the snippet is inserted", -> - editor.setCursorScreenPosition([2, Infinity]) - editor.insertNewline() - editor.insertText 't4b' - atom.commands.dispatch editorElement, 'snippets:expand' - - expect(editor.lineTextForBufferRow(3)).toBe " = line 1 {" # 4 + 1 spaces (because the tab stop is invisible) - expect(editor.lineTextForBufferRow(4)).toBe " line 2" - expect(editor.lineTextForBufferRow(5)).toBe " }" - expect(editor.getCursorBufferPosition()).toEqual [3, 4] - - it "does not change the relative positioning of the tab stops when inserted multiple times", -> - editor.setCursorScreenPosition([2, Infinity]) - editor.insertNewline() - editor.insertText 't4' - atom.commands.dispatch editorElement, 'snippets:expand' - - expect(editor.getSelectedBufferRange()).toEqual [[3, 9], [3, 10]] - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getSelectedBufferRange()).toEqual [[4, 6], [4, 13]] - - editor.insertText 't4' - atom.commands.dispatch editorElement, 'snippets:expand' - - expect(editor.getSelectedBufferRange()).toEqual [[4, 11], [4, 12]] - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getSelectedBufferRange()).toEqual [[5, 8], [5, 15]] - - editor.setText('') # Clear editor - editor.insertText 't4' - atom.commands.dispatch editorElement, 'snippets:expand' - - expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 6]] - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getSelectedBufferRange()).toEqual [[1, 2], [1, 9]] - - describe "when multiple snippets match the prefix", -> - it "expands the snippet that is the longest match for the prefix", -> - editor.insertText('t113') - expect(editor.getCursorScreenPosition()).toEqual [0, 4] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "t113 var quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 6] - - editor.undo() - editor.undo() - - editor.insertText("tt1") - expect(editor.getCursorScreenPosition()).toEqual [0, 3] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "this is another testvar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 20] - - editor.undo() - editor.undo() - - editor.insertText("@t1") - expect(editor.getCursorScreenPosition()).toEqual [0, 3] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "@this is a testvar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 15] - - describe "when the word preceding the cursor ends with a snippet prefix", -> - it "inserts a tab as normal", -> - editor.insertText("t1t1t1") - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "t1t1t1 var quicksort = function () {" - - describe "when the letters preceding the cursor don't match a snippet", -> - it "inserts a tab as normal", -> - editor.insertText("xxte") - expect(editor.getCursorScreenPosition()).toEqual [0, 4] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "xxte var quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 6] - - describe "when text is selected", -> - it "inserts a tab as normal", -> - editor.insertText("t1") - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe " t1var quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 4]] - - describe "when a previous snippet expansion has just been undone", -> - it "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", -> - editor.insertText 't6\n' - editor.setCursorBufferPosition [0, 2] - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "first line" - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe "t6" - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "first line" - - describe "when the prefix contains non-word characters", -> - it "selects the non-word characters as part of the prefix", -> - editor.insertText("@unique") - expect(editor.getCursorScreenPosition()).toEqual [0, 7] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "@unique seevar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 11] - - editor.setCursorBufferPosition [10, 0] - editor.insertText("'@unique") - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(10)).toBe "'@unique see" - expect(editor.getCursorScreenPosition()).toEqual [10, 12] - - it "does not select the whitespace before the prefix", -> - editor.insertText("a; @unique") - expect(editor.getCursorScreenPosition()).toEqual [0, 10] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "a; @unique seevar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 14] - - describe "when snippet contains tabstops with or without placeholder", -> - it "should create two markers", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t8') - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - - expect(editor.getSelectedBufferRange()).toEqual [[0, 17], [0, 21]] - - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[1, 20], [1, 20]] - - describe "when snippet contains multi-caret tabstops with or without placeholder", -> - it "should create two markers", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t9') - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - editor.insertText('hello') - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder hello" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder hellovar quicksort = function () {" - - it "terminates the snippet when cursors are destroyed", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t9b') - simulateTabKeyEvent() - editor.getCursors()[0].destroy() - editor.getCursorBufferPosition() - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(1)).toEqual("without placeholder ") - - it "terminates the snippet expansion if a new cursor moves outside the bounds of the tab stops", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t9b') - simulateTabKeyEvent() - editor.insertText('test') - - editor.getCursors()[0].destroy() - editor.moveDown() # this should destroy the previous expansion - editor.moveToBeginningOfLine() - - # this should insert whitespace instead of going through tabstops of the previous destroyed snippet - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(2).indexOf(" second")).toBe 0 - - it "moves to the second tabstop after a multi-caret tabstop", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t9b') - simulateTabKeyEvent() - editor.insertText('line 1') - - simulateTabKeyEvent() - editor.insertText('line 2') - - simulateTabKeyEvent() - editor.insertText('line 3') - - expect(editor.lineTextForBufferRow(2).indexOf("line 2 ")).toBe -1 - - it "mirrors input properly when a tabstop's placeholder refers to another tabstop", -> - editor.setText('t17') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - editor.insertText("foo") - expect(editor.getText()).toBe "console.log('uh foo', foo);" - simulateTabKeyEvent() - editor.insertText("bar") - expect(editor.getText()).toBe "console.log('bar', foo);" - - describe "when the snippet contains tab stops with transformations", -> - it "transforms the text typed into the first tab stop before setting it in the transformed tab stop", -> - editor.setText('t12') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getText()).toBe("[b][/b]") - editor.insertText('img src') - expect(editor.getText()).toBe("[img src][/img]") - - it "bundles the transform mutations along with the original manual mutation for the purposes of undo and redo", -> - editor.setText('t12') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - editor.insertText('i') - expect(editor.getText()).toBe("[i][/i]") - - editor.insertText('mg src') - expect(editor.getText()).toBe("[img src][/img]") - - editor.undo() - expect(editor.getText()).toBe("[i][/i]") # Would actually expect text to be empty, because undo intervals are time based - - editor.redo() - expect(editor.getText()).toBe("[img src][/img]") - - it "can pick the right insertion to use as the primary even if a transformed insertion occurs first in the snippet", -> - editor.setText('t16') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe("& Q & q") - expect(editor.getCursorBufferPosition()).toEqual([0, 7]) - - editor.insertText('rst') - expect(editor.lineTextForBufferRow(0)).toBe("& RST & rst") - - it "silently ignores a tab stop without a non-transformed insertion to use as the primary", -> - editor.setText('t15') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - editor.insertText('a') - expect(editor.lineTextForBufferRow(0)).toBe(" & a") - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe "when the snippet contains mirrored tab stops and tab stops with transformations", -> - it "adds cursors for the mirrors but not the transformations", -> - editor.setText('t13') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getCursors().length).toBe(2) - expect(editor.getText()).toBe """ - placeholder - PLACEHOLDER - - """ - - editor.insertText('foo') - - expect(editor.getText()).toBe """ - foo - FOO - foo - """ - - describe "when the snippet contains multiple tab stops, some with transformations and some without", -> - it "does not get confused", -> - editor.setText('t14') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getCursors().length).toBe(2) - expect(editor.getText()).toBe "placeholder PLACEHOLDER ANOTHER another " - simulateTabKeyEvent() - expect(editor.getCursors().length).toBe(2) - editor.insertText('FOO') - expect(editor.getText()).toBe """ - placeholder PLACEHOLDER FOO foo FOO - """ - - describe "when the snippet has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step", -> - it "terminates the snippet upon such a cursor move", -> - editor.setText('t18') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getText()).toBe("// \n// ") - expect(editor.getCursorBufferPosition()).toEqual [0, 3] - editor.insertText('wat') - expect(editor.getText()).toBe("// wat\n// ===") - # Move the cursor down one line, then up one line. This puts the cursor - # back in its previous position, but the snippet should no longer be - # active, so when we type more text, it should not be mirrored. - editor.setCursorScreenPosition([1, 6]) - editor.setCursorScreenPosition([0, 6]) - editor.insertText('wat') - expect(editor.getText()).toBe("// watwat\n// ===") - - - describe "when the snippet contains tab stops with an index >= 10", -> - it "parses and orders the indices correctly", -> - editor.setText('t10') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getText()).toBe "hello large indices" - expect(editor.getCursorBufferPosition()).toEqual [0, 19] - simulateTabKeyEvent() - expect(editor.getCursorBufferPosition()).toEqual [0, 5] - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[0, 6], [0, 11]] - - describe "when there are multiple cursors", -> - describe "when the cursors share a common snippet prefix", -> - it "expands the snippet for all cursors and allows simultaneous editing", -> - editor.insertText('t9') - editor.setCursorBufferPosition([12, 2]) - editor.insertText(' t9') - editor.addCursorAtBufferPosition([0, 2]) - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - expect(editor.lineTextForBufferRow(13)).toBe "}; with placeholder test" - expect(editor.lineTextForBufferRow(14)).toBe "without placeholder " - - editor.insertText('hello') - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder hello" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder hellovar quicksort = function () {" - expect(editor.lineTextForBufferRow(13)).toBe "}; with placeholder hello" - expect(editor.lineTextForBufferRow(14)).toBe "without placeholder hello" - - it "applies transformations identically to single-expansion mode", -> - editor.setText('t14\nt14') - editor.setCursorBufferPosition([1, 3]) - editor.addCursorAtBufferPosition([0, 3]) - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "placeholder PLACEHOLDER ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "placeholder PLACEHOLDER ANOTHER another " - - editor.insertText "testing" - - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing ANOTHER another " - - simulateTabKeyEvent() - editor.insertText "AGAIN" - - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing AGAIN again AGAIN" - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing AGAIN again AGAIN" - - it "bundles transform-induced mutations into a single history entry along with their triggering edit, even across multiple snippets", -> - editor.setText('t14\nt14') - editor.setCursorBufferPosition([1, 3]) - editor.addCursorAtBufferPosition([0, 3]) - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "placeholder PLACEHOLDER ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "placeholder PLACEHOLDER ANOTHER another " - - editor.insertText "testing" - - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing ANOTHER another " - - simulateTabKeyEvent() - editor.insertText "AGAIN" - - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing AGAIN again AGAIN" - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing AGAIN again AGAIN" - - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing ANOTHER another " - - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe "placeholder PLACEHOLDER ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "placeholder PLACEHOLDER ANOTHER another " - - editor.redo() - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing ANOTHER another " - - editor.redo() - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing AGAIN again AGAIN" - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing AGAIN again AGAIN" - - describe "when there are many tabstops", -> - it "moves the cursors between the tab stops for their corresponding snippet when tab and shift-tab are pressed", -> - editor.addCursorAtBufferPosition([7, 5]) - editor.addCursorAtBufferPosition([12, 2]) - editor.insertText('t11') - simulateTabKeyEvent() - - cursors = editor.getCursors() - expect(cursors.length).toEqual 3 - - expect(cursors[0].getBufferPosition()).toEqual [0, 3] - expect(cursors[1].getBufferPosition()).toEqual [7, 8] - expect(cursors[2].getBufferPosition()).toEqual [12, 5] - expect(cursors[0].selection.isEmpty()).toBe true - expect(cursors[1].selection.isEmpty()).toBe true - expect(cursors[2].selection.isEmpty()).toBe true - - simulateTabKeyEvent() - expect(cursors[0].getBufferPosition()).toEqual [0, 7] - expect(cursors[1].getBufferPosition()).toEqual [7, 12] - expect(cursors[2].getBufferPosition()).toEqual [12, 9] - expect(cursors[0].selection.isEmpty()).toBe false - expect(cursors[1].selection.isEmpty()).toBe false - expect(cursors[2].selection.isEmpty()).toBe false - expect(cursors[0].selection.getText()).toEqual 'two' - expect(cursors[1].selection.getText()).toEqual 'two' - expect(cursors[2].selection.getText()).toEqual 'two' - - simulateTabKeyEvent() - expect(cursors[0].getBufferPosition()).toEqual [0, 13] - expect(cursors[1].getBufferPosition()).toEqual [7, 18] - expect(cursors[2].getBufferPosition()).toEqual [12, 15] - expect(cursors[0].selection.isEmpty()).toBe true - expect(cursors[1].selection.isEmpty()).toBe true - expect(cursors[2].selection.isEmpty()).toBe true - - simulateTabKeyEvent() - expect(cursors[0].getBufferPosition()).toEqual [0, 0] - expect(cursors[1].getBufferPosition()).toEqual [7, 5] - expect(cursors[2].getBufferPosition()).toEqual [12, 2] - expect(cursors[0].selection.isEmpty()).toBe true - expect(cursors[1].selection.isEmpty()).toBe true - expect(cursors[2].selection.isEmpty()).toBe true - - describe "when the cursors do not share common snippet prefixes", -> - it "inserts tabs as normal", -> - editor.insertText('t9') - editor.setCursorBufferPosition([12, 2]) - editor.insertText(' t8') - editor.addCursorAtBufferPosition([0, 2]) - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "t9 var quicksort = function () {" - expect(editor.lineTextForBufferRow(12)).toBe "}; t8 " - - describe "when a snippet is triggered within an existing snippet expansion", -> - it "ignores the snippet expansion and goes to the next tab stop", -> - editor.addCursorAtBufferPosition([7, 5]) - editor.addCursorAtBufferPosition([12, 2]) - editor.insertText('t11') - simulateTabKeyEvent() - simulateTabKeyEvent() - - editor.insertText('t1') - simulateTabKeyEvent() - - cursors = editor.getCursors() - expect(cursors.length).toEqual 3 - - expect(cursors[0].getBufferPosition()).toEqual [0, 12] - expect(cursors[1].getBufferPosition()).toEqual [7, 17] - expect(cursors[2].getBufferPosition()).toEqual [12, 14] - expect(cursors[0].selection.isEmpty()).toBe true - expect(cursors[1].selection.isEmpty()).toBe true - expect(cursors[2].selection.isEmpty()).toBe true - expect(editor.lineTextForBufferRow(0)).toBe "one t1 threevar quicksort = function () {" - expect(editor.lineTextForBufferRow(7)).toBe " }one t1 three" - expect(editor.lineTextForBufferRow(12)).toBe "};one t1 three" - - describe "when the editor is not a pane item (regression)", -> - it "handles tab stops correctly", -> - editor = new TextEditor() - atom.grammars.assignLanguageMode(editor, 'source.js') - editorElement = editor.getElement() - - editor.insertText('t2') - simulateTabKeyEvent() - editor.insertText('ABC') - expect(editor.getText()).toContain('go here first:(ABC)') - - editor.undo() - editor.undo() - expect(editor.getText()).toBe('t2') - simulateTabKeyEvent() - editor.insertText('ABC') - expect(editor.getText()).toContain('go here first:(ABC)') - - describe "when atom://.atom/snippets is opened", -> - it "opens ~/.atom/snippets.cson", -> - jasmine.unspy(Snippets, 'getUserSnippetsPath') - atom.workspace.destroyActivePaneItem() - configDirPath = temp.mkdirSync('atom-config-dir-') - spyOn(atom, 'getConfigDirPath').andReturn configDirPath - atom.workspace.open('atom://.atom/snippets') - - waitsFor -> - atom.workspace.getActiveTextEditor()? - - runs -> - expect(atom.workspace.getActiveTextEditor().getURI()).toBe path.join(configDirPath, 'snippets.cson') - - describe "snippet insertion API", -> - it "will automatically parse snippet definition and replace selection", -> - editor.setSelectedBufferRange([[0, 4], [0, 13]]) - Snippets.insert("hello ${1:world}", editor) - - expect(editor.lineTextForBufferRow(0)).toBe "var hello world = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 10], [0, 15]] - - describe "when the 'snippets:available' command is triggered", -> - availableSnippetsView = null - - beforeEach -> - Snippets.add __filename, - ".source.js": - "test": - prefix: "test" - body: "${1:Test pass you will}, young " - - "challenge": - prefix: "chal" - body: "$1: ${2:To pass this challenge}" - - delete Snippets.availableSnippetsView - - atom.commands.dispatch(editorElement, "snippets:available") - - waitsFor -> - atom.workspace.getModalPanels().length is 1 - - runs -> - availableSnippetsView = atom.workspace.getModalPanels()[0].getItem() - - it "renders a select list of all available snippets", -> - expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe 'test' - expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe 'test' - expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe '${1:Test pass you will}, young ' - - availableSnippetsView.selectListView.selectNext() - - expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe 'chal' - expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe 'challenge' - expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe '$1: ${2:To pass this challenge}' - - it "writes the selected snippet to the editor as snippet", -> - availableSnippetsView.selectListView.confirmSelection() - - expect(editor.getCursorScreenPosition()).toEqual [0, 18] - expect(editor.getSelectedText()).toBe 'Test pass you will' - expect(editor.lineTextForBufferRow(0)).toBe 'Test pass you will, young var quicksort = function () {' - - it "closes the dialog when triggered again", -> - atom.commands.dispatch availableSnippetsView.selectListView.refs.queryEditor.element, 'snippets:available' - expect(atom.workspace.getModalPanels().length).toBe 0 From 1cb4511120d24ab81636503df790cdef257b1bd9 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Fri, 19 Apr 2019 20:53:14 +1000 Subject: [PATCH 59/77] Test misc cases --- spec/body-parser-spec-old.coffee | 261 ------------------------------- spec/body-parser-spec.js | 116 +++++++++++++- 2 files changed, 113 insertions(+), 264 deletions(-) delete mode 100644 spec/body-parser-spec-old.coffee diff --git a/spec/body-parser-spec-old.coffee b/spec/body-parser-spec-old.coffee deleted file mode 100644 index 6f7cdf4f..00000000 --- a/spec/body-parser-spec-old.coffee +++ /dev/null @@ -1,261 +0,0 @@ -BodyParser = require '../lib/snippet-body-parser' - -describe "Snippet Body Parser", -> - it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", -> - bodyTree = BodyParser.parse """ - the quick brown $1fox ${2:jumped ${3:over} - }the ${4:lazy} dog - """ - - expect(bodyTree).toEqual [ - "the quick brown ", - {index: 1, content: []}, - "fox ", - { - index: 2, - content: [ - "jumped ", - {index: 3, content: ["over"]}, - "\n" - ], - } - "the " - {index: 4, content: ["lazy"]}, - " dog" - ] - - it "removes interpolated variables in placeholder text (we don't currently support it)", -> - bodyTree = BodyParser.parse """ - module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} - """ - - expect(bodyTree).toEqual [ - "module ", - { - "index": 1, - "content": [ - "ActiveRecord::", - { - "variable": "TM_FILENAME", - "substitution": { - "find": /(?:\A|_)([A-Za-z0-9]+)(?:\.rb)?/g, - "replace": [ - "(?2::", - { - "escape": 'u' - }, - { - "backreference": 1 - }, - ")" - ] - } - } - ] - } - ] - - it "skips escaped tabstops", -> - bodyTree = BodyParser.parse """ - snippet $1 escaped \\$2 \\\\$3 - """ - - expect(bodyTree).toEqual [ - "snippet ", - { - index: 1, - content: [] - }, - " escaped $2 \\", - { - index: 3, - content: [] - } - ] - - it "includes escaped right-braces", -> - bodyTree = BodyParser.parse """ - snippet ${1:{\\}} - """ - - expect(bodyTree).toEqual [ - "snippet ", - { - index: 1, - content: ["{}"] - } - ] - - it "parses a snippet with transformations", -> - bodyTree = BodyParser.parse """ - <${1:p}>$0 - """ - expect(bodyTree).toEqual [ - '<', - {index: 1, content: ['p']}, - '>', - {index: 0, content: []}, - '' - ] - - it "parses a snippet with multiple tab stops with transformations", -> - bodyTree = BodyParser.parse """ - ${1:placeholder} ${1/(.)/\\u$1/} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2 - """ - - expect(bodyTree).toEqual [ - {index: 1, content: ['placeholder']}, - ' ', - { - index: 1, - content: [], - substitution: { - find: /(.)/, - replace: [ - {escape: 'u'}, - {backreference: 1} - ] - } - }, - ' ', - {index: 1, content: []}, - ' ', - {index: 2, content: ['ANOTHER']}, - ' ', - { - index: 2, - content: [], - substitution: { - find: /^(.*)$/, - replace: [ - {escape: 'L'}, - {backreference: 1} - ] - } - }, - ' ', - {index: 2, content: []}, - ] - - - it "parses a snippet with transformations and mirrors", -> - bodyTree = BodyParser.parse """ - ${1:placeholder}\n${1/(.)/\\u$1/}\n$1 - """ - - expect(bodyTree).toEqual [ - {index: 1, content: ['placeholder']}, - '\n', - { - index: 1, - content: [], - substitution: { - find: /(.)/, - replace: [ - {escape: 'u'}, - {backreference: 1} - ] - } - }, - '\n', - {index: 1, content: []} - ] - - it "parses a snippet with a format string and case-control flags", -> - bodyTree = BodyParser.parse """ - <${1:p}>$0 - """ - - expect(bodyTree).toEqual [ - '<', - {index: 1, content: ['p']}, - '>', - {index: 0, content: []}, - '' - ] - - it "parses a snippet with an escaped forward slash in a transform", -> - # Annoyingly, a forward slash needs to be double-backslashed just like the - # other escapes. - bodyTree = BodyParser.parse """ - <${1:p}>$0 - """ - - expect(bodyTree).toEqual [ - '<', - {index: 1, content: ['p']}, - '>', - {index: 0, content: []}, - '' - ] - - it "parses a snippet with a placeholder that mirrors another tab stop's content", -> - bodyTree = BodyParser.parse """ - $4console.${3:log}('${2:$1}', $1);$0 - """ - - expect(bodyTree).toEqual [ - {index: 4, content: []}, - 'console.', - {index: 3, content: ['log']}, - '(\'', - { - index: 2, content: [ - {index: 1, content: []} - ] - }, - '\', ', - {index: 1, content: []}, - ');', - {index: 0, content: []} - ] - - it "parses a snippet with a placeholder that mixes text and tab stop references", -> - bodyTree = BodyParser.parse """ - $4console.${3:log}('${2:uh $1}', $1);$0 - """ - - expect(bodyTree).toEqual [ - {index: 4, content: []}, - 'console.', - {index: 3, content: ['log']}, - '(\'', - { - index: 2, content: [ - 'uh ', - {index: 1, content: []} - ] - }, - '\', ', - {index: 1, content: []}, - ');', - {index: 0, content: []} - ] diff --git a/spec/body-parser-spec.js b/spec/body-parser-spec.js index 3b8ef568..887aa1ac 100644 --- a/spec/body-parser-spec.js +++ b/spec/body-parser-spec.js @@ -99,8 +99,8 @@ describe('Snippet Body Parser', () => { describe('for snippets with variables', () => { it('parses simple variables', () => { - expectMatch('$foo', [{variable: 'foo'}]) - expectMatch('$FOO', [{variable: 'FOO'}]) + expectMatch('$f_o_0', [{variable: 'f_o_0'}]) + expectMatch('$_FOO', [{variable: '_FOO'}]) }) it('parses verbose variables', () => { @@ -268,7 +268,7 @@ describe('Snippet Body Parser', () => { ]) }) - it('supports if replacements', () => { + it('supports if-else replacements', () => { // NOTE: the '+' cannot be escaped. If you want it to be part of // a placeholder (else only), use ':-' expectMatch('${a/./${1:+foo$0bar\\}baz}/}', [ @@ -335,4 +335,114 @@ describe('Snippet Body Parser', () => { ]) }) }) + + describe('on miscellaneous examples', () => { + it('handles a simple snippet', () => { + expectMatch( + 'the quick brown $1fox ${2:jumped ${3:over}\n}the ${4:lazy} dog', + [ + 'the quick brown ', + {index: 1, content: []}, + 'fox ', + { + index: 2, + content: [ + 'jumped ', + {index: 3, content: ['over']}, + '\n' + ] + }, + 'the ', + {index: 4, content: ['lazy']}, + ' dog' + ] + ) + }) + + it('handles a snippet with a transformed variable', () => { + expectMatch( + 'module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}}', + [ + 'module ', + { + index: 1, + content: [ + 'ActiveRecord::', + { + variable: 'TM_FILENAME', + substitution: { + find: /(?:\A|_)([A-Za-z0-9]+)(?:\.rb)?/g, + replace: [ + '(?2::', + {escape: 'u'}, + {backreference: 1}, + ')' + ] + } + } + ] + } + ] + ) + }) + + it('handles a snippet with multiple tab stops with transformations', () => { + expectMatch( + '${1:placeholder} ${1/(.)/\\u$1/} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2', + [ + {index: 1, content: ['placeholder']}, + ' ', + { + index: 1, + content: [], + substitution: { + find: /(.)/, + replace: [ + {escape: 'u'}, + {backreference: 1} + ] + } + }, + ' ', + {index: 1, content: []}, + ' ', + {index: 2, content: ['ANOTHER']}, + ' ', + { + index: 2, + content: [], + substitution: { + find: /^(.*)$/, + replace: [ + {escape: 'L'}, + {backreference: 1} + ] + } + }, + ' ', + {index: 2, content: []} + ] + ) + }) + + it('handles a snippet with a placeholder that mirrors another tab stops content', () => { + expectMatch( + '$4console.${3:log}(\'${2:$1}\', $1);$0', + [ + {index: 4, content: []}, + 'console.', + {index: 3, content: ['log']}, + '(\'', + { + index: 2, + content: [{index: 1, content: []}] + }, + '\', ', + {index: 1, content: []}, + ');', + {index: 0, content: []} + ] + ) + }) + }) }) From 7934443cb9ee3f144ab3d488444580e01311d012 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Fri, 19 Apr 2019 23:14:06 +1000 Subject: [PATCH 60/77] remove console log --- lib/snippet-expansion.js | 1 - lib/snippets.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index f0467f26..1c892427 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -64,7 +64,6 @@ module.exports = class SnippetExpansion { return } - console.log('cursor out of bounds') this.destroy(true) } diff --git a/lib/snippets.js b/lib/snippets.js index dc82528f..7bd12f13 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -52,7 +52,6 @@ module.exports = { this.subscriptions.add(atom.commands.add('atom-text-editor', { 'snippets:expand'(event) { - console.log('expanding snippet') const editor = this.getModel() const snippet = snippets.snippetToExpandUnderCursor(editor) if (snippet) { @@ -562,8 +561,6 @@ module.exports = { editor.buffer.createCheckpoint() } - console.log('end:', isEnd, 'succ:', nextTabStopVisited) - return nextTabStopVisited }, @@ -595,7 +592,6 @@ module.exports = { let layer = this.editorMarkerLayers.get(editor) if (layer === undefined) { layer = editor.addMarkerLayer({maintainHistory: true}) - console.log(layer.id, ) this.editorMarkerLayers.set(editor, layer) } return layer From 429a12741ddabcff8178578888bddff593ccfead Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sat, 20 Apr 2019 00:26:19 +1000 Subject: [PATCH 61/77] start testing snippet body text --- spec/insertion-spec.js | 121 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/spec/insertion-spec.js b/spec/insertion-spec.js index 7e6d5056..342ba50b 100644 --- a/spec/insertion-spec.js +++ b/spec/insertion-spec.js @@ -1,9 +1,128 @@ const Insertion = require('../lib/insertion') -const {Range} = require('atom') +const {Range, TextEditor} = require('atom') const range = new Range(0, 0) +const Snippets = require('../lib/snippets') + describe('Insertion', () => { + let editor + let editorElement + + beforeEach(() => { + spyOn(Snippets, 'loadAll') + spyOn(Snippets, 'getUserSnippetsPath').andReturn('') + + waitsForPromise(() => atom.workspace.open()) + waitsForPromise(() => atom.packages.activatePackage('snippets')) + + runs(() => { + editor = atom.workspace.getActiveTextEditor() + editorElement = atom.views.getView(editor) + }) + }) + + function resolve (snippet) { + Snippets.add(__filename, { + '*': { + 'a': { + prefix: 'a', + body: snippet + } + } + }) + + editor.setText('a') + editor.setCursorBufferPosition([0, 1]) + atom.commands.dispatch(editorElement, 'snippets:expand') + return editor.getText() + } + + it('resolves a plain snippet', () => { + expect(resolve('${} $ n $}1} ${/upcase/} \n world ${||}')) + .toEqual('${} $ n $}1} ${/upcase/} \n world ${||}') + }) + + it('resolves a snippet with tabstops', () => { + expect(resolve('hello$1world${2}')).toEqual('helloworld') + }) + + it('resolves snippets with placeholders', () => { + expect(resolve('${1:hello} world')).toEqual('hello world') + expect(resolve('${1:one${2:tw${3:othre}e}}')).toEqual('onetwothree') + }) + + it('uses the first choice as a placeholder', () => { + expect(resolve('${1|one,two,three|}')).toEqual('one') + }) + + describe('when resolving variables', () => { + it('resolves base variables', () => { + expect(resolve('$TM_LINE_INDEX')).toEqual('0') + expect(resolve('$TM_LINE_NUMBER')).toEqual('1') + expect(/\d{4,}/.test(resolve('$CURRENT_YEAR'))).toEqual(true) + + atom.clipboard.write('foo') + expect(resolve('$CLIPBOARD')).toEqual('foo') + }) + + it('uses unknown variables as placeholders', () => { + expect(resolve('$GaRBag3')).toEqual('GaRBag3') + }) + + it('allows more resolvers to be provided', () => { + Snippets.consumeResolver({ + variableResolvers: { + 'EXTENDED': () => 'calculated resolution', + 'POSITION': ({row}) => `${row}` + } + }) + + expect(resolve('$EXTENDED')).toEqual('calculated resolution') + expect(resolve('foo\n$POSITION')).toEqual('foo\n1') + }) + + it('allows provided resolvers to override builtins', () => { + expect(resolve('$TM_LINE_INDEX')).toEqual('0') + Snippets.consumeResolver({ + variableResolvers: { + 'TM_LINE_INDEX': () => 'umbrella' + } + }) + expect(resolve('$TM_LINE_INDEX')).toEqual('umbrella') + }) + }) + + describe('when resolving transforms', () => { + beforeEach(() => { + Snippets.consumeResolver({ + variableResolvers: { + 'A': () => 'hello world', + 'B': () => 'foo\nbar\nbaz', + 'C': () => '😄foo', + 'D': () => 'baz foo' + } + }) + }) + + it('respects the provided flags', () => { + expect(resolve('${A/.//}')).toEqual('ello world') + expect(resolve('${A/.//g}')).toEqual('') + + expect(resolve('${A/HELLO//}')).toEqual('hello world') + expect(resolve('${A/HELLO//i}')).toEqual(' world') + + expect(resolve('${B/^ba(.)$/$1/}')).toEqual('foo\nbar\nbaz') + expect(resolve('${B/^ba(.)$/$1/m}')).toEqual('foo\nr\nbaz') + + expect(resolve('${C/^.foo$/bar/}')).toEqual('😄foo') // without /u, the emoji is seen as two characters + expect(resolve('${C/^.foo$/bar/u}')).toEqual('bar') + + expect(resolve('${D/foo/bar/}')).toEqual('baz bar') + expect(resolve('${D/foo/bar/y}')).toEqual('baz foo') // with /y, the search is only from index 0 and fails + }) + }) + it('returns what it was given when it has no substitution', () => { let insertion = new Insertion({ range, From 5f20163d0dca4d4c28f4f8cc0a4d48ab53d1353d Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sat, 20 Apr 2019 15:09:12 +1000 Subject: [PATCH 62/77] relax undefined check --- lib/snippet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/snippet.js b/lib/snippet.js index fd98f516..b2595520 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -128,7 +128,7 @@ function stringifyVariable (node, params, acc) { resolvedValue = value } - if (resolvedValue === undefined) { // variable known, but no value: use default contents or (implicitly) empty string + if (resolvedValue == undefined) { // variable known, but no value: use default contents or (implicitly) empty string if (node.content) { stringifyContent(node.content, params, acc) } From 43bae26eaf82555a61175d825d2883b5558461d8 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sat, 20 Apr 2019 15:09:42 +1000 Subject: [PATCH 63/77] Make tests more robust and flexible --- spec/insertion-spec.js | 241 +++++++++++++++++------------------------ 1 file changed, 101 insertions(+), 140 deletions(-) diff --git a/spec/insertion-spec.js b/spec/insertion-spec.js index 342ba50b..922b36a7 100644 --- a/spec/insertion-spec.js +++ b/spec/insertion-spec.js @@ -1,8 +1,3 @@ -const Insertion = require('../lib/insertion') -const {Range, TextEditor} = require('atom') - -const range = new Range(0, 0) - const Snippets = require('../lib/snippets') describe('Insertion', () => { @@ -22,6 +17,10 @@ describe('Insertion', () => { }) }) + afterEach(() => { + waitsForPromise(() => atom.packages.deactivatePackage('snippets')) + }) + function resolve (snippet) { Snippets.add(__filename, { '*': { @@ -35,9 +34,14 @@ describe('Insertion', () => { editor.setText('a') editor.setCursorBufferPosition([0, 1]) atom.commands.dispatch(editorElement, 'snippets:expand') + Snippets.clearExpansions(editor) return editor.getText() } + function transform (input, transform, replacement, flags = '') { + return resolve(`\${1:${input}}\${1/${transform}/${replacement}/${flags}}`).slice(input.length) + } + it('resolves a plain snippet', () => { expect(resolve('${} $ n $}1} ${/upcase/} \n world ${||}')) .toEqual('${} $ n $}1} ${/upcase/} \n world ${||}') @@ -49,13 +53,21 @@ describe('Insertion', () => { it('resolves snippets with placeholders', () => { expect(resolve('${1:hello} world')).toEqual('hello world') - expect(resolve('${1:one${2:tw${3:othre}e}}')).toEqual('onetwothree') + expect(resolve('${1:one${2:two${3:thr}e}e}')).toEqual('onetwothree') }) - it('uses the first choice as a placeholder', () => { - expect(resolve('${1|one,two,three|}')).toEqual('one') + describe('when resolving choices', () => { + it('uses the first choice as a placeholder', () => { + expect(resolve('${1|one,two,three|}')).toEqual('one') + }) + + it('uses the first non transforming placeholder for transformations', () => { + expect(resolve('${1:foo} ${1|one,two,three|} ${1/.*/$0/}')).toEqual('foo one foo') + expect(resolve('${1|one,two,three|} ${1:foo} ${1/.*/$0/}')).toEqual('one foo one') + }) }) + describe('when resolving variables', () => { it('resolves base variables', () => { expect(resolve('$TM_LINE_INDEX')).toEqual('0') @@ -66,10 +78,6 @@ describe('Insertion', () => { expect(resolve('$CLIPBOARD')).toEqual('foo') }) - it('uses unknown variables as placeholders', () => { - expect(resolve('$GaRBag3')).toEqual('GaRBag3') - }) - it('allows more resolvers to be provided', () => { Snippets.consumeResolver({ variableResolvers: { @@ -79,7 +87,43 @@ describe('Insertion', () => { }) expect(resolve('$EXTENDED')).toEqual('calculated resolution') - expect(resolve('foo\n$POSITION')).toEqual('foo\n1') + expect(resolve('$POSITION\n$POSITION')).toEqual('0\n1') + }) + + describe('when a variable is unknown', () => { + it('uses uses the variable name as a placeholder', () => { + expect(resolve('$GaRBag3')).toEqual('GaRBag3') + }) + + it('will not try to transform an unknown variable', () => { + expect(resolve('${GaRBag3/.*/foo/}')).toEqual('GaRBag3') + }) + }) + + describe('when a variable is known but not set', () => { + beforeEach(() => { + Snippets.consumeResolver({ + variableResolvers: { + 'UNDEFINED': () => undefined, + 'NULL': () => null, + 'EMPTY': () => '' + } + }) + }) + + it('uses the placeholder value if possible', () => { + expect(resolve('${UNDEFINED:placeholder}')).toEqual('placeholder') + expect(resolve('${NULL:placeholder}')).toEqual('placeholder') + expect(resolve('${EMPTY:placeholder}')).toEqual('') // empty string is a valid resolution + }) + + it('will transform an unset variable as if it was the empty string', () => { + expect(resolve('${UNDEFINED/^$/foo/}')).toEqual('foo') + }) + + it('can resolve variables in placeholders', () => { + expect(resolve('${UNDEFINED:$TM_LINE_INDEX}')).toEqual('0') + }) }) it('allows provided resolvers to override builtins', () => { @@ -100,12 +144,17 @@ describe('Insertion', () => { 'A': () => 'hello world', 'B': () => 'foo\nbar\nbaz', 'C': () => '😄foo', - 'D': () => 'baz foo' + 'D': () => 'baz foo', + 'E': () => 'foo baz foo' } }) }) - it('respects the provided flags', () => { + it('leaves the existing value when the transform is empty', () => { + expect(resolve('${A///}')).toEqual('hello world') + }) + + it('respects the provided regex flags', () => { expect(resolve('${A/.//}')).toEqual('ello world') expect(resolve('${A/.//g}')).toEqual('') @@ -120,134 +169,46 @@ describe('Insertion', () => { expect(resolve('${D/foo/bar/}')).toEqual('baz bar') expect(resolve('${D/foo/bar/y}')).toEqual('baz foo') // with /y, the search is only from index 0 and fails + expect(resolve('${E/foo/bar/g}')).toEqual('bar baz bar') + expect(resolve('${E/foo/bar/gy}')).toEqual('bar baz foo') }) }) - it('returns what it was given when it has no substitution', () => { - let insertion = new Insertion({ - range, - substitution: undefined + describe('when there are case flags', () => { + it('transforms the case of the next character when encountering a \\u or \\l flag', () => { + let find = '(.)(.)(.*)' + let replace = '$1\\u$2$3' + expect(transform('foo!', find, replace, 'g')).toEqual('fOo!') + expect(transform('fOo!', find, replace, 'g')).toEqual('fOo!') + expect(transform('FOO!', find, replace, 'g')).toEqual('FOO!') + + find = '(.{2})(.)(.*)' + replace = '$1\\l$2$3' + expect(transform('FOO!', find, replace, 'g')).toEqual('FOo!') + expect(transform('FOo!', find, replace, 'g')).toEqual('FOo!') + expect(transform('FoO!', find, replace, 'g')).toEqual('Foo!') + expect(transform('foo!', find, replace, 'g')).toEqual('foo!') + }) + + it('transforms the case of all remaining characters when encountering a \\U or \\L flag, up until it sees a \\E flag', () => { + let find = '(.)(.*)' + let replace = '$1\\U$2' + expect(transform('lorem ipsum!', find, replace)).toEqual('lOREM IPSUM!') + expect(transform('lOREM IPSUM!', find, replace)).toEqual('lOREM IPSUM!') + expect(transform('LOREM IPSUM!', find, replace)).toEqual('LOREM IPSUM!') + + find = '(.)(.{3})(.*)' + replace = '$1\\U$2\\E$3' + expect(transform('lorem ipsum!', find, replace)).toEqual('lOREm ipsum!') + expect(transform('lOREm ipsum!', find, replace)).toEqual('lOREm ipsum!') + expect(transform('LOREM IPSUM!', find, replace)).toEqual('LOREM IPSUM!') + + expect(transform('LOREM IPSUM!', '(.{4})(.)(.*)', '$1\\L$2WHAT')).toEqual('LOREmwhat') + + find = '^([A-Fa-f])(.*)(.)$' + replace = '$1\\L$2\\E$3' + expect(transform('LOREM IPSUM!', find, replace)).toEqual('LOREM IPSUM!') + expect(transform('CONSECUETUR', find, replace)).toEqual('ConsecuetuR') }) - let transformed = insertion.transform('foo!') - - expect(transformed).toEqual('foo!') - }) - - it('transforms what it was given when it has a regex transformation', () => { - let insertion = new Insertion({ - range, - substitution: { - find: /foo/g, - replace: ['bar'] - } - }) - let transformed = insertion.transform('foo!') - - expect(transformed).toEqual('bar!') - }) - - it('transforms the case of the next character when encountering a \\u or \\l flag', () => { - let uInsertion = new Insertion({ - range, - substitution: { - find: /(.)(.)(.*)/g, - replace: [ - { backreference: 1 }, - { escape: 'u' }, - { backreference: 2 }, - { backreference: 3 } - ] - } - }) - - expect(uInsertion.transform('foo!')).toEqual('fOo!') - expect(uInsertion.transform('fOo!')).toEqual('fOo!') - expect(uInsertion.transform('FOO!')).toEqual('FOO!') - - let lInsertion = new Insertion({ - range, - substitution: { - find: /(.{2})(.)(.*)/g, - replace: [ - { backreference: 1 }, - { escape: 'l' }, - { backreference: 2 }, - { backreference: 3 } - ] - } - }) - - expect(lInsertion.transform('FOO!')).toEqual('FOo!') - expect(lInsertion.transform('FOo!')).toEqual('FOo!') - expect(lInsertion.transform('FoO!')).toEqual('Foo!') - expect(lInsertion.transform('foo!')).toEqual('foo!') - }) - - it('transforms the case of all remaining characters when encountering a \\U or \\L flag, up until it sees a \\E flag', () => { - let uInsertion = new Insertion({ - range, - substitution: { - find: /(.)(.*)/, - replace: [ - { backreference: 1 }, - { escape: 'U' }, - { backreference: 2 } - ] - } - }) - - expect(uInsertion.transform('lorem ipsum!')).toEqual('lOREM IPSUM!') - expect(uInsertion.transform('lOREM IPSUM!')).toEqual('lOREM IPSUM!') - expect(uInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!') - - let ueInsertion = new Insertion({ - range, - substitution: { - find: /(.)(.{3})(.*)/, - replace: [ - { backreference: 1 }, - { escape: 'U' }, - { backreference: 2 }, - { escape: 'E' }, - { backreference: 3 } - ] - } - }) - - expect(ueInsertion.transform('lorem ipsum!')).toEqual('lOREm ipsum!') - expect(ueInsertion.transform('lOREm ipsum!')).toEqual('lOREm ipsum!') - expect(ueInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!') - - let lInsertion = new Insertion({ - range, - substitution: { - find: /(.{4})(.)(.*)/, - replace: [ - { backreference: 1 }, - { escape: 'L' }, - { backreference: 2 }, - 'WHAT' - ] - } - }) - - expect(lInsertion.transform('LOREM IPSUM!')).toEqual('LOREmwhat') - - let leInsertion = new Insertion({ - range, - substitution: { - find: /^([A-Fa-f])(.*)(.)$/, - replace: [ - { backreference: 1 }, - { escape: 'L' }, - { backreference: 2 }, - { escape: 'E' }, - { backreference: 3 } - ] - } - }) - - expect(leInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!') - expect(leInsertion.transform('CONSECUETUR')).toEqual('ConsecuetuR') }) }) From 260d437281629b6e5a8575f74478ec3ec8ad6e61 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sat, 20 Apr 2019 16:47:22 +1000 Subject: [PATCH 64/77] more specs --- spec/insertion-spec.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/spec/insertion-spec.js b/spec/insertion-spec.js index 922b36a7..d11bb44d 100644 --- a/spec/insertion-spec.js +++ b/spec/insertion-spec.js @@ -67,7 +67,6 @@ describe('Insertion', () => { }) }) - describe('when resolving variables', () => { it('resolves base variables', () => { expect(resolve('$TM_LINE_INDEX')).toEqual('0') @@ -211,4 +210,43 @@ describe('Insertion', () => { expect(transform('CONSECUETUR', find, replace)).toEqual('ConsecuetuR') }) }) + + describe('when there are replacement transformations', () => { + it('knows some basic transformations', () => { + expect(transform('foo', '.*', '${0:/upcase}')).toEqual('FOO') + expect(transform('FOO', '.*', '${0:/downcase}')).toEqual('foo') + expect(transform('foo bar', '.*', '${0:/capitalize}')).toEqual('Foo bar') + }) + + it('uses the empty string for an unknown transformation', () => { + expect(transform('foo', '.*', '${0:/GaRBagE}')).toEqual('') + }) + + it('allows more transformations to be provided', () => { + expect(transform('foo', '.*', '${0:/extension}')).toEqual('') + Snippets.consumeResolver({ + transformResolvers: { + 'extension': () => 'extended', + 'echo': ({input}) => input + '... ' + input + } + }) + expect(transform('foo', '.*', '${0:/extension}')).toEqual('extended') + expect(transform('foo', '.*', '${0:/echo}')).toEqual('foo... foo') + }) + + it('allows provided transformations to override builtins', () => { + expect(transform('foo', '.*', '${0:/capitalize}')).toEqual('Foo') + Snippets.consumeResolver({ + transformResolvers: { + 'capitalize': () => 'different' + } + }) + expect(transform('foo', '.*', '${0:/capitalize}')).toEqual('different') + }) + + it('lets verbose transforms take priority over case flags', () => { + expect(transform('foo bar baz', '(foo) (bar) (baz)', '$1 \\U$2 $3')).toEqual('foo BAR BAZ') + expect(transform('foo bar baz', '(foo) (bar) (baz)', '$1 \\U${2:/downcase} $3')).toEqual('foo bar BAZ') + }) + }) }) From 60f3af02e2d9ecfc112f4b7eaaa5b84f311085e8 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sat, 20 Apr 2019 16:56:47 +1000 Subject: [PATCH 65/77] :art: --- spec/body-parser-spec.js | 3 +++ spec/snippets-spec.js | 5 +---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/body-parser-spec.js b/spec/body-parser-spec.js index 887aa1ac..d01255a3 100644 --- a/spec/body-parser-spec.js +++ b/spec/body-parser-spec.js @@ -271,6 +271,9 @@ describe('Snippet Body Parser', () => { it('supports if-else replacements', () => { // NOTE: the '+' cannot be escaped. If you want it to be part of // a placeholder (else only), use ':-' + // NOTE 2: the ${n:} syntax should really be deprecated, + // as it's very easy to accidentally use; e.g., when you have an invalid + // transformation like `${1:/foo3}` (has number) expectMatch('${a/./${1:+foo$0bar\\}baz}/}', [ { variable: 'a', diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 3cb2cb20..62e09c19 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -873,10 +873,7 @@ describe('Snippets extension', () => { describe('when the editor is not a pane item (regression)', () => { it('handles tab stops correctly', () => { - editor = new TextEditor() - editorElement = editor.getElement() - - editor.insertText('t2') + editor.setText('t2') simulateTabKeyEvent() editor.insertText('ABC') expect(editor.lineTextForBufferRow(1)).toEqual('go here first:(ABC)') From 687b174a85aabac9353aee14246337782134b4c9 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sat, 20 Apr 2019 17:14:11 +1000 Subject: [PATCH 66/77] fix undo specs --- spec/snippets-spec.js | 121 +++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 62e09c19..8d118253 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -304,9 +304,6 @@ describe('Snippets extension', () => { expect(editor.lineTextForBufferRow(0)).toBe('go here next:(\t) and finally go here:()') expect(editor.getCursorBufferPosition()).toEqual([0, 15]) - - // test we can terminate with shift-tab - // TODO: Not sure what this was doing / is for }) }) @@ -632,24 +629,27 @@ describe('Snippets extension', () => { expect(editor.getText()).toBe('[img src][/img]') }) - it('bundles the transform mutations along with the original manual mutation for the purposes of undo and redo', async () => { - // TODO - // editor.setText('t12') - // simulateTabKeyEvent() - // editor.insertText('i') - // expect(editor.getText()).toBe("[i][/i]") - // - // editor.insertText('mg src') - // expect(editor.getText()).toBe("[img src][/img]") - // editor.undo() - // - // expect(editor.getText()).toBe("[b][/b]") - // // let now = 500 - // // while (now) { now--; console.log(now) } - // // expect(editor.getText()).toBe("[i][/i]") // Would actually expect text to be empty, because undo intervals are time based - // // - // // editor.redo() - // // expect(editor.getText()).toBe("[img src][/img]") + it('bundles the transform mutations along with the original manual mutation for the purposes of undo and redo', () => { + // NOTE: Most likely spec here to fail on CI, as it is time based + const transactionDuration = 300 + + editor.setText('t12') + simulateTabKeyEvent() + editor.transact(transactionDuration, () => { + editor.insertText('i') + }) + expect(editor.getText()).toBe("[i][/i]") + + editor.transact(transactionDuration, () => { + editor.insertText('mg src') + }) + expect(editor.getText()).toBe("[img src][/img]") + + editor.undo() + expect(editor.getText()).toBe("[b][/b]") + + editor.redo() + expect(editor.getText()).toBe("[img src][/img]") }) it('can pick the right insertion to use as the primary even if a transformed insertion occurs first in the snippet', () => { @@ -772,30 +772,37 @@ describe('Snippets extension', () => { }) it('bundles transform-induced mutations into a single history entry along with their triggering edit, even across multiple snippets', () => { + // NOTE: Another likely spec to fail on CI, as it is time based + const transactionDuration = 300 + editor.setText('t14\nt14') editor.setCursorBufferPosition([1, 3]) editor.addCursorAtBufferPosition([0, 3]) simulateTabKeyEvent() - editor.insertText('testing') + editor.transact(transactionDuration, () => { + editor.insertText('testing') + }) simulateTabKeyEvent() - editor.insertText('AGAIN') - // TODO - // editor.undo() - // expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing ANOTHER another ') - // expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing ANOTHER another ') - // - // editor.undo() - // expect(editor.lineTextForBufferRow(0)).toBe('placeholder PLACEHOLDER ANOTHER another ') - // expect(editor.lineTextForBufferRow(1)).toBe('placeholder PLACEHOLDER ANOTHER another ') - // - // editor.redo() - // expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing ANOTHER another ') - // expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing ANOTHER another ') - // - // editor.redo() - // expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing AGAIN again AGAIN') - // expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing AGAIN again AGAIN') + editor.transact(transactionDuration, () => { + editor.insertText('AGAIN') + }) + + editor.undo() + expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing ANOTHER another ') + expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing ANOTHER another ') + + editor.undo() + expect(editor.lineTextForBufferRow(0)).toBe('placeholder PLACEHOLDER ANOTHER another ') + expect(editor.lineTextForBufferRow(1)).toBe('placeholder PLACEHOLDER ANOTHER another ') + + editor.redo() + expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing ANOTHER another ') + expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing ANOTHER another ') + + editor.redo() + expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing AGAIN again AGAIN') + expect(editor.lineTextForBufferRow(1)).toBe('testing TESTING testing AGAIN again AGAIN') }) }) @@ -829,14 +836,13 @@ describe('Snippets extension', () => { expect(cursors[1].selection.isEmpty()).toBe(true) expect(cursors[2].selection.isEmpty()).toBe(true) - // TODO - // simulateTabKeyEvent() - // expect(cursors[0].getBufferPosition()).toEqual([0, 0]) - // expect(cursors[1].getBufferPosition()).toEqual([1, 0]) - // expect(cursors[2].getBufferPosition()).toEqual([2, 0]) - // expect(cursors[0].selection.isEmpty()).toBe(true) - // expect(cursors[1].selection.isEmpty()).toBe(true) - // expect(cursors[2].selection.isEmpty()).toBe(true) + simulateTabKeyEvent() + expect(cursors[0].getBufferPosition()).toEqual([0, 0]) + expect(cursors[1].getBufferPosition()).toEqual([1, 0]) + expect(cursors[2].getBufferPosition()).toEqual([2, 0]) + expect(cursors[0].selection.isEmpty()).toBe(true) + expect(cursors[1].selection.isEmpty()).toBe(true) + expect(cursors[2].selection.isEmpty()).toBe(true) }) }) @@ -873,18 +879,23 @@ describe('Snippets extension', () => { describe('when the editor is not a pane item (regression)', () => { it('handles tab stops correctly', () => { + // NOTE: Possibly flaky test + const transactionDuration = 300 editor.setText('t2') simulateTabKeyEvent() - editor.insertText('ABC') + editor.transact(transactionDuration, () => { + editor.insertText('ABC') + }) expect(editor.lineTextForBufferRow(1)).toEqual('go here first:(ABC)') - // TODO - // editor.undo() - // editor.undo() - // expect(editor.getText()).toBe('t2') - // simulateTabKeyEvent() - // editor.insertText('ABC') - // expect(editor.getText()).toContain('go here first:(ABC)') + editor.undo() + editor.undo() + expect(editor.getText()).toBe('t2') + simulateTabKeyEvent() + editor.transact(transactionDuration, () => { + editor.insertText('ABC') + }) + expect(editor.getText()).toContain('go here first:(ABC)') }) }) }) From fb4b3e9beaed0083daaad2d30e61a0ef904c71db Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sat, 20 Apr 2019 22:27:17 +1000 Subject: [PATCH 67/77] Fix yet another undo bug --- lib/snippet-expansion.js | 49 ++++++++++++++++------------------------ lib/snippets.js | 32 ++++++++++++++------------ package.json | 5 ++++ 3 files changed, 41 insertions(+), 45 deletions(-) diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index 1c892427..b2b8af04 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -30,7 +30,6 @@ module.exports = class SnippetExpansion { const tabStops = this.tabStopList.toArray() this.ignoringBufferChanges(() => { const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) - // this.editor.buffer.groupLastChanges() if (this.tabStopList.length > 0) { this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) @@ -64,10 +63,16 @@ module.exports = class SnippetExpansion { return } - this.destroy(true) + this.destroy() + this.snippets.destroyExpansions(this.editor) } - cursorDestroyed () { if (!this.settingTabStop) { this.destroy() } } + cursorDestroyed () { + if (!this.settingTabStop) { + this.destroy() + this.snippets.destroyExpansions(this.editor) + } + } textChanged (event) { if (this.isIgnoringBufferChanges) { return } @@ -154,32 +159,34 @@ module.exports = class SnippetExpansion { // if we have an endstop (implicit ends have already been added) it will be the last one if (nextIndex === this.tabStopMarkers.length - 1 && this.tabStopList.hasEndStop) { - return { - succeeded: this.setTabStopIndex(nextIndex), - end: true - } + const succeeded = this.setTabStopIndex(nextIndex) + this.destroy() + return {succeeded, isDestroyed: true} } // we are not at the end, and the next is not the endstop; just go to next stop if (nextIndex < this.tabStopMarkers.length) { const succeeded = this.setTabStopIndex(nextIndex) - if (succeeded) { return {succeeded, end: false} } + if (succeeded) { return {succeeded, isDestroyed: false} } return this.goToNextTabStop() } // we have just tabbed past the final tabstop; silently clean up, and let an actual tab be inserted this.destroy() - return {succeeded: false, end: true} + return {succeeded: false, isDestroyed: true} } goToPreviousTabStop () { if (this.tabStopIndex > 0) { return { succeeded: this.setTabStopIndex(this.tabStopIndex - 1), - end: false + isDestroyed: false } } - return {succeeded: true, end: false} + return { + succeeded: atom.config.get('snippets.disableTabDedentInSnippet'), + isDestroyed: false + } } setTabStopIndex (tabStopIndex) { @@ -227,27 +234,9 @@ module.exports = class SnippetExpansion { return markerSelected } - goToEndOfLastTabStop () { - if (this.tabStopMarkers.length === 0) { return } - const items = this.tabStopMarkers[this.tabStopMarkers.length - 1] - if (items.length === 0) { return } - const {marker: lastMarker} = items[items.length - 1] - if (lastMarker.isDestroyed()) { - return false - } else { - this.editor.setCursorBufferPosition(lastMarker.getEndBufferPosition()) - return true - } - } - - destroy (all = false) { + destroy () { this.subscriptions.dispose() this.tabStopMarkers = [] - if (all) { - this.getMarkerLayer(this.editor).clear() - this.snippets.stopObservingEditor(this.editor) - this.snippets.clearExpansions(this.editor) - } } getMarkerLayer () { diff --git a/lib/snippets.js b/lib/snippets.js index 7bd12f13..199a46a8 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -545,18 +545,19 @@ module.exports = { goToNextTabStop (editor) { let nextTabStopVisited = false - let isEnd = true + let destroy = false + for (const expansion of this.getExpansions(editor)) { if (!expansion) { continue } - const {succeeded, end} = expansion.goToNextTabStop() - if (isEnd) { isEnd = end } + const {succeeded, isDestroyed} = expansion.goToNextTabStop() + if (!destroy) { destroy = isDestroyed } if (succeeded) { nextTabStopVisited = true } } - if (isEnd) { - this.findOrCreateMarkerLayer(editor).clear() - this.stopObservingEditor(editor) - this.clearExpansions(editor) + + if (destroy) { + this.destroyExpansions(editor) } + if (nextTabStopVisited) { editor.buffer.createCheckpoint() } @@ -566,21 +567,16 @@ module.exports = { goToPreviousTabStop (editor) { let previousTabStopVisited = false - let isEnd = true for (const expansion of this.getExpansions(editor)) { if (!expansion) { continue; } - const {succeeded, end} = expansion.goToPreviousTabStop(!previousTabStopVisited) - // if (isEnd) { isEnd = end } + const {succeeded} = expansion.goToPreviousTabStop(!previousTabStopVisited) if (succeeded) { previousTabStopVisited = true } } - // if (isEnd) { - // this.findOrCreateMarkerLayer(editor).clear() - // this.stopObservingEditor(editor) - // this.clearExpansions(editor) - // } + if (previousTabStopVisited) { editor.buffer.createCheckpoint() } + return previousTabStopVisited }, @@ -614,6 +610,12 @@ module.exports = { this.getStore(editor).addExpansion(snippetExpansion) }, + destroyExpansions (editor) { + this.findOrCreateMarkerLayer(editor).clear() + this.stopObservingEditor(editor) + this.clearExpansions(editor) + }, + textChanged (editor, event) { const store = this.getStore(editor) const activeExpansions = store.getExpansions() diff --git a/package.json b/package.json index 6d4805eb..bbdaa8ef 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,11 @@ "description": "Add a final tabstop at the end of the snippet when the $0 stop is not set", "type": "boolean", "default": true + }, + "disableTabDedentInSnippet": { + "description": "When pressing shift-tab on the first placeholder, prevent the line from being dedented", + "type": "boolean", + "default": false } } } From be3d8002136494daa8e9b3868dc159f66e6893e5 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sat, 20 Apr 2019 22:34:19 +1000 Subject: [PATCH 68/77] extract transaction logic --- spec/snippets-spec.js | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 8d118253..6c983968 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -26,6 +26,16 @@ describe('Snippets extension', () => { atom.commands.dispatch(editorElement, 'snippets:previous-tab-stop') } + // NOTE: Required for undo behaviour to work as if text was + // typed. Time based, so possibly flaky in CI. Increase + // the grouping interval (ms) to be more lenient with the + // grouping. + function editorTransact (input) { + editor.transact(300, () => { + editor.insertText(input) + }) + } + beforeEach(() => { spyOn(Snippets, 'loadAll') spyOn(Snippets, 'getUserSnippetsPath').andReturn('') @@ -630,19 +640,12 @@ describe('Snippets extension', () => { }) it('bundles the transform mutations along with the original manual mutation for the purposes of undo and redo', () => { - // NOTE: Most likely spec here to fail on CI, as it is time based - const transactionDuration = 300 - editor.setText('t12') simulateTabKeyEvent() - editor.transact(transactionDuration, () => { - editor.insertText('i') - }) + editorTransact('i') expect(editor.getText()).toBe("[i][/i]") - editor.transact(transactionDuration, () => { - editor.insertText('mg src') - }) + editorTransact('mg src') expect(editor.getText()).toBe("[img src][/img]") editor.undo() @@ -772,21 +775,14 @@ describe('Snippets extension', () => { }) it('bundles transform-induced mutations into a single history entry along with their triggering edit, even across multiple snippets', () => { - // NOTE: Another likely spec to fail on CI, as it is time based - const transactionDuration = 300 - editor.setText('t14\nt14') editor.setCursorBufferPosition([1, 3]) editor.addCursorAtBufferPosition([0, 3]) simulateTabKeyEvent() - editor.transact(transactionDuration, () => { - editor.insertText('testing') - }) + editorTransact('testing') simulateTabKeyEvent() - editor.transact(transactionDuration, () => { - editor.insertText('AGAIN') - }) + editorTransact('AGAIN') editor.undo() expect(editor.lineTextForBufferRow(0)).toBe('testing TESTING testing ANOTHER another ') @@ -879,22 +875,16 @@ describe('Snippets extension', () => { describe('when the editor is not a pane item (regression)', () => { it('handles tab stops correctly', () => { - // NOTE: Possibly flaky test - const transactionDuration = 300 editor.setText('t2') simulateTabKeyEvent() - editor.transact(transactionDuration, () => { - editor.insertText('ABC') - }) + editorTransact('ABC') expect(editor.lineTextForBufferRow(1)).toEqual('go here first:(ABC)') editor.undo() editor.undo() expect(editor.getText()).toBe('t2') simulateTabKeyEvent() - editor.transact(transactionDuration, () => { - editor.insertText('ABC') - }) + editorTransact('ABC') expect(editor.getText()).toContain('go here first:(ABC)') }) }) From 9a0e477c188bf331d5583676412f9bc87b2400bf Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Tue, 18 Jun 2019 21:23:48 +1000 Subject: [PATCH 69/77] don't add implicit endTabStop if already ending with tabstop --- lib/snippet.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/snippet.js b/lib/snippet.js index b2595520..2d471adb 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -30,11 +30,11 @@ module.exports = class Snippet { column: params.startPosition.column } - stringifyContent(this.bodyTree, params, acc) + let endsWithTabstop = stringifyContent(this.bodyTree, params, acc) addTabstopsForUnknownVariables(acc.unknownVariables, acc.tabStopList) - if (!acc.tabStopList.hasEndStop && atom.config.get('snippets.implicitEndTabstop')) { + if (!acc.tabStopList.hasEndStop && !endsWithTabstop && atom.config.get('snippets.implicitEndTabstop')) { const endRange = new Range([acc.row, acc.column], [acc.row, acc.column]) acc.tabStopList.findOrCreate({index: Infinity, snippet: this}).addInsertion({range: endRange}) } @@ -55,7 +55,9 @@ function addTabstopsForUnknownVariables (unknowns, tabStopList) { } function stringifyContent (content = [], params, acc) { + let endsWithTabstop for (let node of content) { + endsWithTabstop = true if (node.index !== undefined) { // only tabstops and choices have an index if (node.choice !== undefined) { stringifyChoice(node, params, acc) @@ -69,7 +71,9 @@ function stringifyContent (content = [], params, acc) { continue } stringifyText(node, params, acc) + endsWithTabstop = false } + return endsWithTabstop } function stringifyTabstop (node, params, acc) { From d812cf0813333c8ac0447a589226b9b47cc4a7de Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 17 Jun 2019 18:46:01 -0500 Subject: [PATCH 70/77] Bring the changes from #281 into #288. --- lib/insertion.js | 3 +- lib/snippet-expansion.js | 191 ++++++++++++++++++++++++++----- lib/snippet.js | 20 +++- spec/fixtures/test-snippets.cson | 10 ++ spec/snippets-spec.js | 38 ++++++ 5 files changed, 231 insertions(+), 31 deletions(-) diff --git a/lib/insertion.js b/lib/insertion.js index b80d2ea6..0519d783 100644 --- a/lib/insertion.js +++ b/lib/insertion.js @@ -1,9 +1,10 @@ const {transformWithSubstitution} = require('./util') class Insertion { - constructor ({range, substitution, choices=[], transformResolver}) { + constructor ({range, substitution, references, choices=[], transformResolver}) { this.range = range this.substitution = substitution + this.references = references if (substitution && substitution.replace === undefined) { substitution.replace = '' } diff --git a/lib/snippet-expansion.js b/lib/snippet-expansion.js index b2b8af04..928b2ea9 100644 --- a/lib/snippet-expansion.js +++ b/lib/snippet-expansion.js @@ -12,7 +12,21 @@ module.exports = class SnippetExpansion { this.cursor = cursor this.snippets = snippets this.subscriptions = new CompositeDisposable - this.tabStopMarkers = [] + this.insertionsByIndex = [] + this.markersForInsertions = new Map() + + // The index of the active tab stop. We don't use the tab stop's own + // numbering here; we renumber them consecutively starting at 0 in the order + // in which they should be visited. So `$1` will always be index `0` in the + // above list, and `$0` (if present) will always be the last index. + this.tabStopIndex = null + + // If, say, tab stop 4's placeholder references tab stop 2, then tab stop + // 4's insertion goes into this map as a "related" insertion to tab stop 2. + // We need to keep track of this because tab stop 4's marker will need to be + // replaced while 2 is the active index. + this.relatedInsertionsByIndex = new Map() + this.selections = [this.cursor.selection] const startPosition = this.cursor.selection.getBufferRange().start @@ -29,8 +43,11 @@ module.exports = class SnippetExpansion { const tabStops = this.tabStopList.toArray() this.ignoringBufferChanges(() => { + // Insert the snippet body at the cursor. const newRange = this.cursor.selection.insertText(body, {autoIndent: false}) if (this.tabStopList.length > 0) { + // Listen for cursor changes so we can decide whether to keep the + // snippet active or terminate it. this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event))) this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed())) this.placeTabStopMarkers(tabStops) @@ -49,9 +66,16 @@ module.exports = class SnippetExpansion { cursorMoved ({oldBufferPosition, newBufferPosition, textChanged}) { if (this.settingTabStop || (textChanged && !this.isUndoingOrRedoing)) { return } - const itemWithCursor = this.tabStopMarkers[this.tabStopIndex].find(item => item.marker.getBufferRange().containsPoint(newBufferPosition)) - if (itemWithCursor && !itemWithCursor.insertion.isTransformation()) { return } + const insertionAtCursor = this.insertionsByIndex[this.tabStopIndex].find((insertion) => { + let marker = this.markersForInsertions.get(insertion) + return marker.getBufferRange().containsPoint(newBufferPosition) + }) + + if (insertionAtCursor && !insertionAtCursor.isTransformation()) { + // The cursor is still inside an insertion. Return so that the snippet doesn't get destroyed. + return + } // we get here if there is no item for the current index with the cursor if (this.isUndoingOrRedoing) { @@ -95,31 +119,35 @@ module.exports = class SnippetExpansion { } applyAllTransformations () { - this.tabStopMarkers.forEach((item, index) => this.applyTransformations(index)) + this.insertionsByIndex.forEach((_, index) => this.applyTransformations(index)) } applyTransformations (tabStop) { - const items = [...this.tabStopMarkers[tabStop]] - if (items.length === 0) { return } + const insertions = [...this.insertionsByIndex[tabStop]] + if (insertions.length === 0) { return } - const primary = items.shift() - const primaryRange = primary.marker.getBufferRange() + const primaryInsertion = insertions.shift() + const primaryRange = this.markersForInsertions.get(primaryInsertion).getBufferRange() const inputText = this.editor.getTextInBufferRange(primaryRange) this.ignoringBufferChanges(() => { - for (const item of items) { - const {marker, insertion} = item - var range = marker.getBufferRange() - + for (const insertion of insertions) { // Don't transform mirrored tab stops. They have their own cursors, so // mirroring happens automatically. if (!insertion.isTransformation()) { continue } + let marker = this.markersForInsertions.get(insertion) + let range = marker.getBufferRange() + var outputText = insertion.transform(inputText) this.editor.setTextInBufferRange(range, outputText) // this.editor.buffer.groupLastChanges() + // Manually adjust the marker's range rather than rely on its internal + // heuristics. (We don't have to worry about whether it's been + // invalidated because setting its buffer range implicitly marks it as + // valid again.) const newRange = new Range( range.start, range.start.traverse(getEndpointOfText(outputText)) @@ -130,42 +158,125 @@ module.exports = class SnippetExpansion { } placeTabStopMarkers (tabStops) { + // Tab stops within a snippet refer to one another by their external index + // (1 for $1, 3 for $3, etc.). We respect the order of these tab stops, but + // we renumber them starting at 0 and using consecutive numbers. + // + // Luckily, we don't need to convert between the two numbering systems very + // often. But we do have to build a map from external index to our internal + // index. We do this in a separate loop so that the table is complete before + // we need to consult it in the following loop. + let indexTable = {} + Object.keys(tabStops).forEach((key, index) => { + let tabStop = tabStops[key] + indexTable[tabStop.index] = index + }) const markerLayer = this.getMarkerLayer(this.editor) + let tabStopIndex = -1 for (const tabStop of tabStops) { + tabStopIndex++ const {insertions} = tabStop - const markers = [] - if (!tabStop.isValid()) { continue } for (const insertion of insertions) { - const marker = markerLayer.markBufferRange(insertion.range) - markers.push({ - index: markers.length, - marker, - insertion - }) + const {range: {start, end}} = insertion + let references = null + if (insertion.references) { + references = insertion.references.map(external => indexTable[external]) + } + // Since this method is only called once at the beginning of a snippet + // expansion, we know that 0 is about to be the active tab stop. + let shouldBeInclusive = (tabStopIndex === 0) || (references && references.includes(0)) + + const marker = markerLayer.markBufferRange(insertion.range, {exclusive: !shouldBeInclusive}) + this.markersForInsertions.set(insertion, marker) + if (references) { + let relatedInsertions = this.relatedInsertionsByIndex.get(tabStopIndex) || [] + relatedInsertions.push(insertion) + this.relatedInsertionsByIndex.set(tabStopIndex, relatedInsertions) + } } - this.tabStopMarkers.push(markers) + // Since we have to replace markers in place when we change their + // exclusivity, we'll store them in a map keyed on the insertion itself. + this.insertionsByIndex[tabStopIndex] = insertions } this.setTabStopIndex(0) this.applyAllTransformations() } + // When two insertion markers are directly adjacent to one another, and the + // cursor is placed right at the border between them, the marker that should + // "claim" the newly-typed content will vary based on context. + // + // All else being equal, that content should get added to the marker (if any) + // whose tab stop is active (or the marker whose tab stop's placeholder + // references an active tab stop). The `exclusive` setting controls whether a + // marker grows to include content added at its edge. + // + // So we need to revisit the markers whenever the active tab stop changes, + // figure out which ones need to be touched, and replace them with markers + // that have the settings we need. + adjustTabStopMarkers (oldIndex, newIndex) { + // Take all the insertions belonging to the newly-active tab stop (and all + // insertions whose placeholders reference the newly-active tab stop) and + // change their markers to be inclusive. + let insertionsForNewIndex = [ + ...this.insertionsByIndex[newIndex], + ...(this.relatedInsertionsByIndex.get(newIndex) || []) + ] + + for (let insertion of insertionsForNewIndex) { + this.replaceMarkerForInsertion(insertion, {exclusive: false}) + } + + // Take all the insertions whose markers were made inclusive when they + // became active and restore their original marker settings. + let insertionsForOldIndex = [ + ...this.insertionsByIndex[oldIndex], + ...(this.relatedInsertionsByIndex.get(oldIndex) || []) + ] + + for (let insertion of insertionsForOldIndex) { + this.replaceMarkerForInsertion(insertion, {exclusive: true}) + } + } + + replaceMarkerForInsertion (insertion, settings) { + let marker = this.markersForInsertions.get(insertion) + + // If the marker is invalid or destroyed, return it as-is. Other methods + // need to know if a marker has been invalidated or destroyed, and there's + // no case in which we'd need to change the settings on such a marker + // anyway. + if (!marker.isValid() || marker.isDestroyed()) { + return marker + } + + // Otherwise, create a new marker with an identical range and the specified + // settings. + let range = marker.getBufferRange() + let replacement = this.getMarkerLayer(this.editor).markBufferRange(range, settings) + + marker.destroy() + this.markersForInsertions.set(insertion, replacement) + return replacement + } + goToNextTabStop () { const nextIndex = this.tabStopIndex + 1 // if we have an endstop (implicit ends have already been added) it will be the last one - if (nextIndex === this.tabStopMarkers.length - 1 && this.tabStopList.hasEndStop) { + if (nextIndex === this.insertionsByIndex.length - 1 && this.tabStopList.hasEndStop) { const succeeded = this.setTabStopIndex(nextIndex) this.destroy() return {succeeded, isDestroyed: true} } // we are not at the end, and the next is not the endstop; just go to next stop - if (nextIndex < this.tabStopMarkers.length) { + if (nextIndex < this.insertionsByIndex.length) { const succeeded = this.setTabStopIndex(nextIndex) if (succeeded) { return {succeeded, isDestroyed: false} } return this.goToNextTabStop() @@ -190,19 +301,29 @@ module.exports = class SnippetExpansion { } setTabStopIndex (tabStopIndex) { + let oldIndex = this.tabStopIndex this.tabStopIndex = tabStopIndex + + // Set a flag before we move any selections so that our change handlers + // will know that the movements were initiated by us. this.settingTabStop = true + + // Keep track of whether we replaced any selections or cursors. let markerSelected = false - const items = this.tabStopMarkers[this.tabStopIndex] - if (items.length === 0) { return false } + let insertions = this.insertionsByIndex[this.tabStopIndex] + if (insertions.length === 0) { return false } const ranges = [] let hasTransforms = false - for (const item of items) { - const {marker, insertion} = item + // Go through the active tab stop's markers to figure out where to place + // cursors and/or selections. + for (const insertion of insertions) { + const marker = this.markersForInsertions.get(insertion) if (marker.isDestroyed() || !marker.isValid()) { continue } if (insertion.isTransformation()) { + // Set a flag for later, but skip transformation insertions because + // they don't get their own cursors. hasTransforms = true continue } @@ -210,6 +331,8 @@ module.exports = class SnippetExpansion { } if (ranges.length > 0) { + // We have new selections to apply. Reuse existing selections if + // possible, and destroy the unused ones if we already have too many. for (const selection of this.selections.slice(ranges.length)) { selection.destroy() } this.selections = this.selections.slice(0, ranges.length) for (let i = 0; i < ranges.length; i++) { @@ -223,20 +346,30 @@ module.exports = class SnippetExpansion { this.selections.push(newSelection) } } + // We placed at least one selection, so this tab stop was successfully + // set. Update our return value. markerSelected = true } this.settingTabStop = false // If this snippet has at least one transform, we need to observe changes // made to the editor so that we can update the transformed tab stops. - if (hasTransforms) { this.snippets.observeEditor(this.editor) } + if (hasTransforms) { + this.snippets.observeEditor(this.editor) + } else { + this.snippets.stopObservingEditor(this.editor) + } + + if (oldIndex !== null) { + this.adjustTabStopMarkers(oldIndex, this.tabStopIndex) + } return markerSelected } destroy () { this.subscriptions.dispose() - this.tabStopMarkers = [] + this.insertionsByIndex = [] } getMarkerLayer () { diff --git a/lib/snippet.js b/lib/snippet.js index 2d471adb..8e55541d 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -3,6 +3,19 @@ const TabStopList = require('./tab-stop-list') const {transformWithSubstitution} = require('./util') const {VariableResolver, TransformResolver} = require('./resolvers') +const tabStopsReferencedWithinTabStopContent = (segment) => { + let results = [] + for (let item of segment) { + if (item.index) { + results.push( + item.index, + ...tabStopsReferencedWithinTabStopContent(item.content) + ) + } + } + return new Set(results) +} + module.exports = class Snippet { constructor(params) { this.name = params.name @@ -80,8 +93,13 @@ function stringifyTabstop (node, params, acc) { const index = node.index === 0 ? Infinity : node.index const start = new Point(acc.row, acc.column) stringifyContent(node.content, params, acc) + let referencedTabStops = tabStopsReferencedWithinTabStopContent(node.content) const range = new Range(start, [acc.row, acc.column]) - acc.tabStopList.findOrCreate({index, snippet: this}).addInsertion({range, substitution: node.substitution}) + acc.tabStopList.findOrCreate({index, snippet: this}).addInsertion({ + range, + substitution: node.substitution, + references: [...referencedTabStops] + }) } function stringifyChoice (node, params, acc) { diff --git a/spec/fixtures/test-snippets.cson b/spec/fixtures/test-snippets.cson index 72bfe318..24729144 100644 --- a/spec/fixtures/test-snippets.cson +++ b/spec/fixtures/test-snippets.cson @@ -143,3 +143,13 @@ 'has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step': prefix: 't18' body: '// $1\n// ${1/./=/g}' + "has two tab stops adjacent to one another": + prefix: 't19' + body: """ + ${2:bar}${3:baz} + """ + "has several adjacent tab stops, one of which has a placeholder with a reference to another tab stop at its edge": + prefix: 't20' + body: """ + ${1:foo}${2:bar}${3:baz $1}$4 + """ diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 8d118253..5e018210 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -734,6 +734,44 @@ describe('Snippets extension', () => { }) }) + describe('when the snippet has two adjacent tab stops', () => { + it('ensures insertions are treated as part of the active tab stop', () => { + editor.setText('t19') + editor.setCursorScreenPosition([0, 3]) + simulateTabKeyEvent() + expect(editor.getText()).toBe('barbaz') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]) + editor.insertText('w') + expect(editor.getText()).toBe('wbaz') + editor.insertText('at') + expect(editor.getText()).toBe('watbaz') + simulateTabKeyEvent() + expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 6]]) + editor.insertText('foo') + expect(editor.getText()).toBe('watfoo') + }) + }) + + describe('when the snippet has a placeholder with a tabstop mirror at its edge', () => { + it('allows the associated marker to include the inserted text', () => { + editor.setText('t20') + editor.setCursorScreenPosition([0, 3]) + simulateTabKeyEvent() + expect(editor.getText()).toBe('foobarbaz ') + expect(editor.getCursors().length).toBe(2) + let selections = editor.getSelections() + expect(selections[0].getBufferRange()).toEqual([[0, 0], [0, 3]]) + expect(selections[1].getBufferRange()).toEqual([[0, 10], [0, 10]]) + editor.insertText('nah') + expect(editor.getText()).toBe('nahbarbaz nah') + simulateTabKeyEvent() + editor.insertText('meh') + simulateTabKeyEvent() + editor.insertText('yea') + expect(editor.getText()).toBe('nahmehyea') + }) + }) + describe('when there are multiple cursors', () => { describe('when the cursors share a common snippet prefix', () => { it('expands the snippet for all cursors and allows simultaneous editing', () => { From 70ef2f9bf439696aa85206c53a852f18a343cca7 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Tue, 18 Jun 2019 22:35:32 +1000 Subject: [PATCH 71/77] remove unused import --- lib/snippet.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/snippet.js b/lib/snippet.js index 8e55541d..cd6bbe4e 100644 --- a/lib/snippet.js +++ b/lib/snippet.js @@ -1,7 +1,6 @@ const {Point, Range} = require('atom') const TabStopList = require('./tab-stop-list') const {transformWithSubstitution} = require('./util') -const {VariableResolver, TransformResolver} = require('./resolvers') const tabStopsReferencedWithinTabStopContent = (segment) => { let results = [] From 417da46b44139f2d5d62af018dc3e36637c4b766 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Wed, 19 Jun 2019 00:29:19 +1000 Subject: [PATCH 72/77] add back two removed tests --- spec/fixtures/test-snippets.cson | 8 +++++ spec/snippets-spec.js | 58 +++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/spec/fixtures/test-snippets.cson b/spec/fixtures/test-snippets.cson index 24729144..36bd2dda 100644 --- a/spec/fixtures/test-snippets.cson +++ b/spec/fixtures/test-snippets.cson @@ -56,6 +56,14 @@ ${2:placeholder ending second line} ''' + 'tab stops at beginning and then end of snippet': + prefix: 't6b' + body: '$1expanded$0' + + 'tab stops at end and then beginning of snippet': + prefix: 't6c' + body: '$0expanded$1' + 'contains empty lines': prefix: 't7' body: ''' diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 5e018210..464af98e 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -505,20 +505,54 @@ describe('Snippets extension', () => { }) describe('when a previous snippet expansion has just been undone', () => { - it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { - editor.setText('t6\n') - editor.setCursorBufferPosition([0, 2]) - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe('first line') - expect(editor.lineTextForBufferRow(1)).toBe(' placeholder ending second line') + describe('when the tab stops appear in the middle of the snippet', () => { + it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { + editor.setText('t6\n') + editor.setCursorBufferPosition([0, 2]) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('first line') + expect(editor.lineTextForBufferRow(1)).toBe(' placeholder ending second line') - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe('t6') - expect(editor.lineTextForBufferRow(1)).toBe('') + editor.undo() + expect(editor.lineTextForBufferRow(0)).toBe('t6') + expect(editor.lineTextForBufferRow(1)).toBe('') - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe('first line') - expect(editor.lineTextForBufferRow(1)).toBe(' placeholder ending second line') + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('first line') + expect(editor.lineTextForBufferRow(1)).toBe(' placeholder ending second line') + }) + }) + + describe('when the tab stops appear at the beginning and then the end of snippet', () => { + it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { + editor.insertText('t6b\n') + editor.setCursorBufferPosition([0, 3]) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('expanded') + + editor.undo() + expect(editor.lineTextForBufferRow(0)).toBe('t6b') + + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('expanded') + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('when the tab stops appear at the end and then the beginning of snippet', () => { + it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { + editor.insertText('t6c\n') + editor.setCursorBufferPosition ([0, 3]) + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('expanded') + + editor.undo() + expect(editor.lineTextForBufferRow(0)).toBe('t6c') + + simulateTabKeyEvent() + expect(editor.lineTextForBufferRow(0)).toBe('expanded') + expect(editor.getCursorBufferPosition()).toEqual([0, 8]) + }) }) }) From 7f0576786ef792e1ad8b20409ebf594fd72a59f2 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Wed, 19 Jun 2019 00:34:23 +1000 Subject: [PATCH 73/77] remove unused imports --- spec/snippets-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js index 464af98e..fe7b39ad 100644 --- a/spec/snippets-spec.js +++ b/spec/snippets-spec.js @@ -1,9 +1,7 @@ const path = require('path') -const fs = require('fs') const temp = require('temp').track() const CSON = require('season') const Snippets = require('../lib/snippets') -const {TextEditor} = require('atom') describe('Snippets extension', () => { let editorElement From 874aafadb05f9b7c2becef70a3c92fe6171da011 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Wed, 19 Jun 2019 00:39:57 +1000 Subject: [PATCH 74/77] move bundled snippets to the conventional folder --- lib/snippets.js | 2 +- {lib => snippets}/snippets.cson | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {lib => snippets}/snippets.cson (100%) diff --git a/lib/snippets.js b/lib/snippets.js index 2dd2b9c8..20b4525e 100644 --- a/lib/snippets.js +++ b/lib/snippets.js @@ -117,7 +117,7 @@ module.exports = { }, loadBundledSnippets (callback) { - const bundledSnippetsPath = CSON.resolve(path.join(getPackageRoot(), 'lib', 'snippets')) + const bundledSnippetsPath = CSON.resolve(path.join(getPackageRoot(), 'snippets', 'snippets')) this.loadSnippetsFile(bundledSnippetsPath, snippets => { const snippetsByPath = {} snippetsByPath[bundledSnippetsPath] = snippets diff --git a/lib/snippets.cson b/snippets/snippets.cson similarity index 100% rename from lib/snippets.cson rename to snippets/snippets.cson From 64606bc1e547ddf3726d1b88700b47173992ee93 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sun, 30 Jun 2019 19:51:23 -0700 Subject: [PATCH 75/77] alternate if-else syntax --- lib/snippet-body.pegjs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index 04b5e5b1..c78408d4 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -53,7 +53,7 @@ regexString = regex:(escaped / [^/])* { replace = (format / replacetext)* -format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatWithIf / formatWithIfElse / formatWithElse / formatEscape +format = formatWithIfAlt / formatWithIfElseAlt / simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatWithIf / formatWithIfElse / formatWithElse / formatEscape simpleFormat = '$' index:int { return {backreference: makeInteger(index)} @@ -71,6 +71,10 @@ formatWithIf = '${' index:int ':+' iftext:(ifElseText / '') '}' { return {backreference: makeInteger(index), iftext: iftext} } +formatWithIfAlt = '(?' index:int ':' iftext:(ifElseTextAlt / '') ')' { + return {backreference: makeInteger(index), iftext: iftext} +} + formatWithElse = '${' index:int (':-' / ':') elsetext:(ifElseText / '') '}' { return {backreference: makeInteger(index), elsetext: elsetext} } @@ -79,6 +83,10 @@ formatWithIfElse = '${' index:int ':?' iftext:nonColonText ':' elsetext:(ifElseT return {backreference: makeInteger(index), iftext: iftext, elsetext: elsetext} } +formatWithIfElseAlt = '(?' index:int ':' iftext:nonColonText ':' elsetext:(ifElseTextAlt / '') ')' { + return {backreference: makeInteger(index), iftext: iftext, elsetext: elsetext} +} + nonColonText = text:('\\:' { return ':' } / escaped / [^:])* { return text.join('') } @@ -158,3 +166,7 @@ nonCloseBraceText = text:(escaped / !tabstop !variable !choice char:[^}] { retur ifElseText = text:(escaped / char:[^}] { return char })+ { return text.join('') } + +ifElseTextAlt = text:(escaped / char:[^)] { return char })+ { + return text.join('') +} From 6222c429793fd223cb594062208497874a14e861 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sun, 30 Jun 2019 20:03:44 -0700 Subject: [PATCH 76/77] reorder alternate --- lib/snippet-body.pegjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/snippet-body.pegjs b/lib/snippet-body.pegjs index c78408d4..cc54819f 100644 --- a/lib/snippet-body.pegjs +++ b/lib/snippet-body.pegjs @@ -53,7 +53,7 @@ regexString = regex:(escaped / [^/])* { replace = (format / replacetext)* -format = formatWithIfAlt / formatWithIfElseAlt / simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatWithIf / formatWithIfElse / formatWithElse / formatEscape +format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatWithIf / formatWithIfElse / formatWithElse / formatEscape / formatWithIfElseAlt / formatWithIfAlt simpleFormat = '$' index:int { return {backreference: makeInteger(index)} From 0deaa1d06bb30a64191b14a75d0231b6cdb239a3 Mon Sep 17 00:00:00 2001 From: Benjamin Gray Date: Sun, 30 Jun 2019 20:17:51 -0700 Subject: [PATCH 77/77] adjust spec --- spec/body-parser-spec.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/body-parser-spec.js b/spec/body-parser-spec.js index d01255a3..5240e0ee 100644 --- a/spec/body-parser-spec.js +++ b/spec/body-parser-spec.js @@ -364,7 +364,7 @@ describe('Snippet Body Parser', () => { it('handles a snippet with a transformed variable', () => { expectMatch( - 'module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}}', + 'module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/\\u$1/g}}', [ 'module ', { @@ -376,10 +376,8 @@ describe('Snippet Body Parser', () => { substitution: { find: /(?:\A|_)([A-Za-z0-9]+)(?:\.rb)?/g, replace: [ - '(?2::', {escape: 'u'}, - {backreference: 1}, - ')' + {backreference: 1} ] } }