Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
a1ff497
Start on LSP syntax parser
Aerijo Feb 5, 2019
bb03336
Update grammar for 95% compliance
Aerijo Feb 6, 2019
08019c5
Ony escape necessary characters
Aerijo Feb 6, 2019
675b7d4
return regex
Aerijo Feb 6, 2019
df90657
formatting and tweaks
Aerijo Feb 6, 2019
2cf43b8
remove unused file
Aerijo Feb 6, 2019
bd5700b
Merge branch 'master' of github.com:Aerijo/snippets into lsp-snippets
Aerijo Feb 6, 2019
8f022fc
Add variable support
Aerijo Feb 7, 2019
1c42d1b
Hah, pass all specs
Aerijo Feb 7, 2019
6cc9446
Pass editor and cursor to variable resolver
Aerijo Feb 7, 2019
9d32670
remove debugger
Aerijo Feb 7, 2019
c60b3ad
Resolve all the things
Aerijo Feb 7, 2019
4be086c
Fix selection contents & line numbers
Aerijo Feb 7, 2019
4863ca6
remove debugger
Aerijo Feb 7, 2019
139e08a
Apply (some) variable transforms
Aerijo Feb 8, 2019
fc7d227
Refrain from assigning to constant
Aerijo Feb 8, 2019
b9d967a
Take previous advice
Aerijo Feb 8, 2019
f9a2d72
...
Aerijo Feb 8, 2019
7c5cc77
Resolve global replacement properly
Aerijo Feb 8, 2019
5746276
Informative error when failing to transform variable
Aerijo Feb 8, 2019
85107f4
Actually throw an error
Aerijo Feb 8, 2019
c9b2996
general idea
Aerijo Feb 8, 2019
82625e4
Mockup choices presentation
Aerijo Feb 8, 2019
31fe36e
don't save config change
Aerijo Feb 9, 2019
3a29fd9
Refactor
Aerijo Feb 10, 2019
443bd62
remove debugger and pass entire acc to variable resolver
Aerijo Feb 10, 2019
49f7e05
Add default params fallback
Aerijo Feb 10, 2019
6542e76
Add in implicit end stop by default
Aerijo Feb 10, 2019
e002906
Fix implicit end stops & catch transform errors
Aerijo Feb 11, 2019
81dbaa0
rename driver service
Aerijo Feb 11, 2019
c64242e
:art:
Aerijo Feb 11, 2019
b797802
fix resolver service
Aerijo Feb 11, 2019
75a0641
Consistent transform behaviour
Aerijo Feb 11, 2019
ce4d69e
Start messing with history grouping
Aerijo Feb 11, 2019
417dd93
Hack in forced undo barrier
Aerijo Feb 13, 2019
bea05d9
extract to method
Aerijo Feb 13, 2019
b156bb2
Maybe this?
Aerijo Feb 13, 2019
77e8bb1
Use checkpoint instead
Aerijo Feb 13, 2019
30cd9fb
Try more stuff
Aerijo Feb 13, 2019
7cb2c8f
Improvements
Aerijo Feb 13, 2019
b8730af
clean end tabstop logic
Aerijo Feb 13, 2019
0544e51
also make it correct -_-
Aerijo Feb 13, 2019
231e617
Support if/else syntax
Aerijo Feb 13, 2019
c86b630
Fix tabstop position on undo/redo
Aerijo Feb 14, 2019
09d866d
comment the change
Aerijo Feb 14, 2019
cf1441f
Expand and pass around resolver classes
Aerijo Feb 14, 2019
b6bbe9f
remove debugger
Aerijo Feb 14, 2019
80fb5ca
fix typo
Aerijo Feb 17, 2019
a6d94e9
Merge branch 'master' of https://github.com/atom/snippets into lsp-sn…
Aerijo Apr 16, 2019
3c721d7
implicit returns strike again
Aerijo Apr 16, 2019
e7432ed
Style & logic tweaks
Aerijo Apr 16, 2019
dae7f48
use single quotes
Aerijo Apr 16, 2019
df13d06
but not for JSON files
Aerijo Apr 16, 2019
0d8d640
translate snippet tests
Aerijo Apr 17, 2019
ba1bc74
Don't load language-javascript
Aerijo Apr 17, 2019
cea1151
vaguely better undo/redo behaviour
Aerijo Apr 18, 2019
c77cf00
fix
Aerijo Apr 19, 2019
01db360
Remove debugger & tweak gotoPreviousStop
Aerijo Apr 19, 2019
ac06db5
bug fixes
Aerijo Apr 19, 2019
b847db1
Specs and bug fixes
Aerijo Apr 19, 2019
1cb4511
Test misc cases
Aerijo Apr 19, 2019
7934443
remove console log
Aerijo Apr 19, 2019
429a127
start testing snippet body text
Aerijo Apr 19, 2019
5f20163
relax undefined check
Aerijo Apr 20, 2019
43bae26
Make tests more robust and flexible
Aerijo Apr 20, 2019
260d437
more specs
Aerijo Apr 20, 2019
60f3af0
:art:
Aerijo Apr 20, 2019
687b174
fix undo specs
Aerijo Apr 20, 2019
fb4b3e9
Fix yet another undo bug
Aerijo Apr 20, 2019
be3d800
extract transaction logic
Aerijo Apr 20, 2019
9a0e477
don't add implicit endTabStop if already ending with tabstop
Aerijo Jun 18, 2019
d812cf0
Bring the changes from #281 into #288.
savetheclocktower Jun 17, 2019
70ef2f9
remove unused import
Aerijo Jun 18, 2019
77eaef8
Merge branch 'master' of github.com:atom/snippets into lsp-snippets
Aerijo Jun 18, 2019
417da46
add back two removed tests
Aerijo Jun 18, 2019
7f05767
remove unused imports
Aerijo Jun 18, 2019
874aafa
move bundled snippets to the conventional folder
Aerijo Jun 18, 2019
00ffa45
Merge branch 'lsp-snippets' of https://github.com/Aerijo/snippets int…
Aerijo Jul 1, 2019
64606bc
alternate if-else syntax
Aerijo Jul 1, 2019
6222c42
reorder alternate
Aerijo Jul 1, 2019
0deaa1d
adjust spec
Aerijo Jul 1, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 118 additions & 59 deletions lib/snippet-body.pegjs
Original file line number Diff line number Diff line change
@@ -1,82 +1,141 @@
{
// 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 / bodyContentText)* { return content; }
bodyContentText = text:bodyContentChar+ { return text.join(''); }
bodyContentChar = escaped / !tabStop char:. { return char; }

escaped = '\\' char:. { return char; }
tabStop = tabStopWithTransformation / tabStopWithPlaceholder / tabStopWithoutPlaceholder / simpleTabStop
bodyContent = content:(tabstop / choice / variable / text)* { return content; }

innerBodyContent = content:(tabstop / choice / variable / nonCloseBraceText)* { return content; }

simpleTabStop = '$' index:[0-9]+ {
return { index: parseInt(index.join("")), content: [] };
tabstop = simpleTabstop / tabstopWithoutPlaceholder / tabstopWithPlaceholder / tabstopWithTransform

simpleTabstop = '$' index:int {
return { index: makeInteger(index), content: [] }
}
tabStopWithoutPlaceholder = '${' index:[0-9]+ '}' {
return { index: parseInt(index.join("")), content: [] };

tabstopWithoutPlaceholder = '${' index:int '}' {
return { index: makeInteger(index), content: [] }
}
tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' {
return { index: parseInt(index.join("")), content: content };

tabstopWithPlaceholder = '${' index:int ':' content:innerBodyContent '}' {
return { index: makeInteger(index), content: content }
}
tabStopWithTransformation = '${' index:[0-9]+ substitution:transformationSubstitution '}' {

tabstopWithTransform = '${' index:int substitution:transform '}' {
return {
index: parseInt(index.join(""), 10),
index: makeInteger(index),
content: [],
substitution: substitution
};
}
}

placeholderContent = content:(tabStop / placeholderContentText / variable )* { return flatten(content); }
placeholderContentText = text:placeholderContentChar+ { return coalesce(text); }
placeholderContentChar = escaped / placeholderVariableReference / !tabStop !variable char:[^}] { return char; }
choice = '${' index:int '|' choice:choicecontents '|}' {
return { index: makeInteger(index), choice: choice }
}

placeholderVariableReference = '$' digit:[0-9]+ {
return { index: parseInt(digit.join(""), 10), content: [] };
choicecontents = elem:choicetext rest:(',' val:choicetext { return val } )* {
return [elem, ...rest]
}

variable = '${' variableContent '}' {
return ''; // we eat variables and do nothing with them for now
choicetext = choicetext:(choiceEscaped / [^|,] / barred:('|' &[^}]) { return barred.join('') } )+ {
return choicetext.join('')
}
variableContent = content:(variable / variableContentText)* { return content; }
variableContentText = text:variableContentChar+ { return text.join(''); }
variableContentChar = !variable char:('\\}' / [^}]) { return char; }

escapedForwardSlash = pair:'\\/' { return pair; }
// Transform is applied when tabbed off
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment accurate? Looks like the transformation code is largely unchanged, and current behavior is to apply the transform immediately and upon every change to the input text.

Copy link
Copy Markdown
Contributor Author

@Aerijo Aerijo Feb 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VS Code applies when tabbed off. But I guess leaving the existing behaviour also works.

I think I left that comment to remind me that whatever you're typing while filling it out doesn't matter, as only what you leave it as matters.

transform = '/' regex:regexString '/' replace:replace '/' flags:flags {
return { find: new RegExp(regex, flags), replace: replace }
}

// 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] };
regexString = regex:(escaped / [^/])* {
return regex.join('')
}

formatString = content:(formatStringEscape / formatStringReference / escapedForwardSlash / [^/])+ {
return content;
replace = (format / replacetext)*

// TODO: Support conditionals
format = simpleFormat / formatWithoutPlaceholder / formatWithCaseTransform / formatEscape

simpleFormat = '$' index:int {
return { backreference: makeInteger(index) }
}
// 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 };

formatWithoutPlaceholder = '${' index:int '}' {
return { backreference: makeInteger(index) }
}

formatWithCaseTransform = '${' index:int ':' casetransform:casetransform '}' {
return { backreference: makeInteger(index), transform: casetransform }
}

formatEscape = '\\' flag:[ULulErn$] {
return { escape: flag }
}

casetransform = '/' type:[a-zA-Z]* {
return type.join('')
}

replacetext = replacetext:(!formatEscape 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:. {
switch (char) {
case '$':
case '\\':
case '\x7D': // back brace; PEGjs would treat it as the JS scope end though
return char
default:
return '\\' + char
}
}

choiceEscaped = '\\' char:. {
switch (char) {
case '$':
case '\\':
case '\x7D':
case '|':
case ',':
return char
default:
return '\\' + char
}
}

flags = flags:[a-z]* {
return flags.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('')
}
18 changes: 13 additions & 5 deletions lib/snippet-expansion.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -14,22 +14,27 @@ 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({ 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}`)

// TODO: Remove concept of "body"; build on the fly each time to resolve variables and their transformations

tabStops = tabStops.map(tabStop => tabStop.copyWithIndent(indent))
}

this.editor.transact(() => {
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)
Expand Down Expand Up @@ -103,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,
Expand Down Expand Up @@ -152,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.snippet.tabStopList.hasEndStop) {
if (this.tabStopList.hasEndStop) {
this.destroy()
return false
} else {
Expand Down
102 changes: 84 additions & 18 deletions lib/snippet.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,121 @@ const {Range} = require('atom')
const TabStopList = require('./tab-stop-list')

module.exports = class Snippet {
constructor({name, prefix, bodyText, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree}) {
constructor({name, prefix, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree, bodyText, 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.bodyText = bodyText
this.variableResolver = variableResolver
}

extractTabStops (bodyTree) {
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 (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) {
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
}
}
}
}
}

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 }
}
}

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
}
Loading