├── .coffeelintignore
├── .github
├── no-response.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .pairs
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── LICENSE.md
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── coffeelint.json
├── keymaps
├── snippets-1.cson
└── snippets-2.cson
├── lib
├── editor-store.js
├── helpers.js
├── insertion.js
├── snippet-body-parser.js
├── snippet-body.pegjs
├── snippet-expansion.js
├── snippet-history-provider.js
├── snippet.js
├── snippets-available.js
├── snippets.cson
├── snippets.js
├── tab-stop-list.js
└── tab-stop.js
├── menus
└── snippets.cson
├── package-lock.json
├── package.json
└── spec
├── body-parser-spec.js
├── fixtures
├── package-with-broken-snippets
│ └── snippets
│ │ ├── .hidden-file
│ │ └── invalid.json
├── package-with-snippets
│ └── snippets
│ │ ├── .hidden-file
│ │ ├── junk-file
│ │ └── test.cson
└── sample.js
├── insertion-spec.js
├── snippet-loading-spec.js
└── snippets-spec.js
/.coffeelintignore:
--------------------------------------------------------------------------------
1 | spec/fixtures
2 |
--------------------------------------------------------------------------------
/.github/no-response.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-no-response - https://github.com/probot/no-response
2 |
3 | # Number of days of inactivity before an issue is closed for lack of response
4 | daysUntilClose: 28
5 |
6 | # Label requiring a response
7 | responseRequiredLabel: more-information-needed
8 |
9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable.
10 | closeComment: >
11 | This issue has been automatically closed because there has been no response
12 | to our request for more information from the original author. With only the
13 | information that is currently in the issue, we don't have enough information
14 | to take action. Please reach out if you have or find the answers we need so
15 | that we can investigate further.
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | env:
6 | CI: true
7 |
8 | jobs:
9 | Test:
10 | strategy:
11 | matrix:
12 | os: [ubuntu-latest, macos-latest]
13 | channel: [stable, beta]
14 | runs-on: ${{ matrix.os }}
15 | steps:
16 | - uses: actions/checkout@v1
17 | - uses: UziTech/action-setup-atom@v2
18 | with:
19 | version: ${{ matrix.channel }}
20 | - name: Install dependencies
21 | run: apm install
22 | - name: Run tests
23 | run: atom --test spec
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.pairs:
--------------------------------------------------------------------------------
1 | pairs:
2 | ns: Nathan Sobo; nathan
3 | cj: Corey Johnson; cj
4 | dg: David Graham; dgraham
5 | ks: Kevin Sawicki; kevin
6 | jc: Jerry Cheung; jerry
7 | bl: Brian Lopez; brian
8 | jp: Justin Palmer; justin
9 | gt: Garen Torikian; garen
10 | mc: Matt Colyer; mcolyer
11 | bo: Ben Ogle; benogle
12 | jr: Jason Rudolph; jasonrudolph
13 | jl: Jessica Lord; jlord
14 | email:
15 | domain: github.com
16 | #global: true
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | See the [Atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md)
2 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | ### Prerequisites
10 |
11 | * [ ] Put an X between the brackets on this line if you have done all of the following:
12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode
13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/
14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq
15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom
16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages
17 |
18 | ### Description
19 |
20 | [Description of the issue]
21 |
22 | ### Steps to Reproduce
23 |
24 | 1. [First Step]
25 | 2. [Second Step]
26 | 3. [and so on...]
27 |
28 | **Expected behavior:** [What you expect to happen]
29 |
30 | **Actual behavior:** [What actually happens]
31 |
32 | **Reproduces how often:** [What percentage of the time does it reproduce?]
33 |
34 | ### Versions
35 |
36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running.
37 |
38 | ### Additional Information
39 |
40 | Any additional information, configuration or data that might be necessary to reproduce the issue.
41 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 GitHub Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Requirements
2 |
3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.
4 | * All new code requires tests to ensure against regressions
5 |
6 | ### Description of the Change
7 |
8 |
13 |
14 | ### Alternate Designs
15 |
16 |
17 |
18 | ### Benefits
19 |
20 |
21 |
22 | ### Possible Drawbacks
23 |
24 |
25 |
26 | ### Applicable Issues
27 |
28 |
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/)
2 | # Snippets package
3 | [](https://travis-ci.org/atom/snippets) [](https://ci.appveyor.com/project/Atom/snippets/branch/master) [](https://david-dm.org/atom/snippets)
4 |
5 | Expand snippets matching the current prefix with tab in Atom.
6 |
7 | To add your own snippets, select the _Atom > Snippets..._ menu option if you're using macOS, or the _File > Snippets..._ menu option if you're using Windows, or the _Edit > Snippets..._ menu option if you are using Linux.
8 |
9 | ## Snippet Format
10 |
11 | Snippets files are stored in a package's `snippets/` folder and also loaded from `~/.atom/snippets.cson`. They can be either `.json` or `.cson` file types.
12 |
13 | ```coffee
14 | '.source.js':
15 | 'console.log':
16 | 'prefix': 'log'
17 | 'body': 'console.log(${1:"crash"});$2'
18 | ```
19 |
20 | The outermost keys are the selectors where these snippets should be active, prefixed with a period (`.`) (details below).
21 |
22 | The next level of keys are the snippet names.
23 |
24 | Under each snippet name is a `prefix` that should trigger the snippet and a `body` to insert when the snippet is triggered.
25 |
26 | `$` followed by a number are the tabs stops which can be cycled between by pressing tab once a snippet has been triggered.
27 |
28 | The above example adds a `log` snippet to JavaScript files that would expand to.
29 |
30 | ```js
31 | console.log("crash");
32 | ```
33 |
34 | The string `"crash"` would be initially selected and pressing tab again would place the cursor after the `;`
35 |
36 | ### Optional parameters
37 | These parameters are meant to provide extra information about your snippet to [autocomplete-plus](https://github.com/atom/autocomplete-plus/wiki/Provider-API).
38 |
39 | * `leftLabel` will add text to the left part of the autocomplete results box.
40 | * `leftLabelHTML` will overwrite what's in `leftLabel` and allow you to use a bit of CSS such as `color`.
41 | * `rightLabelHTML`. By default, in the right part of the results box you will see the name of the snippet. When using `rightLabelHTML` the name of the snippet will no longer be displayed, and you will be able to use a bit of CSS.
42 | * `description` will add text to a description box under the autocomplete results list.
43 | * `descriptionMoreURL` URL to the documentation of the snippet.
44 |
45 | 
46 |
47 | Example:
48 | ```coffee
49 | '.source.js':
50 | 'console.log':
51 | 'prefix': 'log'
52 | 'body': 'console.log(${1:"crash"});$2'
53 | 'description': 'Output data to the console'
54 | 'rightLabelHTML': 'JS'
55 | ```
56 |
57 | ### Determining the correct scope for a snippet
58 |
59 | The outmost key of a snippet is the "scope" that you want the descendent snippets to be available in. The key should be prefixed with a period (`text.html.basic` => `.text.html.basic`). You can find out the correct scope by opening the Settings (cmd-, on macOS) and selecting the corresponding *Language [xxx]* package, e.g. for *Language Html*:
60 |
61 | 
62 |
63 | If it's difficult to determine the package handling the file type in question (for example, for `.md`-documents), you can also proceed as following. Put your cursor in a file in which you want the snippet to be available, open the [Command Palette](https://github.com/atom/command-palette)
64 | (cmd-shift-p), and run the `Editor: Log Cursor Scope` command. This will trigger a notification which will contain a list of scopes. The first scope that's listed is the scope for that language. Here are some examples: `source.coffee`, `text.plain`, `text.html.basic`.
65 |
66 | ### Snippet syntax
67 |
68 | This package supports a subset of the features of TextMate snippets, [documented here](http://manual.macromates.com/en/snippets#transformations).
69 |
70 | The following features are not yet supported:
71 |
72 | * Variables
73 | * Interpolated shell code
74 | * Conditional insertions in transformations
75 |
76 | ### Multi-line Snippet Body
77 |
78 | You can also use multi-line syntax using `"""` for larger templates:
79 |
80 | ```coffee
81 | '.source.js':
82 | 'if, else if, else':
83 | 'prefix': 'ieie'
84 | 'body': """
85 | if (${1:true}) {
86 | $2
87 | } else if (${3:false}) {
88 | $4
89 | } else {
90 | $5
91 | }
92 | """
93 | ```
94 |
95 | ### Escaping Characters
96 |
97 | Including a literal closing brace inside the text provided by a snippet's tab stop will close that tab stop early. To prevent that, escape the brace with two backslashes, like so:
98 |
99 | ```coffee
100 | '.source.js':
101 | 'function':
102 | 'prefix': 'funct'
103 | 'body': """
104 | ${1:function () {
105 | statements;
106 | \\}
107 | this line is also included in the snippet tab;
108 | }
109 | """
110 | ```
111 |
112 | ### Multiple snippets for the same scope
113 |
114 | Snippets for the same scope must be placed within the same key. See [this section of the Atom Flight Manual](http://flight-manual.atom.io/using-atom/sections/basic-customization/#configuring-with-cson) for more information.
115 |
--------------------------------------------------------------------------------
/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "max_line_length": {
3 | "level": "ignore"
4 | },
5 | "no_empty_param_list": {
6 | "level": "error"
7 | },
8 | "arrow_spacing": {
9 | "level": "error"
10 | },
11 | "no_interpolation_in_single_quotes": {
12 | "level": "error"
13 | },
14 | "no_debugger": {
15 | "level": "error"
16 | },
17 | "prefer_english_operator": {
18 | "level": "error"
19 | },
20 | "colon_assignment_spacing": {
21 | "spacing": {
22 | "left": 0,
23 | "right": 1
24 | },
25 | "level": "error"
26 | },
27 | "braces_spacing": {
28 | "spaces": 0,
29 | "level": "error"
30 | },
31 | "spacing_after_comma": {
32 | "level": "error"
33 | },
34 | "no_stand_alone_at": {
35 | "level": "error"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/keymaps/snippets-1.cson:
--------------------------------------------------------------------------------
1 | 'atom-text-editor:not([mini])':
2 | 'tab': 'snippets:expand'
3 |
--------------------------------------------------------------------------------
/keymaps/snippets-2.cson:
--------------------------------------------------------------------------------
1 | # it's critical that these bindings be loaded after those snippets-1 so they
2 | # are later in the cascade, hence breaking the keymap into 2 files
3 |
4 | 'atom-text-editor:not([mini])':
5 | 'tab': 'snippets:next-tab-stop'
6 | 'shift-tab': 'snippets:previous-tab-stop'
7 |
--------------------------------------------------------------------------------
/lib/editor-store.js:
--------------------------------------------------------------------------------
1 | const SnippetHistoryProvider = require('./snippet-history-provider')
2 |
3 | class EditorStore {
4 | constructor (editor) {
5 | this.editor = editor
6 | this.buffer = this.editor.getBuffer()
7 | this.observer = null
8 | this.checkpoint = null
9 | this.expansions = []
10 | this.existingHistoryProvider = null
11 | }
12 |
13 | getExpansions () {
14 | return this.expansions
15 | }
16 |
17 | setExpansions (list) {
18 | this.expansions = list
19 | }
20 |
21 | clearExpansions () {
22 | this.expansions = []
23 | }
24 |
25 | addExpansion (snippetExpansion) {
26 | this.expansions.push(snippetExpansion)
27 | }
28 |
29 | observeHistory (delegates) {
30 | if (this.existingHistoryProvider == null) {
31 | this.existingHistoryProvider = this.buffer.historyProvider
32 | }
33 |
34 | const newProvider = SnippetHistoryProvider(this.existingHistoryProvider, delegates)
35 | this.buffer.setHistoryProvider(newProvider)
36 | }
37 |
38 | stopObservingHistory (editor) {
39 | if (this.existingHistoryProvider == null) { return }
40 | this.buffer.setHistoryProvider(this.existingHistoryProvider)
41 | this.existingHistoryProvider = null
42 | }
43 |
44 | observe (callback) {
45 | if (this.observer != null) { this.observer.dispose() }
46 | this.observer = this.buffer.onDidChangeText(callback)
47 | }
48 |
49 | stopObserving () {
50 | if (this.observer == null) { return false }
51 | this.observer.dispose()
52 | this.observer = null
53 | return true
54 | }
55 |
56 | makeCheckpoint () {
57 | const existing = this.checkpoint
58 | if (existing) {
59 | this.buffer.groupChangesSinceCheckpoint(existing)
60 | }
61 | this.checkpoint = this.buffer.createCheckpoint()
62 | }
63 | }
64 |
65 | EditorStore.store = new WeakMap()
66 | EditorStore.findOrCreate = function (editor) {
67 | if (!this.store.has(editor)) {
68 | this.store.set(editor, new EditorStore(editor))
69 | }
70 | return this.store.get(editor)
71 | }
72 |
73 | module.exports = EditorStore
74 |
--------------------------------------------------------------------------------
/lib/helpers.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import path from 'path'
4 |
5 | export function getPackageRoot() {
6 | const {resourcePath} = atom.getLoadSettings()
7 | const currentFileWasRequiredFromSnapshot = !path.isAbsolute(__dirname)
8 | if (currentFileWasRequiredFromSnapshot) {
9 | return path.join(resourcePath, 'node_modules', 'snippets')
10 | } else {
11 | return path.resolve(__dirname, '..')
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/insertion.js:
--------------------------------------------------------------------------------
1 | const ESCAPES = {
2 | u: (flags) => {
3 | flags.lowercaseNext = false
4 | flags.uppercaseNext = true
5 | },
6 | l: (flags) => {
7 | flags.uppercaseNext = false
8 | flags.lowercaseNext = true
9 | },
10 | U: (flags) => {
11 | flags.lowercaseAll = false
12 | flags.uppercaseAll = true
13 | },
14 | L: (flags) => {
15 | flags.uppercaseAll = false
16 | flags.lowercaseAll = true
17 | },
18 | E: (flags) => {
19 | flags.uppercaseAll = false
20 | flags.lowercaseAll = false
21 | },
22 | r: (flags, result) => {
23 | result.push('\\r')
24 | },
25 | n: (flags, result) => {
26 | result.push('\\n')
27 | },
28 | $: (flags, result) => {
29 | result.push('$')
30 | }
31 | }
32 |
33 | function transformText (str, flags) {
34 | if (flags.uppercaseAll) {
35 | return str.toUpperCase()
36 | } else if (flags.lowercaseAll) {
37 | return str.toLowerCase()
38 | } else if (flags.uppercaseNext) {
39 | flags.uppercaseNext = false
40 | return str.replace(/^./, s => s.toUpperCase())
41 | } else if (flags.lowercaseNext) {
42 | return str.replace(/^./, s => s.toLowerCase())
43 | }
44 | return str
45 | }
46 |
47 | class Insertion {
48 | constructor ({ range, substitution, references }) {
49 | this.range = range
50 | this.substitution = substitution
51 | this.references = references
52 | if (substitution) {
53 | if (substitution.replace === undefined) {
54 | substitution.replace = ''
55 | }
56 | this.replacer = this.makeReplacer(substitution.replace)
57 | }
58 | }
59 |
60 | isTransformation () {
61 | return !!this.substitution
62 | }
63 |
64 | makeReplacer (replace) {
65 | return function replacer (...match) {
66 | let flags = {
67 | uppercaseAll: false,
68 | lowercaseAll: false,
69 | uppercaseNext: false,
70 | lowercaseNext: false
71 | }
72 | replace = [...replace]
73 | let result = []
74 | replace.forEach(token => {
75 | if (typeof token === 'string') {
76 | result.push(transformText(token, flags))
77 | } else if (token.escape) {
78 | ESCAPES[token.escape](flags, result)
79 | } else if (token.backreference) {
80 | let transformed = transformText(match[token.backreference], flags)
81 | result.push(transformed)
82 | }
83 | })
84 | return result.join('')
85 | }
86 | }
87 |
88 | transform (input) {
89 | let { substitution } = this
90 | if (!substitution) { return input }
91 | return input.replace(substitution.find, this.replacer)
92 | }
93 | }
94 |
95 | module.exports = Insertion
96 |
--------------------------------------------------------------------------------
/lib/snippet-body-parser.js:
--------------------------------------------------------------------------------
1 | let parser
2 | try {
3 | parser = require('./snippet-body')
4 | } catch (error) {
5 | const {allowUnsafeEval} = require('loophole')
6 | const fs = require('fs-plus')
7 | const PEG = require('pegjs')
8 |
9 | const grammarSrc = fs.readFileSync(require.resolve('./snippet-body.pegjs'), 'utf8')
10 | parser = null
11 | allowUnsafeEval(() => parser = PEG.buildParser(grammarSrc))
12 | }
13 |
14 | module.exports = parser
15 |
--------------------------------------------------------------------------------
/lib/snippet-body.pegjs:
--------------------------------------------------------------------------------
1 | {
2 | // Joins all consecutive strings in a collection without clobbering any
3 | // non-string members.
4 | function coalesce (parts) {
5 | const result = [];
6 | for (let i = 0; i < parts.length; i++) {
7 | const part = parts[i];
8 | const ri = result.length - 1;
9 | if (typeof part === 'string' && typeof result[ri] === 'string') {
10 | result[ri] = result[ri] + part;
11 | } else {
12 | result.push(part);
13 | }
14 | }
15 | return result;
16 | }
17 |
18 | function flatten (parts) {
19 | return parts.reduce(function (flat, rest) {
20 | return flat.concat(Array.isArray(rest) ? flatten(rest) : rest);
21 | }, []);
22 | }
23 | }
24 | bodyContent = content:(tabStop / bodyContentText)* { return content; }
25 | bodyContentText = text:bodyContentChar+ { return text.join(''); }
26 | bodyContentChar = escaped / !tabStop char:. { return char; }
27 |
28 | escaped = '\\' char:. { return char; }
29 | tabStop = tabStopWithTransformation / tabStopWithPlaceholder / tabStopWithoutPlaceholder / simpleTabStop
30 |
31 | simpleTabStop = '$' index:[0-9]+ {
32 | return { index: parseInt(index.join("")), content: [] };
33 | }
34 | tabStopWithoutPlaceholder = '${' index:[0-9]+ '}' {
35 | return { index: parseInt(index.join("")), content: [] };
36 | }
37 | tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' {
38 | return { index: parseInt(index.join("")), content: content };
39 | }
40 | tabStopWithTransformation = '${' index:[0-9]+ substitution:transformationSubstitution '}' {
41 | return {
42 | index: parseInt(index.join(""), 10),
43 | content: [],
44 | substitution: substitution
45 | };
46 | }
47 |
48 | placeholderContent = content:(tabStop / placeholderContentText / variable )* { return flatten(content); }
49 | placeholderContentText = text:placeholderContentChar+ { return coalesce(text); }
50 | placeholderContentChar = escaped / placeholderVariableReference / !tabStop !variable char:[^}] { return char; }
51 |
52 | placeholderVariableReference = '$' digit:[0-9]+ {
53 | return { index: parseInt(digit.join(""), 10), content: [] };
54 | }
55 |
56 | variable = '${' variableContent '}' {
57 | return ''; // we eat variables and do nothing with them for now
58 | }
59 | variableContent = content:(variable / variableContentText)* { return content; }
60 | variableContentText = text:variableContentChar+ { return text.join(''); }
61 | variableContentChar = !variable char:('\\}' / [^}]) { return char; }
62 |
63 | escapedForwardSlash = pair:'\\/' { return pair; }
64 |
65 | // A pattern and replacement for a transformed tab stop.
66 | transformationSubstitution = '/' find:(escapedForwardSlash / [^/])* '/' replace:formatString* '/' flags:[imy]* {
67 | let reFind = new RegExp(find.join(''), flags.join('') + 'g');
68 | return { find: reFind, replace: replace[0] };
69 | }
70 |
71 | formatString = content:(formatStringEscape / formatStringReference / escapedForwardSlash / [^/])+ {
72 | return content;
73 | }
74 | // Backreferencing a substitution. Different from a tab stop.
75 | formatStringReference = '$' digits:[0-9]+ {
76 | return { backreference: parseInt(digits.join(''), 10) };
77 | };
78 | // One of the special control flags in a format string for case folding and
79 | // other tasks.
80 | formatStringEscape = '\\' flag:[ULulErn$] {
81 | return { escape: flag };
82 | }
83 |
--------------------------------------------------------------------------------
/lib/snippet-expansion.js:
--------------------------------------------------------------------------------
1 | const {CompositeDisposable, Range, Point} = require('atom')
2 |
3 | module.exports = class SnippetExpansion {
4 | constructor(snippet, editor, cursor, snippets) {
5 | this.settingTabStop = false
6 | this.isIgnoringBufferChanges = false
7 | this.onUndoOrRedo = this.onUndoOrRedo.bind(this)
8 | this.snippet = snippet
9 | this.editor = editor
10 | this.cursor = cursor
11 | this.snippets = snippets
12 | this.subscriptions = new CompositeDisposable
13 | this.selections = [this.cursor.selection]
14 |
15 | // Holds the `Insertion` instance corresponding to each tab stop marker. We
16 | // don't use the tab stop's own numbering here; we renumber them
17 | // consecutively starting at 0 in the order in which they should be
18 | // visited. So `$1` (if present) will always be at index `0`, and `$0` (if
19 | // present) will always be the last index.
20 | this.insertionsByIndex = []
21 |
22 | // Each insertion has a corresponding marker. We keep them in a map so we
23 | // can easily reassociate an insertion with its new marker when we destroy
24 | // its old one.
25 | this.markersForInsertions = new Map()
26 |
27 | // The index of the active tab stop.
28 | this.tabStopIndex = null
29 |
30 | // If, say, tab stop 4's placeholder references tab stop 2, then tab stop
31 | // 4's insertion goes into this map as a "related" insertion to tab stop 2.
32 | // We need to keep track of this because tab stop 4's marker will need to
33 | // be replaced while 2 is the active index.
34 | this.relatedInsertionsByIndex = new Map()
35 |
36 | const startPosition = this.cursor.selection.getBufferRange().start
37 | let {body, tabStopList} = this.snippet
38 | let tabStops = tabStopList.toArray()
39 |
40 | let indent = this.editor.lineTextForBufferRow(startPosition.row).match(/^\s*/)[0]
41 | if (this.snippet.lineCount > 1 && indent) {
42 | // Add proper leading indentation to the snippet
43 | body = body.replace(/\n/g, `\n${indent}`)
44 |
45 | tabStops = tabStops.map(tabStop => tabStop.copyWithIndent(indent))
46 | }
47 |
48 | this.editor.transact(() => {
49 | this.ignoringBufferChanges(() => {
50 | this.editor.transact(() => {
51 | // Insert the snippet body at the cursor.
52 | const newRange = this.cursor.selection.insertText(body, {autoIndent: false})
53 | if (this.snippet.tabStopList.length > 0) {
54 | // Listen for cursor changes so we can decide whether to keep the
55 | // snippet active or terminate it.
56 | this.subscriptions.add(this.cursor.onDidChangePosition(event => this.cursorMoved(event)))
57 | this.subscriptions.add(this.cursor.onDidDestroy(() => this.cursorDestroyed()))
58 | this.placeTabStopMarkers(startPosition, tabStops)
59 | this.snippets.addExpansion(this.editor, this)
60 | this.editor.normalizeTabsInBufferRange(newRange)
61 | }
62 | })
63 | })
64 | })
65 | }
66 |
67 | // Set a flag on undo or redo so that we know not to re-apply transforms.
68 | // They're already accounted for in the history.
69 | onUndoOrRedo (isUndo) {
70 | this.isUndoingOrRedoing = true
71 | }
72 |
73 | cursorMoved ({oldBufferPosition, newBufferPosition, textChanged}) {
74 | if (this.settingTabStop || textChanged) { return }
75 | const insertionAtCursor = this.insertionsByIndex[this.tabStopIndex].find(insertion => {
76 | let marker = this.markersForInsertions.get(insertion)
77 | return marker.getBufferRange().containsPoint(newBufferPosition)
78 | })
79 |
80 | if (insertionAtCursor && !insertionAtCursor.isTransformation()) { return }
81 |
82 | this.destroy()
83 | }
84 |
85 | cursorDestroyed () { if (!this.settingTabStop) { this.destroy() } }
86 |
87 | textChanged (event) {
88 | if (this.isIgnoringBufferChanges) { return }
89 |
90 | // Don't try to alter the buffer if all we're doing is restoring a
91 | // snapshot from history.
92 | if (this.isUndoingOrRedoing) {
93 | this.isUndoingOrRedoing = false
94 | return
95 | }
96 |
97 | this.applyTransformations(this.tabStopIndex)
98 | }
99 |
100 | ignoringBufferChanges (callback) {
101 | const wasIgnoringBufferChanges = this.isIgnoringBufferChanges
102 | this.isIgnoringBufferChanges = true
103 | callback()
104 | this.isIgnoringBufferChanges = wasIgnoringBufferChanges
105 | }
106 |
107 | applyAllTransformations () {
108 | this.editor.transact(() => {
109 | this.insertionsByIndex.forEach((insertion, index) =>
110 | this.applyTransformations(index))
111 | })
112 | }
113 |
114 | applyTransformations (tabStopIndex) {
115 | const insertions = [...this.insertionsByIndex[tabStopIndex]]
116 | if (insertions.length === 0) { return }
117 |
118 | const primaryInsertion = insertions.shift()
119 | const primaryRange = this.markersForInsertions.get(primaryInsertion).getBufferRange()
120 | const inputText = this.editor.getTextInBufferRange(primaryRange)
121 |
122 | this.ignoringBufferChanges(() => {
123 | for (const [index, insertion] of insertions.entries()) {
124 | // Don't transform mirrored tab stops. They have their own cursors, so
125 | // mirroring happens automatically.
126 | if (!insertion.isTransformation()) { continue }
127 |
128 | var marker = this.markersForInsertions.get(insertion)
129 | var range = marker.getBufferRange()
130 |
131 | var outputText = insertion.transform(inputText)
132 | this.editor.transact(() => this.editor.setTextInBufferRange(range, outputText))
133 |
134 | // Manually adjust the marker's range rather than rely on its internal
135 | // heuristics. (We don't have to worry about whether it's been
136 | // invalidated because setting its buffer range implicitly marks it as
137 | // valid again.)
138 | const newRange = new Range(
139 | range.start,
140 | range.start.traverse(new Point(0, outputText.length))
141 | )
142 | marker.setBufferRange(newRange)
143 | }
144 | })
145 | }
146 |
147 | placeTabStopMarkers (startPosition, tabStops) {
148 | // Tab stops within a snippet refer to one another by their external index
149 | // (1 for $1, 3 for $3, etc.). We respect the order of these tab stops, but
150 | // we renumber them starting at 0 and using consecutive numbers.
151 | //
152 | // Luckily, we don't need to convert between the two numbering systems very
153 | // often. But we do have to build a map from external index to our internal
154 | // index. We do this in a separate loop so that the table is complete
155 | // before we need to consult it in the following loop.
156 | const indexTable = {}
157 | for (let [index, tabStop] of tabStops.entries()) {
158 | indexTable[tabStop.index] = index
159 | }
160 |
161 | for (let [index, tabStop] of tabStops.entries()) {
162 | const {insertions} = tabStop
163 |
164 | if (!tabStop.isValid()) { continue }
165 |
166 | for (const insertion of insertions) {
167 | const {range} = insertion
168 | const {start, end} = range
169 | let references = null
170 | if (insertion.references) {
171 | references = insertion.references.map(external => indexTable[external])
172 | }
173 | // Since this method is called only once at the beginning of a snippet expansion, we know that 0 is about to be the active tab stop.
174 | const shouldBeInclusive = (index === 0) || (references && references.includes(0))
175 | const marker = this.getMarkerLayer(this.editor).markBufferRange([
176 | startPosition.traverse(start),
177 | startPosition.traverse(end)
178 | ], { exclusive: !shouldBeInclusive })
179 | // Now that we've created these markers, we need to store them in a
180 | // data structure because they'll need to be deleted and re-created
181 | // when their exclusivity changes.
182 | this.markersForInsertions.set(insertion, marker)
183 |
184 | if (references) {
185 | const relatedInsertions = this.relatedInsertionsByIndex.get(index) || []
186 | relatedInsertions.push(insertion)
187 | this.relatedInsertionsByIndex.set(index, relatedInsertions)
188 | }
189 | }
190 | this.insertionsByIndex[index] = insertions
191 | }
192 |
193 | this.setTabStopIndex(0)
194 | this.applyAllTransformations()
195 | }
196 |
197 | // When two insertion markers are directly adjacent to one another, and the
198 | // cursor is placed right at the border between them, the marker that should
199 | // "claim" the newly typed content will vary based on context.
200 | //
201 | // All else being equal, that content should get added to the marker (if any)
202 | // whose tab stop is active, or else the marker whose tab stop's placeholder
203 | // references an active tab stop. The `exclusive` setting on a marker
204 | // controls whether that marker grows to include content added at its edge.
205 | //
206 | // So we need to revisit the markers whenever the active tab stop changes,
207 | // figure out which ones need to be touched, and replace them with markers
208 | // that have the settings we need.
209 | adjustTabStopMarkers (oldIndex, newIndex) {
210 | // Take all the insertions whose markers were made inclusive when they
211 | // became active and restore their original marker settings.
212 | const insertionsForOldIndex = [
213 | ...this.insertionsByIndex[oldIndex],
214 | ...(this.relatedInsertionsByIndex.get(oldIndex) || [])
215 | ]
216 |
217 | for (let insertion of insertionsForOldIndex) {
218 | this.replaceMarkerForInsertion(insertion, {exclusive: true})
219 | }
220 |
221 | // Take all the insertions belonging to the newly active tab stop (and all
222 | // insertions whose placeholders reference the newly active tab stop) and
223 | // change their markers to be inclusive.
224 | const insertionsForNewIndex = [
225 | ...this.insertionsByIndex[newIndex],
226 | ...(this.relatedInsertionsByIndex.get(newIndex) || [])
227 | ]
228 |
229 | for (let insertion of insertionsForNewIndex) {
230 | this.replaceMarkerForInsertion(insertion, {exclusive: false})
231 | }
232 | }
233 |
234 | replaceMarkerForInsertion (insertion, settings) {
235 | const marker = this.markersForInsertions.get(insertion)
236 |
237 | // If the marker is invalid or destroyed, return it as-is. Other methods
238 | // need to know if a marker has been invalidated or destroyed, and we have
239 | // no need to change the settings on such markers anyway.
240 | if (!marker.isValid() || marker.isDestroyed()) {
241 | return marker
242 | }
243 |
244 | // Otherwise, create a new marker with an identical range and the specified
245 | // settings.
246 | const range = marker.getBufferRange()
247 | const replacement = this.getMarkerLayer(this.editor).markBufferRange(range, settings)
248 |
249 | marker.destroy()
250 | this.markersForInsertions.set(insertion, replacement)
251 | return replacement
252 | }
253 |
254 | goToNextTabStop () {
255 | const nextIndex = this.tabStopIndex + 1
256 | if (nextIndex < this.insertionsByIndex.length) {
257 | if (this.setTabStopIndex(nextIndex)) {
258 | return true
259 | } else {
260 | return this.goToNextTabStop()
261 | }
262 | } else {
263 | // The user has tabbed past the last tab stop. If the last tab stop is a
264 | // $0, we shouldn't move the cursor any further.
265 | if (this.snippet.tabStopList.hasEndStop) {
266 | this.destroy()
267 | return false
268 | } else {
269 | const succeeded = this.goToEndOfLastTabStop()
270 | this.destroy()
271 | return succeeded
272 | }
273 | }
274 | }
275 |
276 | goToPreviousTabStop () {
277 | if (this.tabStopIndex > 0) { this.setTabStopIndex(this.tabStopIndex - 1) }
278 | }
279 |
280 | setTabStopIndex (newIndex) {
281 | const oldIndex = this.tabStopIndex
282 | this.tabStopIndex = newIndex
283 | // Set a flag before moving any selections so that our change handlers know
284 | // that the movements were initiated by us.
285 | this.settingTabStop = true
286 | // Keep track of whether we placed any selections or cursors.
287 | let markerSelected = false
288 |
289 | const insertions = this.insertionsByIndex[this.tabStopIndex]
290 | if (insertions.length === 0) { return false }
291 |
292 | const ranges = []
293 | this.hasTransforms = false
294 |
295 | // Go through the active tab stop's markers to figure out where to place
296 | // cursors and/or selections.
297 | for (const insertion of insertions) {
298 | const marker = this.markersForInsertions.get(insertion)
299 | if (marker.isDestroyed()) { continue }
300 | if (!marker.isValid()) { continue }
301 | if (insertion.isTransformation()) {
302 | // Set a flag for later, but skip transformation insertions because
303 | // they don't get their own cursors.
304 | this.hasTransforms = true
305 | continue
306 | }
307 | ranges.push(marker.getBufferRange())
308 | }
309 |
310 | if (ranges.length > 0) {
311 | // We have new selections to apply. Reuse existing selections if
312 | // possible, destroying the unused ones if we already have too many.
313 | for (const selection of this.selections.slice(ranges.length)) { selection.destroy() }
314 | this.selections = this.selections.slice(0, ranges.length)
315 | for (let i = 0; i < ranges.length; i++) {
316 | const range = ranges[i]
317 | if (this.selections[i]) {
318 | this.selections[i].setBufferRange(range)
319 | } else {
320 | const newSelection = this.editor.addSelectionForBufferRange(range)
321 | this.subscriptions.add(newSelection.cursor.onDidChangePosition(event => this.cursorMoved(event)))
322 | this.subscriptions.add(newSelection.cursor.onDidDestroy(() => this.cursorDestroyed()))
323 | this.selections.push(newSelection)
324 | }
325 | }
326 | // We placed at least one selection, so this tab stop was successfully
327 | // set.
328 | markerSelected = true
329 | }
330 |
331 | this.settingTabStop = false
332 | // If this snippet has at least one transform, we need to observe changes
333 | // made to the editor so that we can update the transformed tab stops.
334 | if (this.hasTransforms) {
335 | this.snippets.observeEditor(this.editor)
336 | } else {
337 | this.snippets.stopObservingEditor(this.editor)
338 | }
339 |
340 | if (oldIndex !== null) {
341 | this.adjustTabStopMarkers(oldIndex, newIndex)
342 | }
343 |
344 | return markerSelected
345 | }
346 |
347 | goToEndOfLastTabStop () {
348 | const size = this.insertionsByIndex.length
349 | if (size === 0) { return }
350 | const insertions = this.insertionsByIndex[size - 1]
351 | if (insertions.length === 0) { return }
352 | const lastMarker = this.markersForInsertions.get(insertions[insertions.length - 1])
353 |
354 | if (lastMarker.isDestroyed()) {
355 | return false
356 | } else {
357 | this.seditor.setCursorBufferPosition(lastMarker.getEndBufferPosition())
358 | return true
359 | }
360 | }
361 |
362 | destroy () {
363 | this.subscriptions.dispose()
364 | this.getMarkerLayer(this.editor).clear()
365 | this.insertionsByIndex = []
366 | this.relatedInsertionsByIndex = new Map()
367 | this.markersForInsertions = new Map();
368 | this.snippets.stopObservingEditor(this.editor)
369 | this.snippets.clearExpansions(this.editor)
370 | }
371 |
372 | getMarkerLayer () {
373 | return this.snippets.findOrCreateMarkerLayer(this.editor)
374 | }
375 |
376 | restore (editor) {
377 | this.editor = editor
378 | this.snippets.addExpansion(this.editor, this)
379 | }
380 | }
381 |
--------------------------------------------------------------------------------
/lib/snippet-history-provider.js:
--------------------------------------------------------------------------------
1 | function wrap (manager, callbacks) {
2 | let klass = new SnippetHistoryProvider(manager)
3 | return new Proxy(manager, {
4 | get (target, name) {
5 | if (name in callbacks) {
6 | callbacks[name]()
7 | }
8 | return name in klass ? klass[name] : target[name]
9 | }
10 | })
11 | }
12 |
13 | class SnippetHistoryProvider {
14 | constructor (manager) {
15 | this.manager = manager
16 | }
17 |
18 | undo (...args) {
19 | return this.manager.undo(...args)
20 | }
21 |
22 | redo (...args) {
23 | return this.manager.redo(...args)
24 | }
25 | }
26 |
27 | module.exports = wrap
28 |
--------------------------------------------------------------------------------
/lib/snippet.js:
--------------------------------------------------------------------------------
1 | const {Range} = require('atom')
2 | const TabStopList = require('./tab-stop-list')
3 |
4 | function tabStopsReferencedWithinTabStopContent (segment) {
5 | const results = []
6 | for (const item of segment) {
7 | if (item.index) {
8 | results.push(item.index, ...tabStopsReferencedWithinTabStopContent(item.content))
9 | }
10 | }
11 | return new Set(results)
12 | }
13 |
14 | module.exports = class Snippet {
15 | constructor({name, prefix, bodyText, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyTree}) {
16 | this.name = name
17 | this.prefix = prefix
18 | this.bodyText = bodyText
19 | this.description = description
20 | this.descriptionMoreURL = descriptionMoreURL
21 | this.rightLabelHTML = rightLabelHTML
22 | this.leftLabel = leftLabel
23 | this.leftLabelHTML = leftLabelHTML
24 | this.tabStopList = new TabStopList(this)
25 | this.body = this.extractTabStops(bodyTree)
26 | }
27 |
28 | extractTabStops (bodyTree) {
29 | const bodyText = []
30 | let row = 0
31 | let column = 0
32 |
33 | // recursive helper function; mutates vars above
34 | let extractTabStops = bodyTree => {
35 | for (const segment of bodyTree) {
36 | if (segment.index != null) {
37 | let {index, content, substitution} = segment
38 | if (index === 0) { index = Infinity; }
39 | const start = [row, column]
40 | extractTabStops(content)
41 | const referencedTabStops = tabStopsReferencedWithinTabStopContent(content)
42 | const range = new Range(start, [row, column])
43 | const tabStop = this.tabStopList.findOrCreate({
44 | index,
45 | snippet: this
46 | })
47 | tabStop.addInsertion({
48 | range,
49 | substitution,
50 | references: Array.from(referencedTabStops)
51 | })
52 | } else if (typeof segment === 'string') {
53 | bodyText.push(segment)
54 | var segmentLines = segment.split('\n')
55 | column += segmentLines.shift().length
56 | let nextLine
57 | while ((nextLine = segmentLines.shift()) != null) {
58 | row += 1
59 | column = nextLine.length
60 | }
61 | }
62 | }
63 | }
64 |
65 | extractTabStops(bodyTree)
66 | this.lineCount = row + 1
67 | this.insertions = this.tabStopList.getInsertions()
68 |
69 | return bodyText.join('')
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/snippets-available.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import _ from 'underscore-plus'
4 | import SelectListView from 'atom-select-list'
5 |
6 | export default class SnippetsAvailable {
7 | constructor (snippets) {
8 | this.panel = null
9 | this.snippets = snippets
10 | this.selectListView = new SelectListView({
11 | items: [],
12 | filterKeyForItem: (snippet) => snippet.searchText,
13 | elementForItem: (snippet) => {
14 | const li = document.createElement('li')
15 | li.classList.add('two-lines')
16 |
17 | const primaryLine = document.createElement('div')
18 | primaryLine.classList.add('primary-line')
19 | primaryLine.textContent = snippet.prefix
20 | li.appendChild(primaryLine)
21 |
22 | const secondaryLine = document.createElement('div')
23 | secondaryLine.classList.add('secondary-line')
24 | secondaryLine.textContent = snippet.name
25 | li.appendChild(secondaryLine)
26 |
27 | return li
28 | },
29 | didConfirmSelection: (snippet) => {
30 | for (const cursor of this.editor.getCursors()) {
31 | this.snippets.insert(snippet.bodyText, this.editor, cursor)
32 | }
33 | this.cancel()
34 | },
35 | didConfirmEmptySelection: () => {
36 | this.cancel()
37 | },
38 | didCancelSelection: () => {
39 | this.cancel()
40 | }
41 | })
42 | this.selectListView.element.classList.add('available-snippets')
43 | this.element = this.selectListView.element
44 | }
45 |
46 | async toggle (editor) {
47 | this.editor = editor
48 | if (this.panel != null) {
49 | this.cancel()
50 | } else {
51 | this.selectListView.reset()
52 | await this.populate()
53 | this.attach()
54 | }
55 | }
56 |
57 | cancel () {
58 | this.editor = null
59 |
60 | if (this.panel != null) {
61 | this.panel.destroy()
62 | this.panel = null
63 | }
64 |
65 | if (this.previouslyFocusedElement) {
66 | this.previouslyFocusedElement.focus()
67 | this.previouslyFocusedElement = null
68 | }
69 | }
70 |
71 | populate () {
72 | const snippets = Object.values(this.snippets.getSnippets(this.editor))
73 | for (let snippet of snippets) {
74 | snippet.searchText = _.compact([snippet.prefix, snippet.name]).join(' ')
75 | }
76 | return this.selectListView.update({items: snippets})
77 | }
78 |
79 | attach () {
80 | this.previouslyFocusedElement = document.activeElement
81 | this.panel = atom.workspace.addModalPanel({item: this})
82 | this.selectListView.focus()
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/lib/snippets.cson:
--------------------------------------------------------------------------------
1 | '.source.json':
2 | 'Atom Snippet':
3 | prefix: 'snip'
4 | body: """
5 | {
6 | "${1:.source.js}": {
7 | "${2:Snippet Name}": {
8 | "prefix": "${3:Snippet Trigger}",
9 | "body": "${4:Hello World!}"
10 | }
11 | }
12 | }$5
13 | """
14 |
15 | 'Atom Snippet With No Selector':
16 | prefix: 'snipns'
17 | body: """
18 | "${1:Snippet Name}": {
19 | "prefix": "${2:Snippet Trigger}",
20 | "body": "${3:Hello World!}"
21 | }$4
22 | """
23 |
24 | 'Atom Keymap':
25 | prefix: 'key'
26 | body: """
27 | {
28 | "${1:body}": {
29 | "${2:cmd}-${3:i}": "${4:namespace}:${5:event}"
30 | }
31 | }$6
32 | """
33 |
34 | '.source.coffee':
35 | 'Atom Snippet':
36 | prefix: 'snip'
37 | body: """
38 | '${1:.source.js}':
39 | '${2:Snippet Name}':
40 | 'prefix': '${3:Snippet Trigger}'
41 | 'body': '${4:Hello World!}'$5
42 | """
43 |
44 | 'Atom Snippet With No Selector':
45 | prefix: 'snipns'
46 | body: """
47 | '${1:Snippet Name}':
48 | 'prefix': '${2:Snippet Trigger}'
49 | 'body': '${3:Hello World!}'$4
50 | """
51 |
52 | 'Atom Keymap':
53 | prefix: 'key'
54 | body: """
55 | '${1:body}':
56 | '${2:cmd}-${3:i}': '${4:namespace}:${5:event}'$6
57 | """
58 |
--------------------------------------------------------------------------------
/lib/snippets.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const {Emitter, Disposable, CompositeDisposable, File} = require('atom')
3 | const _ = require('underscore-plus')
4 | const async = require('async')
5 | const CSON = require('season')
6 | const fs = require('fs-plus')
7 | const ScopedPropertyStore = require('scoped-property-store')
8 |
9 | const Snippet = require('./snippet')
10 | const SnippetExpansion = require('./snippet-expansion')
11 | const EditorStore = require('./editor-store')
12 | const {getPackageRoot} = require('./helpers')
13 |
14 | module.exports = {
15 | activate () {
16 | this.loaded = false
17 | this.userSnippetsPath = null
18 | this.snippetIdCounter = 0
19 | this.snippetsByPackage = new Map
20 | this.parsedSnippetsById = new Map
21 | this.editorMarkerLayers = new WeakMap
22 |
23 | this.scopedPropertyStore = new ScopedPropertyStore
24 | // The above ScopedPropertyStore will store the main registry of snippets.
25 | // But we need a separate ScopedPropertyStore for the snippets that come
26 | // from disabled packages. They're isolated so that they're not considered
27 | // as candidates when the user expands a prefix, but we still need the data
28 | // around so that the snippets provided by those packages can be shown in
29 | // the settings view.
30 | this.disabledSnippetsScopedPropertyStore = new ScopedPropertyStore
31 |
32 | this.subscriptions = new CompositeDisposable
33 | this.subscriptions.add(atom.workspace.addOpener(uri => {
34 | if (uri === 'atom://.atom/snippets') {
35 | return atom.workspace.openTextFile(this.getUserSnippetsPath())
36 | }
37 | }))
38 |
39 | this.loadAll()
40 | this.watchUserSnippets(watchDisposable => {
41 | this.subscriptions.add(watchDisposable)
42 | })
43 |
44 | this.subscriptions.add(atom.config.onDidChange('core.packagesWithSnippetsDisabled', ({newValue, oldValue}) => {
45 | this.handleDisabledPackagesDidChange(newValue, oldValue)
46 | }))
47 |
48 | const snippets = this
49 |
50 | this.subscriptions.add(atom.commands.add('atom-text-editor', {
51 | 'snippets:expand'(event) {
52 | const editor = this.getModel()
53 | if (snippets.snippetToExpandUnderCursor(editor)) {
54 | snippets.clearExpansions(editor)
55 | snippets.expandSnippetsUnderCursors(editor)
56 | } else {
57 | event.abortKeyBinding()
58 | }
59 | },
60 |
61 | 'snippets:next-tab-stop'(event) {
62 | const editor = this.getModel()
63 | if (!snippets.goToNextTabStop(editor)) { event.abortKeyBinding() }
64 | },
65 |
66 | 'snippets:previous-tab-stop'(event) {
67 | const editor = this.getModel()
68 | if (!snippets.goToPreviousTabStop(editor)) { event.abortKeyBinding() }
69 | },
70 |
71 | 'snippets:available'(event) {
72 | const editor = this.getModel()
73 | const SnippetsAvailable = require('./snippets-available')
74 | if (snippets.availableSnippetsView == null) { snippets.availableSnippetsView = new SnippetsAvailable(snippets) }
75 | snippets.availableSnippetsView.toggle(editor)
76 | }
77 | }))
78 | },
79 |
80 | deactivate () {
81 | if (this.emitter != null) {
82 | this.emitter.dispose()
83 | }
84 | this.emitter = null
85 | this.editorSnippetExpansions = null
86 | atom.config.transact(() => this.subscriptions.dispose())
87 | },
88 |
89 | getUserSnippetsPath () {
90 | if (this.userSnippetsPath != null) { return this.userSnippetsPath }
91 |
92 | this.userSnippetsPath = CSON.resolve(path.join(atom.getConfigDirPath(), 'snippets'))
93 | if (this.userSnippetsPath == null) { this.userSnippetsPath = path.join(atom.getConfigDirPath(), 'snippets.cson') }
94 | return this.userSnippetsPath
95 | },
96 |
97 | loadAll () {
98 | this.loadBundledSnippets(bundledSnippets => {
99 | this.loadPackageSnippets(packageSnippets => {
100 | this.loadUserSnippets(userSnippets => {
101 | atom.config.transact(() => {
102 | for (const snippetSet of [bundledSnippets, packageSnippets, userSnippets]) {
103 | for (const filepath in snippetSet) {
104 | const snippetsBySelector = snippetSet[filepath]
105 | this.add(filepath, snippetsBySelector)
106 | }
107 | }
108 | })
109 | this.doneLoading()
110 | })
111 | })
112 | })
113 | },
114 |
115 | loadBundledSnippets (callback) {
116 | const bundledSnippetsPath = CSON.resolve(path.join(getPackageRoot(), 'lib', 'snippets'))
117 | this.loadSnippetsFile(bundledSnippetsPath, snippets => {
118 | const snippetsByPath = {}
119 | snippetsByPath[bundledSnippetsPath] = snippets
120 | callback(snippetsByPath)
121 | })
122 | },
123 |
124 | loadUserSnippets (callback) {
125 | const userSnippetsPath = this.getUserSnippetsPath()
126 | fs.stat(userSnippetsPath, (error, stat) => {
127 | if (stat != null && stat.isFile()) {
128 | this.loadSnippetsFile(userSnippetsPath, snippets => {
129 | const result = {}
130 | result[userSnippetsPath] = snippets
131 | callback(result)
132 | })
133 | } else {
134 | callback({})
135 | }
136 | })
137 | },
138 |
139 | watchUserSnippets (callback) {
140 | const userSnippetsPath = this.getUserSnippetsPath()
141 | fs.stat(userSnippetsPath, (error, stat) => {
142 | if (stat != null && stat.isFile()) {
143 | const userSnippetsFileDisposable = new CompositeDisposable()
144 | const userSnippetsFile = new File(userSnippetsPath)
145 | try {
146 | userSnippetsFileDisposable.add(userSnippetsFile.onDidChange(() => this.handleUserSnippetsDidChange()))
147 | userSnippetsFileDisposable.add(userSnippetsFile.onDidDelete(() => this.handleUserSnippetsDidChange()))
148 | userSnippetsFileDisposable.add(userSnippetsFile.onDidRename(() => this.handleUserSnippetsDidChange()))
149 | } catch (e) {
150 | const message = `\
151 | Unable to watch path: \`snippets.cson\`. Make sure you have permissions
152 | to the \`~/.atom\` directory and \`${userSnippetsPath}\`.
153 |
154 | On linux there are currently problems with watch sizes. See
155 | [this document][watches] for more info.
156 | [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\
157 | `
158 | atom.notifications.addError(message, {dismissable: true})
159 | }
160 |
161 | callback(userSnippetsFileDisposable)
162 | } else {
163 | callback(new Disposable())
164 | }
165 | })
166 | },
167 |
168 | // Called when a user's snippets file is changed, deleted, or moved so that we
169 | // can immediately re-process the snippets it contains.
170 | handleUserSnippetsDidChange () {
171 | const userSnippetsPath = this.getUserSnippetsPath()
172 | atom.config.transact(() => {
173 | this.clearSnippetsForPath(userSnippetsPath)
174 | this.loadSnippetsFile(userSnippetsPath, result => {
175 | this.add(userSnippetsPath, result)
176 | })
177 | })
178 | },
179 |
180 | // Called when the "Enable" checkbox is checked/unchecked in the Snippets
181 | // section of a package's settings view.
182 | handleDisabledPackagesDidChange (newDisabledPackages = [], oldDisabledPackages = []) {
183 | const packagesToAdd = []
184 | const packagesToRemove = []
185 | for (const p of oldDisabledPackages) {
186 | if (!newDisabledPackages.includes(p)) { packagesToAdd.push(p) }
187 | }
188 |
189 | for (const p of newDisabledPackages) {
190 | if (!oldDisabledPackages.includes(p)) { packagesToRemove.push(p) }
191 | }
192 |
193 | atom.config.transact(() => {
194 | for (const p of packagesToRemove) { this.removeSnippetsForPackage(p) }
195 | for (const p of packagesToAdd) { this.addSnippetsForPackage(p) }
196 | })
197 | },
198 |
199 | addSnippetsForPackage (packageName) {
200 | const snippetSet = this.snippetsByPackage.get(packageName)
201 | for (const filePath in snippetSet) {
202 | const snippetsBySelector = snippetSet[filePath]
203 | this.add(filePath, snippetsBySelector)
204 | }
205 | },
206 |
207 | removeSnippetsForPackage (packageName) {
208 | const snippetSet = this.snippetsByPackage.get(packageName)
209 | // Copy these snippets to the "quarantined" ScopedPropertyStore so that they
210 | // remain present in the list of unparsed snippets reported to the settings
211 | // view.
212 | this.addSnippetsInDisabledPackage(snippetSet)
213 | for (const filePath in snippetSet) {
214 | this.clearSnippetsForPath(filePath)
215 | }
216 | },
217 |
218 | loadPackageSnippets (callback) {
219 | const disabledPackageNames = atom.config.get('core.packagesWithSnippetsDisabled') || []
220 | const packages = atom.packages.getLoadedPackages().sort((pack, _) => {
221 | return /\/node_modules\//.test(pack.path) ? -1 : 1
222 | })
223 |
224 | const snippetsDirPaths = []
225 | for (const pack of packages) {
226 | snippetsDirPaths.push(path.join(pack.path, 'snippets'))
227 | }
228 |
229 | async.map(snippetsDirPaths, this.loadSnippetsDirectory.bind(this), (error, results) => {
230 | const zipped = []
231 | for (const key in results) {
232 | zipped.push({result: results[key], pack: packages[key]})
233 | }
234 |
235 | const enabledPackages = []
236 | for (const o of zipped) {
237 | // Skip packages that contain no snippets.
238 | if (Object.keys(o.result).length === 0) { continue }
239 | // Keep track of which snippets come from which packages so we can
240 | // unload them selectively later. All packages get put into this map,
241 | // even disabled packages, because we need to know which snippets to add
242 | // if those packages are enabled again.
243 | this.snippetsByPackage.set(o.pack.name, o.result)
244 | if (disabledPackageNames.includes(o.pack.name)) {
245 | // Since disabled packages' snippets won't get added to the main
246 | // ScopedPropertyStore, we'll keep track of them in a separate
247 | // ScopedPropertyStore so that they can still be represented in the
248 | // settings view.
249 | this.addSnippetsInDisabledPackage(o.result)
250 | } else {
251 | enabledPackages.push(o.result)
252 | }
253 | }
254 |
255 | callback(_.extend({}, ...enabledPackages))
256 | })
257 | },
258 |
259 | doneLoading () {
260 | this.loaded = true
261 | this.getEmitter().emit('did-load-snippets')
262 | },
263 |
264 | onDidLoadSnippets (callback) {
265 | this.getEmitter().on('did-load-snippets', callback)
266 | },
267 |
268 | getEmitter () {
269 | if (this.emitter == null) {
270 | this.emitter = new Emitter
271 | }
272 | return this.emitter
273 | },
274 |
275 | loadSnippetsDirectory (snippetsDirPath, callback) {
276 | fs.isDirectory(snippetsDirPath, isDirectory => {
277 | if (!isDirectory) { return callback(null, {}) }
278 |
279 | fs.readdir(snippetsDirPath, (error, entries) => {
280 | if (error) {
281 | console.warn(`Error reading snippets directory ${snippetsDirPath}`, error)
282 | return callback(null, {})
283 | }
284 |
285 | async.map(
286 | entries,
287 | (entry, done) => {
288 | const filePath = path.join(snippetsDirPath, entry)
289 | this.loadSnippetsFile(filePath, snippets => done(null, {filePath, snippets}))
290 | },
291 | (error, results) => {
292 | const snippetsByPath = {}
293 | for (const {filePath, snippets} of results) {
294 | snippetsByPath[filePath] = snippets
295 | }
296 | callback(null, snippetsByPath)
297 | })
298 | })
299 | })
300 | },
301 |
302 | loadSnippetsFile (filePath, callback) {
303 | if (!CSON.isObjectPath(filePath)) { return callback({}) }
304 | CSON.readFile(filePath, {allowDuplicateKeys: false}, (error, object = {}) => {
305 | if (error != null) {
306 | console.warn(`Error reading snippets file '${filePath}': ${error.stack != null ? error.stack : error}`)
307 | atom.notifications.addError(`Failed to load snippets from '${filePath}'`, {detail: error.message, dismissable: true})
308 | }
309 | callback(object)
310 | })
311 | },
312 |
313 | add (filePath, snippetsBySelector, isDisabled = false) {
314 | for (const selector in snippetsBySelector) {
315 | const snippetsByName = snippetsBySelector[selector]
316 | const unparsedSnippetsByPrefix = {}
317 | for (const name in snippetsByName) {
318 | const attributes = snippetsByName[name]
319 | const {prefix, body} = attributes
320 | attributes.name = name
321 | attributes.id = this.snippetIdCounter++
322 | if (typeof body === 'string') {
323 | unparsedSnippetsByPrefix[prefix] = attributes
324 | } else if (body == null) {
325 | unparsedSnippetsByPrefix[prefix] = null
326 | }
327 | }
328 |
329 | this.storeUnparsedSnippets(unparsedSnippetsByPrefix, filePath, selector, isDisabled)
330 | }
331 | },
332 |
333 | addSnippetsInDisabledPackage (bundle) {
334 | for (const filePath in bundle) {
335 | const snippetsBySelector = bundle[filePath]
336 | this.add(filePath, snippetsBySelector, true)
337 | }
338 | },
339 |
340 | getScopeChain (object) {
341 | let scopesArray = object
342 | if (object && object.getScopesArray) {
343 | scopesArray = object.getScopesArray()
344 | }
345 |
346 | return scopesArray
347 | .map(scope => scope[0] === '.' ? scope : `.${scope}`)
348 | .join(' ')
349 | },
350 |
351 | storeUnparsedSnippets (value, path, selector, isDisabled = false) {
352 | // The `isDisabled` flag determines which scoped property store we'll use.
353 | // Active snippets get put into one and inactive snippets get put into
354 | // another. Only the first one gets consulted when we look up a snippet
355 | // prefix for expansion, but both stores have their contents exported when
356 | // the settings view asks for all available snippets.
357 | const unparsedSnippets = {}
358 | unparsedSnippets[selector] = {"snippets": value}
359 | const store = isDisabled ? this.disabledSnippetsScopedPropertyStore : this.scopedPropertyStore
360 | store.addProperties(path, unparsedSnippets, {priority: this.priorityForSource(path)})
361 | },
362 |
363 | clearSnippetsForPath (path) {
364 | for (const scopeSelector in this.scopedPropertyStore.propertiesForSource(path)) {
365 | const object = this.scopedPropertyStore.propertiesForSourceAndSelector(path, scopeSelector)
366 | for (const prefix in object) {
367 | const attributes = object[prefix]
368 | this.parsedSnippetsById.delete(attributes.id)
369 | }
370 |
371 | this.scopedPropertyStore.removePropertiesForSourceAndSelector(path, scopeSelector)
372 | }
373 | },
374 |
375 | parsedSnippetsForScopes (scopeDescriptor) {
376 | let unparsedLegacySnippetsByPrefix
377 |
378 | const unparsedSnippetsByPrefix = this.scopedPropertyStore.getPropertyValue(
379 | this.getScopeChain(scopeDescriptor),
380 | "snippets"
381 | )
382 |
383 | const legacyScopeDescriptor = atom.config.getLegacyScopeDescriptorForNewScopeDescriptor
384 | ? atom.config.getLegacyScopeDescriptorForNewScopeDescriptor(scopeDescriptor)
385 | : undefined
386 |
387 | if (legacyScopeDescriptor) {
388 | unparsedLegacySnippetsByPrefix = this.scopedPropertyStore.getPropertyValue(
389 | this.getScopeChain(legacyScopeDescriptor),
390 | "snippets"
391 | )
392 | }
393 |
394 | const snippets = {}
395 |
396 | if (unparsedSnippetsByPrefix) {
397 | for (const prefix in unparsedSnippetsByPrefix) {
398 | const attributes = unparsedSnippetsByPrefix[prefix]
399 | if (typeof (attributes != null ? attributes.body : undefined) !== 'string') { continue }
400 | snippets[prefix] = this.getParsedSnippet(attributes)
401 | }
402 | }
403 |
404 | if (unparsedLegacySnippetsByPrefix) {
405 | for (const prefix in unparsedLegacySnippetsByPrefix) {
406 | const attributes = unparsedLegacySnippetsByPrefix[prefix]
407 | if (snippets[prefix]) { continue }
408 | if (typeof (attributes != null ? attributes.body : undefined) !== 'string') { continue }
409 | snippets[prefix] = this.getParsedSnippet(attributes)
410 | }
411 | }
412 |
413 | return snippets
414 | },
415 |
416 | getParsedSnippet (attributes) {
417 | let snippet = this.parsedSnippetsById.get(attributes.id)
418 | if (snippet == null) {
419 | let {id, prefix, name, body, bodyTree, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML} = attributes
420 | if (bodyTree == null) { bodyTree = this.getBodyParser().parse(body) }
421 | snippet = new Snippet({id, name, prefix, bodyTree, description, descriptionMoreURL, rightLabelHTML, leftLabel, leftLabelHTML, bodyText: body})
422 | this.parsedSnippetsById.set(attributes.id, snippet)
423 | }
424 | return snippet
425 | },
426 |
427 | priorityForSource (source) {
428 | if (source === this.getUserSnippetsPath()) {
429 | return 1000
430 | } else {
431 | return 0
432 | }
433 | },
434 |
435 | getBodyParser () {
436 | if (this.bodyParser == null) {
437 | this.bodyParser = require('./snippet-body-parser')
438 | }
439 | return this.bodyParser
440 | },
441 |
442 | // Get an {Object} with these keys:
443 | // * `snippetPrefix`: the possible snippet prefix text preceding the cursor
444 | // * `wordPrefix`: the word preceding the cursor
445 | //
446 | // Returns `null` if the values aren't the same for all cursors
447 | getPrefixText (snippets, editor) {
448 | const wordRegex = this.wordRegexForSnippets(snippets)
449 |
450 | let snippetPrefix = null
451 | let wordPrefix = null
452 |
453 | for (const cursor of editor.getCursors()) {
454 | const position = cursor.getBufferPosition()
455 |
456 | const prefixStart = cursor.getBeginningOfCurrentWordBufferPosition({wordRegex})
457 | const cursorSnippetPrefix = editor.getTextInRange([prefixStart, position])
458 | if ((snippetPrefix != null) && (cursorSnippetPrefix !== snippetPrefix)) { return null }
459 | snippetPrefix = cursorSnippetPrefix
460 |
461 | const wordStart = cursor.getBeginningOfCurrentWordBufferPosition()
462 | const cursorWordPrefix = editor.getTextInRange([wordStart, position])
463 | if ((wordPrefix != null) && (cursorWordPrefix !== wordPrefix)) { return null }
464 | wordPrefix = cursorWordPrefix
465 | }
466 |
467 | return {snippetPrefix, wordPrefix}
468 | },
469 |
470 | // Get a RegExp of all the characters used in the snippet prefixes
471 | wordRegexForSnippets (snippets) {
472 | const prefixes = {}
473 |
474 | for (const prefix in snippets) {
475 | for (const character of prefix) { prefixes[character] = true }
476 | }
477 |
478 | const prefixCharacters = Object.keys(prefixes).join('')
479 | return new RegExp(`[${_.escapeRegExp(prefixCharacters)}]+`)
480 | },
481 |
482 | // Get the best match snippet for the given prefix text. This will return
483 | // the longest match where there is no exact match to the prefix text.
484 | snippetForPrefix (snippets, prefix, wordPrefix) {
485 | let longestPrefixMatch = null
486 |
487 | for (const snippetPrefix in snippets) {
488 | const snippet = snippets[snippetPrefix]
489 | if (prefix.endsWith(snippetPrefix) && (wordPrefix.length <= snippetPrefix.length)) {
490 | if ((longestPrefixMatch == null) || (snippetPrefix.length > longestPrefixMatch.prefix.length)) {
491 | longestPrefixMatch = snippet
492 | }
493 | }
494 | }
495 |
496 | return longestPrefixMatch
497 | },
498 |
499 | getSnippets (editor) {
500 | return this.parsedSnippetsForScopes(editor.getLastCursor().getScopeDescriptor())
501 | },
502 |
503 | snippetToExpandUnderCursor (editor) {
504 | if (!editor.getLastSelection().isEmpty()) { return false }
505 | const snippets = this.getSnippets(editor)
506 | if (_.isEmpty(snippets)) { return false }
507 |
508 | const prefixData = this.getPrefixText(snippets, editor)
509 | if (prefixData) {
510 | return this.snippetForPrefix(snippets, prefixData.snippetPrefix, prefixData.wordPrefix)
511 | }
512 | },
513 |
514 | expandSnippetsUnderCursors (editor) {
515 | const snippet = this.snippetToExpandUnderCursor(editor)
516 | if (!snippet) { return false }
517 |
518 | this.getStore(editor).observeHistory({
519 | undo: event => { this.onUndoOrRedo(editor, event, true) },
520 | redo: event => { this.onUndoOrRedo(editor, event, false) }
521 | })
522 |
523 | this.findOrCreateMarkerLayer(editor)
524 | editor.transact(() => {
525 | const cursors = editor.getCursors()
526 | for (const cursor of cursors) {
527 | const cursorPosition = cursor.getBufferPosition()
528 | const startPoint = cursorPosition.translate([0, -snippet.prefix.length], [0, 0])
529 | cursor.selection.setBufferRange([startPoint, cursorPosition])
530 | this.insert(snippet, editor, cursor)
531 | }
532 | })
533 | return true
534 | },
535 |
536 | goToNextTabStop (editor) {
537 | let nextTabStopVisited = false
538 | for (const expansion of this.getExpansions(editor)) {
539 | if (expansion && expansion.goToNextTabStop()) {
540 | nextTabStopVisited = true
541 | }
542 | }
543 | return nextTabStopVisited
544 | },
545 |
546 | goToPreviousTabStop (editor) {
547 | let previousTabStopVisited = false
548 | for (const expansion of this.getExpansions(editor)) {
549 | if (expansion && expansion.goToPreviousTabStop()) {
550 | previousTabStopVisited = true
551 | }
552 | }
553 | return previousTabStopVisited
554 | },
555 |
556 | getStore (editor) {
557 | return EditorStore.findOrCreate(editor)
558 | },
559 |
560 | findOrCreateMarkerLayer (editor) {
561 | let layer = this.editorMarkerLayers.get(editor)
562 | if (layer === undefined) {
563 | layer = editor.addMarkerLayer({maintainHistory: true})
564 | this.editorMarkerLayers.set(editor, layer)
565 | }
566 | return layer
567 | },
568 |
569 | getExpansions (editor) {
570 | return this.getStore(editor).getExpansions()
571 | },
572 |
573 | clearExpansions (editor) {
574 | const store = this.getStore(editor)
575 | store.clearExpansions()
576 | // There are no more active instances of this expansion, so we should undo
577 | // the spying we set up on this editor.
578 | store.stopObserving()
579 | store.stopObservingHistory()
580 | },
581 |
582 | addExpansion (editor, snippetExpansion) {
583 | this.getStore(editor).addExpansion(snippetExpansion)
584 | },
585 |
586 | textChanged (editor, event) {
587 | const store = this.getStore(editor)
588 | const activeExpansions = store.getExpansions()
589 |
590 | if ((activeExpansions.length === 0) || activeExpansions[0].isIgnoringBufferChanges) { return }
591 |
592 | this.ignoringTextChangesForEditor(editor, () =>
593 | editor.transact(() =>
594 | activeExpansions.map(expansion => expansion.textChanged(event)))
595 | )
596 |
597 | // Create a checkpoint here to consolidate all the changes we just made into
598 | // the transaction that prompted them.
599 | this.makeCheckpoint(editor)
600 | },
601 |
602 | // Perform an action inside the editor without triggering our `textChanged`
603 | // callback.
604 | ignoringTextChangesForEditor (editor, callback) {
605 | this.stopObservingEditor(editor)
606 | callback()
607 | this.observeEditor(editor)
608 | },
609 |
610 | observeEditor (editor) {
611 | this.getStore(editor).observe(event => this.textChanged(editor, event))
612 | },
613 |
614 | stopObservingEditor (editor) {
615 | this.getStore(editor).stopObserving()
616 | },
617 |
618 | makeCheckpoint (editor) {
619 | this.getStore(editor).makeCheckpoint()
620 | },
621 |
622 | insert (snippet, editor, cursor) {
623 | if (editor == null) { editor = atom.workspace.getActiveTextEditor() }
624 | if (cursor == null) { cursor = editor.getLastCursor() }
625 | if (typeof snippet === 'string') {
626 | const bodyTree = this.getBodyParser().parse(snippet)
627 | snippet = new Snippet({name: '__anonymous', prefix: '', bodyTree, bodyText: snippet})
628 | }
629 | return new SnippetExpansion(snippet, editor, cursor, this)
630 | },
631 |
632 | getUnparsedSnippets () {
633 | const results = []
634 | const iterate = sets => {
635 | for (const item of sets) {
636 | const newItem = _.deepClone(item)
637 | // The atom-slick library has already parsed the `selector` property, so
638 | // it's an AST here instead of a string. The object has a `toString`
639 | // method that turns it back into a string. That custom behavior won't
640 | // be preserved in the deep clone of the object, so we have to handle it
641 | // separately.
642 | newItem.selectorString = item.selector.toString()
643 | results.push(newItem)
644 | }
645 | }
646 |
647 | iterate(this.scopedPropertyStore.propertySets)
648 | iterate(this.disabledSnippetsScopedPropertyStore.propertySets)
649 | return results
650 | },
651 |
652 | provideSnippets () {
653 | return {
654 | bundledSnippetsLoaded: () => this.loaded,
655 | insertSnippet: this.insert.bind(this),
656 | snippetsForScopes: this.parsedSnippetsForScopes.bind(this),
657 | getUnparsedSnippets: this.getUnparsedSnippets.bind(this),
658 | getUserSnippetsPath: this.getUserSnippetsPath.bind(this)
659 | }
660 | },
661 |
662 | onUndoOrRedo (editor, isUndo) {
663 | const activeExpansions = this.getExpansions(editor)
664 | activeExpansions.forEach(expansion => expansion.onUndoOrRedo(isUndo))
665 | }
666 | }
667 |
--------------------------------------------------------------------------------
/lib/tab-stop-list.js:
--------------------------------------------------------------------------------
1 | const TabStop = require('./tab-stop')
2 |
3 | class TabStopList {
4 | constructor (snippet) {
5 | this.snippet = snippet
6 | this.list = {}
7 | }
8 |
9 | get length () {
10 | return Object.keys(this.list).length
11 | }
12 |
13 | get hasEndStop () {
14 | return !!this.list[Infinity]
15 | }
16 |
17 | findOrCreate ({ index, snippet }) {
18 | if (!this.list[index]) {
19 | this.list[index] = new TabStop({ index, snippet })
20 | }
21 | return this.list[index]
22 | }
23 |
24 | forEachIndex (iterator) {
25 | let indices = Object.keys(this.list).sort((a1, a2) => a1 - a2)
26 | indices.forEach(iterator)
27 | }
28 |
29 | getInsertions () {
30 | let results = []
31 | this.forEachIndex(index => {
32 | results.push(...this.list[index].insertions)
33 | })
34 | return results
35 | }
36 |
37 | toArray () {
38 | let results = []
39 | this.forEachIndex(index => {
40 | let tabStop = this.list[index]
41 | if (!tabStop.isValid()) return
42 | results.push(tabStop)
43 | })
44 | return results
45 | }
46 | }
47 |
48 | module.exports = TabStopList
49 |
--------------------------------------------------------------------------------
/lib/tab-stop.js:
--------------------------------------------------------------------------------
1 | const {Range} = require('atom')
2 | const Insertion = require('./insertion')
3 |
4 | // A tab stop:
5 | // * belongs to a snippet
6 | // * has an index (one tab stop per index)
7 | // * has multiple Insertions
8 | class TabStop {
9 | constructor ({ snippet, index, insertions }) {
10 | this.insertions = insertions || []
11 | Object.assign(this, { snippet, index })
12 | }
13 |
14 | isValid () {
15 | let any = this.insertions.some(insertion => insertion.isTransformation())
16 | if (!any) return true
17 | let all = this.insertions.every(insertion => insertion.isTransformation())
18 | // If there are any transforming insertions, there must be at least one
19 | // non-transforming insertion to act as the primary.
20 | return !all
21 | }
22 |
23 | addInsertion ({ range, substitution, references }) {
24 | let insertion = new Insertion({ range, substitution, references })
25 | let insertions = this.insertions
26 | insertions.push(insertion)
27 | insertions = insertions.sort((i1, i2) => {
28 | return i1.range.start.compare(i2.range.start)
29 | })
30 | let initial = insertions.find(insertion => !insertion.isTransformation())
31 | if (initial) {
32 | insertions.splice(insertions.indexOf(initial), 1)
33 | insertions.unshift(initial)
34 | }
35 | this.insertions = insertions
36 | }
37 |
38 | copyWithIndent (indent) {
39 | let { snippet, index, insertions } = this
40 | let newInsertions = insertions.map(insertion => {
41 | let { range, substitution } = insertion
42 | let newRange = Range.fromObject(range, true)
43 | if (newRange.start.row) {
44 | newRange.start.column += indent.length
45 | newRange.end.column += indent.length
46 | }
47 | return new Insertion({
48 | range: newRange,
49 | substitution
50 | })
51 | })
52 |
53 | return new TabStop({
54 | snippet,
55 | index,
56 | insertions: newInsertions
57 | })
58 | }
59 | }
60 |
61 | module.exports = TabStop
62 |
--------------------------------------------------------------------------------
/menus/snippets.cson:
--------------------------------------------------------------------------------
1 | 'menu': [
2 | 'label': 'Packages'
3 | 'submenu': [
4 | 'label': 'Snippets'
5 | 'submenu': [
6 | { 'label': 'Expand', 'command': 'snippets:show' }
7 | { 'label': 'Next Stop', 'command': 'snippets:next-tab-stop' }
8 | { 'label': 'Previous Stop', 'command': 'snippets:previous-tab-stop' }
9 | { 'label': 'Available', 'command': 'snippets:available' }
10 | ]
11 | ]
12 | ]
13 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "snippets",
3 | "version": "1.6.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "ansi-regex": {
8 | "version": "2.1.1",
9 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
10 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
11 | },
12 | "async": {
13 | "version": "0.2.10",
14 | "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
15 | "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
16 | },
17 | "atom-select-list": {
18 | "version": "0.7.2",
19 | "resolved": "https://registry.npmjs.org/atom-select-list/-/atom-select-list-0.7.2.tgz",
20 | "integrity": "sha512-a707OB1DhLGjzqtFrtMQKH7BBxFuCh8UBoUWxgFOrLrSwVh3g+/TlVPVDOz12+U0mDu3mIrnYLqQyhywQOTxhw==",
21 | "requires": {
22 | "etch": "^0.12.6",
23 | "fuzzaldrin": "^2.1.0"
24 | }
25 | },
26 | "atom-slick": {
27 | "version": "2.0.0",
28 | "resolved": "https://registry.npmjs.org/atom-slick/-/atom-slick-2.0.0.tgz",
29 | "integrity": "sha1-/w2+Fb4sTtomi50w124lF+C308o="
30 | },
31 | "balanced-match": {
32 | "version": "1.0.0",
33 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
34 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
35 | },
36 | "brace-expansion": {
37 | "version": "1.1.8",
38 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
39 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
40 | "requires": {
41 | "balanced-match": "^1.0.0",
42 | "concat-map": "0.0.1"
43 | }
44 | },
45 | "camelcase": {
46 | "version": "2.1.1",
47 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
48 | "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
49 | },
50 | "cliui": {
51 | "version": "3.2.0",
52 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
53 | "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
54 | "requires": {
55 | "string-width": "^1.0.1",
56 | "strip-ansi": "^3.0.1",
57 | "wrap-ansi": "^2.0.0"
58 | }
59 | },
60 | "code-point-at": {
61 | "version": "1.1.0",
62 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
63 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
64 | },
65 | "coffee-script": {
66 | "version": "1.12.7",
67 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz",
68 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw=="
69 | },
70 | "coffeelint": {
71 | "version": "1.16.0",
72 | "resolved": "https://registry.npmjs.org/coffeelint/-/coffeelint-1.16.0.tgz",
73 | "integrity": "sha1-g9jtHa/eOmd95E57ihi+YHdh5tg=",
74 | "dev": true,
75 | "requires": {
76 | "coffee-script": "~1.11.0",
77 | "glob": "^7.0.6",
78 | "ignore": "^3.0.9",
79 | "optimist": "^0.6.1",
80 | "resolve": "^0.6.3",
81 | "strip-json-comments": "^1.0.2"
82 | },
83 | "dependencies": {
84 | "coffee-script": {
85 | "version": "1.11.1",
86 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.11.1.tgz",
87 | "integrity": "sha1-vxxHrWREOg2V0S3ysUfMCk2q1uk=",
88 | "dev": true
89 | },
90 | "optimist": {
91 | "version": "0.6.1",
92 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
93 | "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
94 | "dev": true,
95 | "requires": {
96 | "minimist": "~0.0.1",
97 | "wordwrap": "~0.0.2"
98 | }
99 | }
100 | }
101 | },
102 | "concat-map": {
103 | "version": "0.0.1",
104 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
105 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
106 | },
107 | "cson-parser": {
108 | "version": "1.3.5",
109 | "resolved": "https://registry.npmjs.org/cson-parser/-/cson-parser-1.3.5.tgz",
110 | "integrity": "sha1-fsZ14DkUVTO/KmqFYHPxWZ2cLSQ=",
111 | "requires": {
112 | "coffee-script": "^1.10.0"
113 | }
114 | },
115 | "d": {
116 | "version": "0.1.1",
117 | "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz",
118 | "integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=",
119 | "requires": {
120 | "es5-ext": "~0.10.2"
121 | }
122 | },
123 | "decamelize": {
124 | "version": "1.2.0",
125 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
126 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
127 | },
128 | "emissary": {
129 | "version": "1.3.3",
130 | "resolved": "https://registry.npmjs.org/emissary/-/emissary-1.3.3.tgz",
131 | "integrity": "sha1-phjZLWgrIy0xER3DYlpd9mF5lgY=",
132 | "requires": {
133 | "es6-weak-map": "^0.1.2",
134 | "mixto": "1.x",
135 | "property-accessors": "^1.1",
136 | "underscore-plus": "1.x"
137 | }
138 | },
139 | "es5-ext": {
140 | "version": "0.10.30",
141 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.30.tgz",
142 | "integrity": "sha1-cUGhaDZpfbq/qq7uQUlc4p9SyTk=",
143 | "requires": {
144 | "es6-iterator": "2",
145 | "es6-symbol": "~3.1"
146 | },
147 | "dependencies": {
148 | "d": {
149 | "version": "1.0.0",
150 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
151 | "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
152 | "requires": {
153 | "es5-ext": "^0.10.9"
154 | }
155 | },
156 | "es6-iterator": {
157 | "version": "2.0.1",
158 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz",
159 | "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=",
160 | "requires": {
161 | "d": "1",
162 | "es5-ext": "^0.10.14",
163 | "es6-symbol": "^3.1"
164 | }
165 | },
166 | "es6-symbol": {
167 | "version": "3.1.1",
168 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
169 | "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
170 | "requires": {
171 | "d": "1",
172 | "es5-ext": "~0.10.14"
173 | }
174 | }
175 | }
176 | },
177 | "es6-iterator": {
178 | "version": "0.1.3",
179 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-0.1.3.tgz",
180 | "integrity": "sha1-1vWLjE/EE8JJtLqhl2j45NfIlE4=",
181 | "requires": {
182 | "d": "~0.1.1",
183 | "es5-ext": "~0.10.5",
184 | "es6-symbol": "~2.0.1"
185 | }
186 | },
187 | "es6-symbol": {
188 | "version": "2.0.1",
189 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-2.0.1.tgz",
190 | "integrity": "sha1-dhtcZ8/U8dGK+yNPaR1nhoLLO/M=",
191 | "requires": {
192 | "d": "~0.1.1",
193 | "es5-ext": "~0.10.5"
194 | }
195 | },
196 | "es6-weak-map": {
197 | "version": "0.1.4",
198 | "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-0.1.4.tgz",
199 | "integrity": "sha1-cGzvnpmqI2undmwjnIueKG6n0ig=",
200 | "requires": {
201 | "d": "~0.1.1",
202 | "es5-ext": "~0.10.6",
203 | "es6-iterator": "~0.1.3",
204 | "es6-symbol": "~2.0.1"
205 | }
206 | },
207 | "etch": {
208 | "version": "0.12.8",
209 | "resolved": "https://registry.npmjs.org/etch/-/etch-0.12.8.tgz",
210 | "integrity": "sha512-dFLRe4wLroVtwzyy1vGlE3BSDZHiL0kZME5XgNGzZIULcYTvVno8vbiIleAesoKJmwWaxDTzG+4eppg2zk14JQ=="
211 | },
212 | "event-kit": {
213 | "version": "1.5.0",
214 | "resolved": "https://registry.npmjs.org/event-kit/-/event-kit-1.5.0.tgz",
215 | "integrity": "sha1-Ek72qtgyjcsmtxxHWQtbjmPrxIc=",
216 | "requires": {
217 | "grim": "^1.2.1"
218 | }
219 | },
220 | "fs-plus": {
221 | "version": "3.0.1",
222 | "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.0.1.tgz",
223 | "integrity": "sha1-VMFpxA4ohKZtNSeA0Y3TH5HToQ0=",
224 | "requires": {
225 | "async": "^1.5.2",
226 | "mkdirp": "^0.5.1",
227 | "rimraf": "^2.5.2",
228 | "underscore-plus": "1.x"
229 | },
230 | "dependencies": {
231 | "async": {
232 | "version": "1.5.2",
233 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
234 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
235 | }
236 | }
237 | },
238 | "fs.realpath": {
239 | "version": "1.0.0",
240 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
241 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
242 | },
243 | "fuzzaldrin": {
244 | "version": "2.1.0",
245 | "resolved": "https://registry.npmjs.org/fuzzaldrin/-/fuzzaldrin-2.1.0.tgz",
246 | "integrity": "sha1-kCBMPi/appQbso0WZF1BgGOpDps="
247 | },
248 | "glob": {
249 | "version": "7.1.2",
250 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
251 | "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
252 | "requires": {
253 | "fs.realpath": "^1.0.0",
254 | "inflight": "^1.0.4",
255 | "inherits": "2",
256 | "minimatch": "^3.0.4",
257 | "once": "^1.3.0",
258 | "path-is-absolute": "^1.0.0"
259 | }
260 | },
261 | "grim": {
262 | "version": "1.5.0",
263 | "resolved": "https://registry.npmjs.org/grim/-/grim-1.5.0.tgz",
264 | "integrity": "sha1-sysI71Z88YUvgXWe2caLDXE5ajI=",
265 | "requires": {
266 | "emissary": "^1.2.0"
267 | }
268 | },
269 | "ignore": {
270 | "version": "3.3.5",
271 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.5.tgz",
272 | "integrity": "sha1-xOcVRV9gc6jX5drnLS/J1xZj26Y=",
273 | "dev": true
274 | },
275 | "inflight": {
276 | "version": "1.0.6",
277 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
278 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
279 | "requires": {
280 | "once": "^1.3.0",
281 | "wrappy": "1"
282 | }
283 | },
284 | "inherits": {
285 | "version": "2.0.3",
286 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
287 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
288 | },
289 | "invert-kv": {
290 | "version": "1.0.0",
291 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
292 | "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
293 | },
294 | "is-fullwidth-code-point": {
295 | "version": "1.0.0",
296 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
297 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
298 | "requires": {
299 | "number-is-nan": "^1.0.0"
300 | }
301 | },
302 | "key-path-helpers": {
303 | "version": "0.1.0",
304 | "resolved": "https://registry.npmjs.org/key-path-helpers/-/key-path-helpers-0.1.0.tgz",
305 | "integrity": "sha1-zYFJULeZzHRaNGqlIfkilK9du6Q="
306 | },
307 | "lcid": {
308 | "version": "1.0.0",
309 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
310 | "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
311 | "requires": {
312 | "invert-kv": "^1.0.0"
313 | }
314 | },
315 | "loophole": {
316 | "version": "1.1.0",
317 | "resolved": "https://registry.npmjs.org/loophole/-/loophole-1.1.0.tgz",
318 | "integrity": "sha1-N5Sf6kU7YlasxyXDIM4MWn9wor0="
319 | },
320 | "minimatch": {
321 | "version": "3.0.4",
322 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
323 | "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
324 | "requires": {
325 | "brace-expansion": "^1.1.7"
326 | }
327 | },
328 | "minimist": {
329 | "version": "0.0.8",
330 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
331 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
332 | },
333 | "mixto": {
334 | "version": "1.0.0",
335 | "resolved": "https://registry.npmjs.org/mixto/-/mixto-1.0.0.tgz",
336 | "integrity": "sha1-wyDvYbUvKJj1IuF9i7xtUG2EJbY="
337 | },
338 | "mkdirp": {
339 | "version": "0.5.1",
340 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
341 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
342 | "requires": {
343 | "minimist": "0.0.8"
344 | }
345 | },
346 | "number-is-nan": {
347 | "version": "1.0.1",
348 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
349 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
350 | },
351 | "once": {
352 | "version": "1.4.0",
353 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
354 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
355 | "requires": {
356 | "wrappy": "1"
357 | }
358 | },
359 | "os-locale": {
360 | "version": "1.4.0",
361 | "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
362 | "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
363 | "requires": {
364 | "lcid": "^1.0.0"
365 | }
366 | },
367 | "os-tmpdir": {
368 | "version": "1.0.2",
369 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
370 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
371 | },
372 | "path-is-absolute": {
373 | "version": "1.0.1",
374 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
375 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
376 | },
377 | "pegjs": {
378 | "version": "0.8.0",
379 | "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.8.0.tgz",
380 | "integrity": "sha1-l28GfaE+XFsVAcAXklZoolOBFWE="
381 | },
382 | "property-accessors": {
383 | "version": "1.1.3",
384 | "resolved": "https://registry.npmjs.org/property-accessors/-/property-accessors-1.1.3.tgz",
385 | "integrity": "sha1-Hd6EAkYxhlkJ7zBwM2VoDF+SixU=",
386 | "requires": {
387 | "es6-weak-map": "^0.1.2",
388 | "mixto": "1.x"
389 | }
390 | },
391 | "resolve": {
392 | "version": "0.6.3",
393 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz",
394 | "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=",
395 | "dev": true
396 | },
397 | "rimraf": {
398 | "version": "2.6.2",
399 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
400 | "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
401 | "requires": {
402 | "glob": "^7.0.5"
403 | }
404 | },
405 | "scoped-property-store": {
406 | "version": "0.17.0",
407 | "resolved": "https://registry.npmjs.org/scoped-property-store/-/scoped-property-store-0.17.0.tgz",
408 | "integrity": "sha1-raAsANYC/SBQlh4nF92dArozGDE=",
409 | "requires": {
410 | "atom-slick": "^2",
411 | "event-kit": "^1.0.0",
412 | "grim": "^1.2.1",
413 | "key-path-helpers": "^0.1.0",
414 | "underscore-plus": "^1.6.3"
415 | }
416 | },
417 | "season": {
418 | "version": "6.0.2",
419 | "resolved": "https://registry.npmjs.org/season/-/season-6.0.2.tgz",
420 | "integrity": "sha1-naWPsd3SSCTXYhstxjpxI7UCF7Y=",
421 | "requires": {
422 | "cson-parser": "^1.3.0",
423 | "fs-plus": "^3.0.0",
424 | "yargs": "^3.23.0"
425 | }
426 | },
427 | "string-width": {
428 | "version": "1.0.2",
429 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
430 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
431 | "requires": {
432 | "code-point-at": "^1.0.0",
433 | "is-fullwidth-code-point": "^1.0.0",
434 | "strip-ansi": "^3.0.0"
435 | }
436 | },
437 | "strip-ansi": {
438 | "version": "3.0.1",
439 | "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
440 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
441 | "requires": {
442 | "ansi-regex": "^2.0.0"
443 | }
444 | },
445 | "strip-json-comments": {
446 | "version": "1.0.4",
447 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
448 | "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=",
449 | "dev": true
450 | },
451 | "temp": {
452 | "version": "0.8.3",
453 | "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz",
454 | "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=",
455 | "requires": {
456 | "os-tmpdir": "^1.0.0",
457 | "rimraf": "~2.2.6"
458 | },
459 | "dependencies": {
460 | "rimraf": {
461 | "version": "2.2.8",
462 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
463 | "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI="
464 | }
465 | }
466 | },
467 | "underscore": {
468 | "version": "1.6.0",
469 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
470 | "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag="
471 | },
472 | "underscore-plus": {
473 | "version": "1.6.6",
474 | "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.6.6.tgz",
475 | "integrity": "sha1-ZezeG9xEGjXYnmUP1w3PE65Dmn0=",
476 | "requires": {
477 | "underscore": "~1.6.0"
478 | }
479 | },
480 | "window-size": {
481 | "version": "0.1.4",
482 | "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz",
483 | "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY="
484 | },
485 | "wordwrap": {
486 | "version": "0.0.3",
487 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
488 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
489 | "dev": true
490 | },
491 | "wrap-ansi": {
492 | "version": "2.1.0",
493 | "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
494 | "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
495 | "requires": {
496 | "string-width": "^1.0.1",
497 | "strip-ansi": "^3.0.1"
498 | }
499 | },
500 | "wrappy": {
501 | "version": "1.0.2",
502 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
503 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
504 | },
505 | "y18n": {
506 | "version": "3.2.1",
507 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
508 | "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
509 | },
510 | "yargs": {
511 | "version": "3.32.0",
512 | "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
513 | "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=",
514 | "requires": {
515 | "camelcase": "^2.0.1",
516 | "cliui": "^3.0.3",
517 | "decamelize": "^1.1.1",
518 | "os-locale": "^1.4.0",
519 | "string-width": "^1.0.1",
520 | "window-size": "^0.1.4",
521 | "y18n": "^3.2.0"
522 | }
523 | }
524 | }
525 | }
526 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "snippets",
3 | "version": "1.6.0",
4 | "main": "./lib/snippets",
5 | "description": "Expand snippets matching the current prefix with `tab`.",
6 | "repository": "https://github.com/atom/snippets",
7 | "license": "MIT",
8 | "engines": {
9 | "atom": "*"
10 | },
11 | "dependencies": {
12 | "async": "~0.2.6",
13 | "atom-select-list": "^0.7.0",
14 | "fs-plus": "^3.0.0",
15 | "loophole": "^1",
16 | "pegjs": "~0.8.0",
17 | "scoped-property-store": "^0.17.0",
18 | "season": "^6.0.2",
19 | "temp": "~0.8.0",
20 | "underscore-plus": "^1.0.0"
21 | },
22 | "providedServices": {
23 | "snippets": {
24 | "description": "Snippets are text shortcuts that can be expanded to their definition.",
25 | "versions": {
26 | "0.1.0": "provideSnippets"
27 | }
28 | }
29 | },
30 | "devDependencies": {
31 | "coffeelint": "^1.9.7"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/spec/body-parser-spec.js:
--------------------------------------------------------------------------------
1 | const BodyParser = require('../lib/snippet-body-parser');
2 |
3 | describe("Snippet Body Parser", () => {
4 | it("breaks a snippet body into lines, with each line containing tab stops at the appropriate position", () => {
5 | const bodyTree = BodyParser.parse(`\
6 | the quick brown $1fox \${2:jumped \${3:over}
7 | }the \${4:lazy} dog\
8 | `
9 | );
10 |
11 | expect(bodyTree).toEqual([
12 | "the quick brown ",
13 | {index: 1, content: []},
14 | "fox ",
15 | {
16 | index: 2,
17 | content: [
18 | "jumped ",
19 | {index: 3, content: ["over"]},
20 | "\n"
21 | ],
22 | },
23 | "the ",
24 | {index: 4, content: ["lazy"]},
25 | " dog"
26 | ]);
27 | });
28 |
29 | it("removes interpolated variables in placeholder text (we don't currently support it)", () => {
30 | const bodyTree = BodyParser.parse("module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}}");
31 | expect(bodyTree).toEqual([
32 | "module ",
33 | {
34 | "index": 1,
35 | "content": ["ActiveRecord::", ""]
36 | }
37 | ]);
38 | });
39 |
40 | it("skips escaped tabstops", () => {
41 | const bodyTree = BodyParser.parse("snippet $1 escaped \\$2 \\\\$3");
42 | expect(bodyTree).toEqual([
43 | "snippet ",
44 | {
45 | index: 1,
46 | content: []
47 | },
48 | " escaped $2 \\",
49 | {
50 | index: 3,
51 | content: []
52 | }
53 | ]);
54 | });
55 |
56 | it("includes escaped right-braces", () => {
57 | const bodyTree = BodyParser.parse("snippet ${1:{\\}}");
58 | expect(bodyTree).toEqual([
59 | "snippet ",
60 | {
61 | index: 1,
62 | content: ["{}"]
63 | }
64 | ]);
65 | });
66 |
67 | it("parses a snippet with transformations", () => {
68 | const bodyTree = BodyParser.parse("<${1:p}>$0${1/f/F/}>");
69 | expect(bodyTree).toEqual([
70 | '<',
71 | {index: 1, content: ['p']},
72 | '>',
73 | {index: 0, content: []},
74 | '',
75 | {index: 1, content: [], substitution: {find: /f/g, replace: ['F']}},
76 | '>'
77 | ]);
78 | });
79 |
80 | it("parses a snippet with multiple tab stops with transformations", () => {
81 | const bodyTree = BodyParser.parse("${1:placeholder} ${1/(.)/\\u$1/} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2");
82 | expect(bodyTree).toEqual([
83 | {index: 1, content: ['placeholder']},
84 | ' ',
85 | {
86 | index: 1,
87 | content: [],
88 | substitution: {
89 | find: /(.)/g,
90 | replace: [
91 | {escape: 'u'},
92 | {backreference: 1}
93 | ]
94 | }
95 | },
96 | ' ',
97 | {index: 1, content: []},
98 | ' ',
99 | {index: 2, content: ['ANOTHER']},
100 | ' ',
101 | {
102 | index: 2,
103 | content: [],
104 | substitution: {
105 | find: /^(.*)$/g,
106 | replace: [
107 | {escape: 'L'},
108 | {backreference: 1}
109 | ]
110 | }
111 | },
112 | ' ',
113 | {index: 2, content: []},
114 | ]);
115 | });
116 |
117 |
118 | it("parses a snippet with transformations and mirrors", () => {
119 | const bodyTree = BodyParser.parse("${1:placeholder}\n${1/(.)/\\u$1/}\n$1");
120 | expect(bodyTree).toEqual([
121 | {index: 1, content: ['placeholder']},
122 | '\n',
123 | {
124 | index: 1,
125 | content: [],
126 | substitution: {
127 | find: /(.)/g,
128 | replace: [
129 | {escape: 'u'},
130 | {backreference: 1}
131 | ]
132 | }
133 | },
134 | '\n',
135 | {index: 1, content: []}
136 | ]);
137 | });
138 |
139 | it("parses a snippet with a format string and case-control flags", () => {
140 | const bodyTree = BodyParser.parse("<${1:p}>$0${1/(.)(.*)/\\u$1$2/}>");
141 | expect(bodyTree).toEqual([
142 | '<',
143 | {index: 1, content: ['p']},
144 | '>',
145 | {index: 0, content: []},
146 | '',
147 | {
148 | index: 1,
149 | content: [],
150 | substitution: {
151 | find: /(.)(.*)/g,
152 | replace: [
153 | {escape: 'u'},
154 | {backreference: 1},
155 | {backreference: 2}
156 | ]
157 | }
158 | },
159 | '>'
160 | ]);
161 | });
162 |
163 | it("parses a snippet with an escaped forward slash in a transform", () => {
164 | // Annoyingly, a forward slash needs to be double-backslashed just like the
165 | // other escapes.
166 | const bodyTree = BodyParser.parse("<${1:p}>$0${1/(.)\\/(.*)/\\u$1$2/}>");
167 | expect(bodyTree).toEqual([
168 | '<',
169 | {index: 1, content: ['p']},
170 | '>',
171 | {index: 0, content: []},
172 | '',
173 | {
174 | index: 1,
175 | content: [],
176 | substitution: {
177 | find: /(.)\/(.*)/g,
178 | replace: [
179 | {escape: 'u'},
180 | {backreference: 1},
181 | {backreference: 2}
182 | ]
183 | }
184 | },
185 | '>'
186 | ]);
187 | });
188 |
189 | it("parses a snippet with a placeholder that mirrors another tab stop's content", () => {
190 | const bodyTree = BodyParser.parse("$4console.${3:log}('${2:$1}', $1);$0");
191 | expect(bodyTree).toEqual([
192 | {index: 4, content: []},
193 | 'console.',
194 | {index: 3, content: ['log']},
195 | '(\'',
196 | {
197 | index: 2, content: [
198 | {index: 1, content: []}
199 | ]
200 | },
201 | '\', ',
202 | {index: 1, content: []},
203 | ');',
204 | {index: 0, content: []}
205 | ]);
206 | });
207 |
208 | it("parses a snippet with a placeholder that mixes text and tab stop references", () => {
209 | const bodyTree = BodyParser.parse("$4console.${3:log}('${2:uh $1}', $1);$0");
210 | expect(bodyTree).toEqual([
211 | {index: 4, content: []},
212 | 'console.',
213 | {index: 3, content: ['log']},
214 | '(\'',
215 | {
216 | index: 2, content: [
217 | 'uh ',
218 | {index: 1, content: []}
219 | ]
220 | },
221 | '\', ',
222 | {index: 1, content: []},
223 | ');',
224 | {index: 0, content: []}
225 | ]);
226 | });
227 | });
228 |
--------------------------------------------------------------------------------
/spec/fixtures/package-with-broken-snippets/snippets/.hidden-file:
--------------------------------------------------------------------------------
1 | I am hidden so I shouldn't be loaded
2 |
--------------------------------------------------------------------------------
/spec/fixtures/package-with-broken-snippets/snippets/invalid.json:
--------------------------------------------------------------------------------
1 | I am not a valid JSON file but that shouldn't cause a crisis
2 |
--------------------------------------------------------------------------------
/spec/fixtures/package-with-snippets/snippets/.hidden-file:
--------------------------------------------------------------------------------
1 | This is a hidden file. Don't even try to load it as a snippet
2 |
--------------------------------------------------------------------------------
/spec/fixtures/package-with-snippets/snippets/junk-file:
--------------------------------------------------------------------------------
1 | This file isn't CSON, but shouldn't be a big deal
--------------------------------------------------------------------------------
/spec/fixtures/package-with-snippets/snippets/test.cson:
--------------------------------------------------------------------------------
1 | ".test":
2 | "Test Snippet":
3 | prefix: "test"
4 | body: "testing 123"
5 | "Test Snippet With Description":
6 | prefix: "testd"
7 | body: "testing 456"
8 | description: "a description"
9 | descriptionMoreURL: "http://google.com"
10 | "Test Snippet With A Label On The Left":
11 | prefix: "testlabelleft"
12 | body: "testing 456"
13 | leftLabel: "a label"
14 | "Test Snippet With HTML Labels":
15 | prefix: "testhtmllabels"
16 | body: "testing 456"
17 | leftLabelHTML: "Label"
18 | rightLabelHTML: "Label"
19 |
20 | ".package-with-snippets-unique-scope":
21 | "Test Snippet":
22 | prefix: "test"
23 | body: "testing 123"
24 |
25 | ".source.js":
26 | "Overrides a core package's snippet":
27 | prefix: "log"
28 | body: "from-a-community-package"
29 |
--------------------------------------------------------------------------------
/spec/fixtures/sample.js:
--------------------------------------------------------------------------------
1 | var quicksort = function () {
2 | var sort = function(items) {
3 | if (items.length <= 1) return items;
4 | var pivot = items.shift(), current, left = [], right = [];
5 | while(items.length > 0) {
6 | current = items.shift();
7 | current < pivot ? left.push(current) : right.push(current);
8 | }
9 | return sort(left).concat(pivot).concat(sort(right));
10 | };
11 |
12 | return sort(Array.apply(this, arguments));
13 | };
14 |
--------------------------------------------------------------------------------
/spec/insertion-spec.js:
--------------------------------------------------------------------------------
1 | const Insertion = require('../lib/insertion')
2 | const { Range } = require('atom')
3 |
4 | const range = new Range(0, 0)
5 |
6 | describe('Insertion', () => {
7 | it('returns what it was given when it has no substitution', () => {
8 | let insertion = new Insertion({
9 | range,
10 | substitution: undefined
11 | })
12 | let transformed = insertion.transform('foo!')
13 |
14 | expect(transformed).toEqual('foo!')
15 | })
16 |
17 | it('transforms what it was given when it has a regex transformation', () => {
18 | let insertion = new Insertion({
19 | range,
20 | substitution: {
21 | find: /foo/g,
22 | replace: ['bar']
23 | }
24 | })
25 | let transformed = insertion.transform('foo!')
26 |
27 | expect(transformed).toEqual('bar!')
28 | })
29 |
30 | it('transforms the case of the next character when encountering a \\u or \\l flag', () => {
31 | let uInsertion = new Insertion({
32 | range,
33 | substitution: {
34 | find: /(.)(.)(.*)/g,
35 | replace: [
36 | { backreference: 1 },
37 | { escape: 'u' },
38 | { backreference: 2 },
39 | { backreference: 3 }
40 | ]
41 | }
42 | })
43 |
44 | expect(uInsertion.transform('foo!')).toEqual('fOo!')
45 | expect(uInsertion.transform('fOo!')).toEqual('fOo!')
46 | expect(uInsertion.transform('FOO!')).toEqual('FOO!')
47 |
48 | let lInsertion = new Insertion({
49 | range,
50 | substitution: {
51 | find: /(.{2})(.)(.*)/g,
52 | replace: [
53 | { backreference: 1 },
54 | { escape: 'l' },
55 | { backreference: 2 },
56 | { backreference: 3 }
57 | ]
58 | }
59 | })
60 |
61 | expect(lInsertion.transform('FOO!')).toEqual('FOo!')
62 | expect(lInsertion.transform('FOo!')).toEqual('FOo!')
63 | expect(lInsertion.transform('FoO!')).toEqual('Foo!')
64 | expect(lInsertion.transform('foo!')).toEqual('foo!')
65 | })
66 |
67 | it('transforms the case of all remaining characters when encountering a \\U or \\L flag, up until it sees a \\E flag', () => {
68 | let uInsertion = new Insertion({
69 | range,
70 | substitution: {
71 | find: /(.)(.*)/,
72 | replace: [
73 | { backreference: 1 },
74 | { escape: 'U' },
75 | { backreference: 2 }
76 | ]
77 | }
78 | })
79 |
80 | expect(uInsertion.transform('lorem ipsum!')).toEqual('lOREM IPSUM!')
81 | expect(uInsertion.transform('lOREM IPSUM!')).toEqual('lOREM IPSUM!')
82 | expect(uInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!')
83 |
84 | let ueInsertion = new Insertion({
85 | range,
86 | substitution: {
87 | find: /(.)(.{3})(.*)/,
88 | replace: [
89 | { backreference: 1 },
90 | { escape: 'U' },
91 | { backreference: 2 },
92 | { escape: 'E' },
93 | { backreference: 3 }
94 | ]
95 | }
96 | })
97 |
98 | expect(ueInsertion.transform('lorem ipsum!')).toEqual('lOREm ipsum!')
99 | expect(ueInsertion.transform('lOREm ipsum!')).toEqual('lOREm ipsum!')
100 | expect(ueInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!')
101 |
102 | let lInsertion = new Insertion({
103 | range,
104 | substitution: {
105 | find: /(.{4})(.)(.*)/,
106 | replace: [
107 | { backreference: 1 },
108 | { escape: 'L' },
109 | { backreference: 2 },
110 | 'WHAT'
111 | ]
112 | }
113 | })
114 |
115 | expect(lInsertion.transform('LOREM IPSUM!')).toEqual('LOREmwhat')
116 |
117 | let leInsertion = new Insertion({
118 | range,
119 | substitution: {
120 | find: /^([A-Fa-f])(.*)(.)$/,
121 | replace: [
122 | { backreference: 1 },
123 | { escape: 'L' },
124 | { backreference: 2 },
125 | { escape: 'E' },
126 | { backreference: 3 }
127 | ]
128 | }
129 | })
130 |
131 | expect(leInsertion.transform('LOREM IPSUM!')).toEqual('LOREM IPSUM!')
132 | expect(leInsertion.transform('CONSECUETUR')).toEqual('ConsecuetuR')
133 | })
134 | })
135 |
--------------------------------------------------------------------------------
/spec/snippet-loading-spec.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs-plus');
3 | const temp = require('temp').track();
4 |
5 | describe("Snippet Loading", () => {
6 | let configDirPath, snippetsService;
7 |
8 | beforeEach(() => {
9 | configDirPath = temp.mkdirSync('atom-config-dir-');
10 | spyOn(atom, 'getConfigDirPath').andReturn(configDirPath);
11 |
12 | spyOn(console, 'warn');
13 | if (atom.notifications != null) { spyOn(atom.notifications, 'addError'); }
14 |
15 | spyOn(atom.packages, 'getLoadedPackages').andReturn([
16 | atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-snippets')),
17 | atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-broken-snippets')),
18 | ]);
19 | });
20 |
21 | afterEach(() => {
22 | waitsForPromise(() => Promise.resolve(atom.packages.deactivatePackages('snippets')));
23 | runs(() => {
24 | jasmine.unspy(atom.packages, 'getLoadedPackages');
25 | });
26 | });
27 |
28 | const activateSnippetsPackage = () => {
29 | waitsForPromise(() => atom.packages.activatePackage("snippets").then(({mainModule}) => {
30 | snippetsService = mainModule.provideSnippets();
31 | mainModule.loaded = false;
32 | }));
33 |
34 | waitsFor("all snippets to load", 3000, () => snippetsService.bundledSnippetsLoaded());
35 | };
36 |
37 | it("loads the bundled snippet template snippets", () => {
38 | activateSnippetsPackage();
39 |
40 | runs(() => {
41 | const jsonSnippet = snippetsService.snippetsForScopes(['.source.json'])['snip'];
42 | expect(jsonSnippet.name).toBe('Atom Snippet');
43 | expect(jsonSnippet.prefix).toBe('snip');
44 | expect(jsonSnippet.body).toContain('"prefix":');
45 | expect(jsonSnippet.body).toContain('"body":');
46 | expect(jsonSnippet.tabStopList.length).toBeGreaterThan(0);
47 |
48 | const csonSnippet = snippetsService.snippetsForScopes(['.source.coffee'])['snip'];
49 | expect(csonSnippet.name).toBe('Atom Snippet');
50 | expect(csonSnippet.prefix).toBe('snip');
51 | expect(csonSnippet.body).toContain("'prefix':");
52 | expect(csonSnippet.body).toContain("'body':");
53 | expect(csonSnippet.tabStopList.length).toBeGreaterThan(0);
54 | });
55 | });
56 |
57 | it("loads non-hidden snippet files from atom packages with snippets directories", () => {
58 | activateSnippetsPackage();
59 |
60 | runs(() => {
61 | let snippet = snippetsService.snippetsForScopes(['.test'])['test'];
62 | expect(snippet.prefix).toBe('test');
63 | expect(snippet.body).toBe('testing 123');
64 |
65 | snippet = snippetsService.snippetsForScopes(['.test'])['testd'];
66 | expect(snippet.prefix).toBe('testd');
67 | expect(snippet.body).toBe('testing 456');
68 | expect(snippet.description).toBe('a description');
69 | expect(snippet.descriptionMoreURL).toBe('http://google.com');
70 |
71 | snippet = snippetsService.snippetsForScopes(['.test'])['testlabelleft'];
72 | expect(snippet.prefix).toBe('testlabelleft');
73 | expect(snippet.body).toBe('testing 456');
74 | expect(snippet.leftLabel).toBe('a label');
75 |
76 | snippet = snippetsService.snippetsForScopes(['.test'])['testhtmllabels'];
77 | expect(snippet.prefix).toBe('testhtmllabels');
78 | expect(snippet.body).toBe('testing 456');
79 | expect(snippet.leftLabelHTML).toBe('Label');
80 | expect(snippet.rightLabelHTML).toBe('Label');
81 | });
82 | });
83 |
84 | it("logs a warning if package snippets files cannot be parsed", () => {
85 | activateSnippetsPackage();
86 |
87 | runs(() => {
88 | // Warn about invalid-file, but don't even try to parse a hidden file
89 | expect(console.warn.calls.length).toBeGreaterThan(0);
90 | expect(console.warn.mostRecentCall.args[0]).toMatch(/Error reading.*package-with-broken-snippets/);
91 | });
92 | });
93 |
94 | describe("::loadPackageSnippets(callback)", () => {
95 | beforeEach(() => { // simulate a list of packages where the javascript core package is returned at the end
96 | atom.packages.getLoadedPackages.andReturn([
97 | atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-snippets')),
98 | atom.packages.loadPackage('language-javascript')
99 | ])
100 | });
101 |
102 | it("allows other packages to override core packages' snippets", () => {
103 | waitsForPromise(() => atom.packages.activatePackage("language-javascript"));
104 |
105 | activateSnippetsPackage();
106 |
107 | runs(() => {
108 | const snippet = snippetsService.snippetsForScopes(['.source.js'])['log'];
109 | expect(snippet.body).toBe("from-a-community-package");
110 | });
111 | });
112 | });
113 |
114 | describe("::onDidLoadSnippets(callback)", () => {
115 | it("invokes listeners when all snippets are loaded", () => {
116 | let loadedCallback = null;
117 |
118 | waitsFor("package to activate", done => atom.packages.activatePackage("snippets").then(({mainModule}) => {
119 | mainModule.onDidLoadSnippets(loadedCallback = jasmine.createSpy('onDidLoadSnippets callback'));
120 | done();
121 | }));
122 |
123 | waitsFor("onDidLoad callback to be called", () => loadedCallback.callCount > 0);
124 | });
125 | });
126 |
127 | describe("when ~/.atom/snippets.json exists", () => {
128 | beforeEach(() => {
129 | fs.writeFileSync(path.join(configDirPath, 'snippets.json'), `\
130 | {
131 | ".foo": {
132 | "foo snippet": {
133 | "prefix": "foo",
134 | "body": "bar1"
135 | }
136 | }
137 | }\
138 | `
139 | );
140 | activateSnippetsPackage();
141 | });
142 |
143 | it("loads the snippets from that file", () => {
144 | let snippet = null;
145 |
146 | waitsFor(() => snippet = snippetsService.snippetsForScopes(['.foo'])['foo']);
147 |
148 | runs(() => {
149 | expect(snippet.name).toBe('foo snippet');
150 | expect(snippet.prefix).toBe("foo");
151 | expect(snippet.body).toBe("bar1");
152 | });
153 | });
154 |
155 | describe("when that file changes", () => {
156 | it("reloads the snippets", () => {
157 | fs.writeFileSync(path.join(configDirPath, 'snippets.json'), `\
158 | {
159 | ".foo": {
160 | "foo snippet": {
161 | "prefix": "foo",
162 | "body": "bar2"
163 | }
164 | }
165 | }\
166 | `
167 | );
168 |
169 | waitsFor("snippets to be changed", () => {
170 | const snippet = snippetsService.snippetsForScopes(['.foo'])['foo'];
171 | return snippet && snippet.body === 'bar2';
172 | });
173 |
174 | runs(() => {
175 | fs.writeFileSync(path.join(configDirPath, 'snippets.json'), "");
176 | });
177 |
178 | waitsFor("snippets to be removed", () => !snippetsService.snippetsForScopes(['.foo'])['foo']);
179 | });
180 | });
181 | });
182 |
183 | describe("when ~/.atom/snippets.cson exists", () => {
184 | beforeEach(() => {
185 | fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), `\
186 | ".foo":
187 | "foo snippet":
188 | "prefix": "foo"
189 | "body": "bar1"\
190 | `
191 | );
192 | activateSnippetsPackage();
193 | });
194 |
195 | it("loads the snippets from that file", () => {
196 | let snippet = null;
197 |
198 | waitsFor(() => snippet = snippetsService.snippetsForScopes(['.foo'])['foo']);
199 |
200 | runs(() => {
201 | expect(snippet.name).toBe('foo snippet');
202 | expect(snippet.prefix).toBe("foo");
203 | expect(snippet.body).toBe("bar1");
204 | });
205 | });
206 |
207 | describe("when that file changes", () => {
208 | it("reloads the snippets", () => {
209 | fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), `\
210 | ".foo":
211 | "foo snippet":
212 | "prefix": "foo"
213 | "body": "bar2"\
214 | `
215 | );
216 |
217 | waitsFor("snippets to be changed", () => {
218 | const snippet = snippetsService.snippetsForScopes(['.foo'])['foo'];
219 | return snippet && snippet.body === 'bar2';
220 | });
221 |
222 | runs(() => {
223 | fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), "");
224 | });
225 |
226 | waitsFor("snippets to be removed", () => {
227 | const snippet = snippetsService.snippetsForScopes(['.foo'])['foo'];
228 | return snippet == null;
229 | });
230 | });
231 | });
232 | });
233 |
234 | it("notifies the user when the user snippets file cannot be loaded", () => {
235 | fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), '".junk":::');
236 |
237 | activateSnippetsPackage();
238 |
239 | runs(() => {
240 | expect(console.warn).toHaveBeenCalled();
241 | if (atom.notifications != null) {
242 | expect(atom.notifications.addError).toHaveBeenCalled();
243 | }
244 | });
245 | });
246 |
247 | describe("packages-with-snippets-disabled feature", () => {
248 | it("disables no snippets if the config option is empty", () => {
249 | const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled');
250 | atom.config.set('core.packagesWithSnippetsDisabled', []);
251 |
252 | activateSnippetsPackage();
253 | runs(() => {
254 | const snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']);
255 | expect(Object.keys(snippets).length).toBe(1);
256 | atom.config.set('core.packagesWithSnippetsDisabled', originalConfig);
257 | });
258 | });
259 |
260 | it("still includes a disabled package's snippets in the list of unparsed snippets", () => {
261 | let originalConfig = atom.config.get('core.packagesWithSnippetsDisabled');
262 | atom.config.set('core.packagesWithSnippetsDisabled', []);
263 |
264 | activateSnippetsPackage();
265 | runs(() => {
266 | atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']);
267 | const allSnippets = snippetsService.getUnparsedSnippets();
268 | const scopedSnippet = allSnippets.find(s => s.selectorString === '.package-with-snippets-unique-scope');
269 | expect(scopedSnippet).not.toBe(undefined);
270 | atom.config.set('core.packagesWithSnippetsDisabled', originalConfig);
271 | });
272 | });
273 |
274 | it("never loads a package's snippets when that package is disabled in config", () => {
275 | const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled');
276 | atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']);
277 |
278 | activateSnippetsPackage();
279 | runs(() => {
280 | const snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']);
281 | expect(Object.keys(snippets).length).toBe(0);
282 | atom.config.set('core.packagesWithSnippetsDisabled', originalConfig);
283 | });
284 | });
285 |
286 | it("unloads and/or reloads snippets from a package if the config option is changed after activation", () => {
287 | const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled');
288 | atom.config.set('core.packagesWithSnippetsDisabled', []);
289 |
290 | activateSnippetsPackage();
291 | runs(() => {
292 | let snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']);
293 | expect(Object.keys(snippets).length).toBe(1);
294 |
295 | // Disable it.
296 | atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']);
297 | snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']);
298 | expect(Object.keys(snippets).length).toBe(0);
299 |
300 | // Re-enable it.
301 | atom.config.set('core.packagesWithSnippetsDisabled', []);
302 | snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']);
303 | expect(Object.keys(snippets).length).toBe(1);
304 |
305 | atom.config.set('core.packagesWithSnippetsDisabled', originalConfig);
306 | });
307 | });
308 | });
309 | });
310 |
--------------------------------------------------------------------------------
/spec/snippets-spec.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const temp = require('temp').track();
3 | const Snippets = require('../lib/snippets');
4 | const {TextEditor} = require('atom');
5 |
6 | describe("Snippets extension", () => {
7 | let editorElement, editor;
8 |
9 | const simulateTabKeyEvent = (param) => {
10 | if (param == null) {
11 | param = {};
12 | }
13 | const {shift} = param;
14 | const event = atom.keymaps.constructor.buildKeydownEvent('tab', {shift, target: editorElement});
15 | atom.keymaps.handleKeyboardEvent(event);
16 | };
17 |
18 | beforeEach(() => {
19 | spyOn(Snippets, 'loadAll');
20 | spyOn(Snippets, 'getUserSnippetsPath').andReturn('');
21 |
22 | waitsForPromise(() => atom.workspace.open('sample.js'));
23 | waitsForPromise(() => atom.packages.activatePackage('language-javascript'));
24 | waitsForPromise(() => atom.packages.activatePackage('snippets'));
25 |
26 | runs(() => {
27 | editor = atom.workspace.getActiveTextEditor();
28 | editorElement = atom.views.getView(editor);
29 | });
30 | });
31 |
32 | afterEach(() => {
33 | waitsForPromise(() => atom.packages.deactivatePackage('snippets'));
34 | });
35 |
36 | describe("provideSnippets interface", () => {
37 | let snippetsInterface = null;
38 |
39 | beforeEach(() => {
40 | snippetsInterface = Snippets.provideSnippets();
41 | });
42 |
43 | describe("bundledSnippetsLoaded", () => {
44 | it("indicates the loaded state of the bundled snippets", () => {
45 | expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false);
46 | Snippets.doneLoading();
47 | expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true);
48 | });
49 |
50 | it("resets the loaded state after snippets is deactivated", () => {
51 | expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false);
52 | Snippets.doneLoading();
53 | expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true);
54 |
55 | waitsForPromise(() => atom.packages.deactivatePackage('snippets'));
56 | waitsForPromise(() => atom.packages.activatePackage('snippets'));
57 |
58 | runs(() => {
59 | expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false);
60 | Snippets.doneLoading();
61 | expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true);
62 | });
63 | });
64 | });
65 |
66 | describe("insertSnippet", () => {
67 | it("can insert a snippet", () => {
68 | editor.setSelectedBufferRange([[0, 4], [0, 13]]);
69 | snippetsInterface.insertSnippet("hello ${1:world}", editor);
70 | expect(editor.lineTextForBufferRow(0)).toBe("var hello world = function () {");
71 | });
72 | });
73 | });
74 |
75 | it("returns false for snippetToExpandUnderCursor if getSnippets returns {}", () => {
76 | const snippets = atom.packages.getActivePackage('snippets').mainModule;
77 | expect(snippets.snippetToExpandUnderCursor(editor)).toEqual(false);
78 | });
79 |
80 | it("ignores invalid snippets in the config", () => {
81 | const snippets = atom.packages.getActivePackage('snippets').mainModule;
82 |
83 | let invalidSnippets = null;
84 | spyOn(snippets.scopedPropertyStore, 'getPropertyValue').andCallFake(() => invalidSnippets);
85 | expect(snippets.getSnippets(editor)).toEqual({});
86 |
87 | invalidSnippets = 'test';
88 | expect(snippets.getSnippets(editor)).toEqual({});
89 |
90 | invalidSnippets = [];
91 | expect(snippets.getSnippets(editor)).toEqual({});
92 |
93 | invalidSnippets = 3;
94 | expect(snippets.getSnippets(editor)).toEqual({});
95 |
96 | invalidSnippets = {a: null};
97 | expect(snippets.getSnippets(editor)).toEqual({});
98 | });
99 |
100 | describe("when null snippets are present", () => {
101 | beforeEach(() => Snippets.add(__filename, {
102 | ".source.js": {
103 | "some snippet": {
104 | prefix: "t1",
105 | body: "this is a test"
106 | }
107 | },
108 |
109 | ".source.js .nope": {
110 | "some snippet": {
111 | prefix: "t1",
112 | body: null
113 | }
114 | }
115 | }));
116 |
117 | it("overrides the less-specific defined snippet", () => {
118 | const snippets = Snippets.provideSnippets();
119 | expect(snippets.snippetsForScopes(['.source.js'])['t1']).toBeTruthy();
120 | expect(snippets.snippetsForScopes(['.source.js .nope.not-today'])['t1']).toBeFalsy();
121 | });
122 | });
123 |
124 | describe("when 'tab' is triggered on the editor", () => {
125 | beforeEach(() => {
126 | Snippets.add(__filename, {
127 | ".source.js": {
128 | "without tab stops": {
129 | prefix: "t1",
130 | body: "this is a test"
131 | },
132 |
133 | "with only an end tab stop": {
134 | prefix: "t1a",
135 | body: "something $0 strange"
136 | },
137 |
138 | "overlapping prefix": {
139 | prefix: "tt1",
140 | body: "this is another test"
141 | },
142 |
143 | "special chars": {
144 | prefix: "@unique",
145 | body: "@unique see"
146 | },
147 |
148 | "tab stops": {
149 | prefix: "t2",
150 | body: `\
151 | go here next:($2) and finally go here:($0)
152 | go here first:($1)
153 | \
154 | `
155 | },
156 |
157 | "indented second line": {
158 | prefix: "t3",
159 | body: `\
160 | line 1
161 | \tline 2$1
162 | $2\
163 | `
164 | },
165 |
166 | "multiline with indented placeholder tabstop": {
167 | prefix: "t4",
168 | body: `\
169 | line \${1:1}
170 | \${2:body...}\
171 | `
172 | },
173 |
174 | "multiline starting with tabstop": {
175 | prefix: "t4b",
176 | body: `\
177 | $1 = line 1 {
178 | line 2
179 | }\
180 | `
181 | },
182 |
183 | "nested tab stops": {
184 | prefix: "t5",
185 | body: '${1:"${2:key}"}: ${3:value}'
186 | },
187 |
188 | "caused problems with undo": {
189 | prefix: "t6",
190 | body: `\
191 | first line$1
192 | \${2:placeholder ending second line}\
193 | `
194 | },
195 |
196 | "tab stops at beginning and then end of snippet": {
197 | prefix: "t6b",
198 | body: "$1expanded$0"
199 | },
200 |
201 | "tab stops at end and then beginning of snippet": {
202 | prefix: "t6c",
203 | body: "$0expanded$1"
204 | },
205 |
206 | "contains empty lines": {
207 | prefix: "t7",
208 | body: `\
209 | first line $1
210 |
211 |
212 | fourth line after blanks $2\
213 | `
214 | },
215 | "with/without placeholder": {
216 | prefix: "t8",
217 | body: `\
218 | with placeholder \${1:test}
219 | without placeholder \${2}\
220 | `
221 | },
222 |
223 | "multi-caret": {
224 | prefix: "t9",
225 | body: `\
226 | with placeholder \${1:test}
227 | without placeholder $1\
228 | `
229 | },
230 |
231 | "multi-caret-multi-tabstop": {
232 | prefix: "t9b",
233 | body: `\
234 | with placeholder \${1:test}
235 | without placeholder $1
236 | second tabstop $2
237 | third tabstop $3\
238 | `
239 | },
240 |
241 | "large indices": {
242 | prefix: "t10",
243 | body: "hello${10} ${11:large} indices${1}"
244 | },
245 |
246 | "no body": {
247 | prefix: "bad1"
248 | },
249 |
250 | "number body": {
251 | prefix: "bad2",
252 | body: 100
253 | },
254 |
255 | "many tabstops": {
256 | prefix: "t11",
257 | body: "$0one${1} ${2:two} three${3}"
258 | },
259 |
260 | "simple transform": {
261 | prefix: "t12",
262 | body: "[${1:b}][/${1/[ ]+.*$//}]"
263 | },
264 | "transform with non-transforming mirrors": {
265 | prefix: "t13",
266 | body: "${1:placeholder}\n${1/(.)/\\u$1/}\n$1"
267 | },
268 | "multiple tab stops, some with transforms and some without": {
269 | prefix: "t14",
270 | body: "${1:placeholder} ${1/(.)/\\u$1/} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2"
271 | },
272 | "has a transformed tab stop without a corresponding ordinary tab stop": {
273 | prefix: 't15',
274 | body: "${1/(.)/\\u$1/} & $2"
275 | },
276 | "has a transformed tab stop that occurs before the corresponding ordinary tab stop": {
277 | prefix: 't16',
278 | body: "& ${1/(.)/\\u$1/} & ${1:q}"
279 | },
280 | "has a placeholder that mirrors another tab stop's content": {
281 | prefix: 't17',
282 | body: "$4console.${3:log}('${2:uh $1}', $1);$0"
283 | },
284 | "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": {
285 | prefix: 't18',
286 | body: '// $1\n// ${1/./=/}'
287 | },
288 | "has two tab stops adjacent to one another": {
289 | prefix: 't19',
290 | body: '${2:bar}${3:baz}'
291 | },
292 | "has several adjacent tab stops, one of which has a placeholder with reference to another tab stop at its edge": {
293 | prefix: 't20',
294 | body: '${1:foo}${2:bar}${3:baz $1}$4'
295 | }
296 | }
297 | });
298 | });
299 |
300 | it("parses snippets once, reusing cached ones on subsequent queries", () => {
301 | spyOn(Snippets, "getBodyParser").andCallThrough();
302 |
303 | editor.insertText("t1");
304 | simulateTabKeyEvent();
305 |
306 | expect(Snippets.getBodyParser).toHaveBeenCalled();
307 | expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {");
308 | expect(editor.getCursorScreenPosition()).toEqual([0, 14]);
309 |
310 | Snippets.getBodyParser.reset();
311 |
312 | editor.setText("");
313 | editor.insertText("t1");
314 | simulateTabKeyEvent();
315 |
316 | expect(Snippets.getBodyParser).not.toHaveBeenCalled();
317 | expect(editor.lineTextForBufferRow(0)).toBe("this is a test");
318 | expect(editor.getCursorScreenPosition()).toEqual([0, 14]);
319 |
320 | Snippets.getBodyParser.reset();
321 |
322 | Snippets.add(__filename, {
323 | ".source.js": {
324 | "invalidate previous snippet": {
325 | prefix: "t1",
326 | body: "new snippet"
327 | }
328 | }
329 | });
330 |
331 | editor.setText("");
332 | editor.insertText("t1");
333 | simulateTabKeyEvent();
334 |
335 | expect(Snippets.getBodyParser).toHaveBeenCalled();
336 | expect(editor.lineTextForBufferRow(0)).toBe("new snippet");
337 | expect(editor.getCursorScreenPosition()).toEqual([0, 11]);
338 | });
339 |
340 | describe("when the snippet body is invalid or missing", () => {
341 | it("does not register the snippet", () => {
342 | editor.setText('');
343 | editor.insertText('bad1');
344 | atom.commands.dispatch(editorElement, 'snippets:expand');
345 | expect(editor.getText()).toBe('bad1');
346 |
347 | editor.setText('');
348 | editor.setText('bad2');
349 | atom.commands.dispatch(editorElement, 'snippets:expand');
350 | expect(editor.getText()).toBe('bad2');
351 | });
352 | });
353 |
354 | describe("when the letters preceding the cursor trigger a snippet", () => {
355 | describe("when the snippet contains no tab stops", () => {
356 | it("replaces the prefix with the snippet text and places the cursor at its end", () => {
357 | editor.insertText("t1");
358 | expect(editor.getCursorScreenPosition()).toEqual([0, 2]);
359 |
360 | simulateTabKeyEvent();
361 | expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {");
362 | expect(editor.getCursorScreenPosition()).toEqual([0, 14]);
363 | });
364 |
365 | it("inserts a real tab the next time a tab is pressed after the snippet is expanded", () => {
366 | editor.insertText("t1");
367 | simulateTabKeyEvent();
368 | expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {");
369 | simulateTabKeyEvent();
370 | expect(editor.lineTextForBufferRow(0)).toBe("this is a test var quicksort = function () {");
371 | });
372 | });
373 |
374 | describe("when the snippet contains tab stops", () => {
375 | it("places the cursor at the first tab-stop, and moves the cursor in response to 'next-tab-stop' events", () => {
376 | const markerCountBefore = editor.getMarkerCount();
377 | editor.setCursorScreenPosition([2, 0]);
378 | editor.insertText('t2');
379 | simulateTabKeyEvent();
380 | expect(editor.lineTextForBufferRow(2)).toBe("go here next:() and finally go here:()");
381 | expect(editor.lineTextForBufferRow(3)).toBe("go here first:()");
382 | expect(editor.lineTextForBufferRow(4)).toBe(" if (items.length <= 1) return items;");
383 | expect(editor.getSelectedBufferRange()).toEqual([[3, 15], [3, 15]]);
384 |
385 | simulateTabKeyEvent();
386 | expect(editor.getSelectedBufferRange()).toEqual([[2, 14], [2, 14]]);
387 | editor.insertText('abc');
388 |
389 | simulateTabKeyEvent();
390 | expect(editor.getSelectedBufferRange()).toEqual([[2, 40], [2, 40]]);
391 |
392 | // tab backwards
393 | simulateTabKeyEvent({shift: true});
394 | expect(editor.getSelectedBufferRange()).toEqual([[2, 14], [2, 17]]); // should highlight text typed at tab stop
395 |
396 | simulateTabKeyEvent({shift: true});
397 | expect(editor.getSelectedBufferRange()).toEqual([[3, 15], [3, 15]]);
398 |
399 | // shift-tab on first tab-stop does nothing
400 | simulateTabKeyEvent({shift: true});
401 | expect(editor.getCursorScreenPosition()).toEqual([3, 15]);
402 |
403 | // tab through all tab stops, then tab on last stop to terminate snippet
404 | simulateTabKeyEvent();
405 | simulateTabKeyEvent();
406 | simulateTabKeyEvent();
407 | expect(editor.lineTextForBufferRow(2)).toBe("go here next:(abc) and finally go here:( )");
408 | expect(editor.getMarkerCount()).toBe(markerCountBefore);
409 | });
410 |
411 | describe("when tab stops are nested", () => {
412 | it("destroys the inner tab stop if the outer tab stop is modified", () => {
413 | editor.setText('');
414 | editor.insertText('t5');
415 | atom.commands.dispatch(editorElement, 'snippets:expand');
416 | expect(editor.lineTextForBufferRow(0)).toBe('"key": value');
417 | expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 5]]);
418 | editor.insertText("foo");
419 | simulateTabKeyEvent();
420 | expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 10]]);
421 | });
422 | });
423 |
424 | describe("when the only tab stop is an end stop", () => {
425 | it("terminates the snippet immediately after moving the cursor to the end stop", () => {
426 | editor.setText('');
427 | editor.insertText('t1a');
428 | simulateTabKeyEvent();
429 |
430 | expect(editor.lineTextForBufferRow(0)).toBe("something strange");
431 | expect(editor.getCursorBufferPosition()).toEqual([0, 10]);
432 |
433 | simulateTabKeyEvent();
434 | expect(editor.lineTextForBufferRow(0)).toBe("something strange");
435 | expect(editor.getCursorBufferPosition()).toEqual([0, 12]);
436 | });
437 | });
438 |
439 | describe("when tab stops are separated by blank lines", () => {
440 | it("correctly places the tab stops (regression)", () => {
441 | editor.setText('');
442 | editor.insertText('t7');
443 | atom.commands.dispatch(editorElement, 'snippets:expand');
444 | atom.commands.dispatch(editorElement, 'snippets:next-tab-stop');
445 | expect(editor.getCursorBufferPosition()).toEqual([3, 25]);
446 | });
447 | });
448 |
449 | describe("when the cursor is moved beyond the bounds of the current tab stop", () => {
450 | it("terminates the snippet", () => {
451 | editor.setCursorScreenPosition([2, 0]);
452 | editor.insertText('t2');
453 | simulateTabKeyEvent();
454 |
455 | editor.moveUp();
456 | editor.moveLeft();
457 | simulateTabKeyEvent();
458 |
459 | expect(editor.lineTextForBufferRow(2)).toBe("go here next:( ) and finally go here:()");
460 | expect(editor.getCursorBufferPosition()).toEqual([2, 16]);
461 |
462 | // test we can terminate with shift-tab
463 | editor.setCursorScreenPosition([4, 0]);
464 | editor.insertText('t2');
465 | simulateTabKeyEvent();
466 | simulateTabKeyEvent();
467 |
468 | editor.moveRight();
469 | simulateTabKeyEvent({shift: true});
470 | expect(editor.getCursorBufferPosition()).toEqual([4, 15]);
471 | });
472 | });
473 |
474 | describe("when the cursor is moved within the bounds of the current tab stop", () => {
475 | it("should not terminate the snippet", () => {
476 | editor.setCursorScreenPosition([0, 0]);
477 | editor.insertText('t8');
478 | simulateTabKeyEvent();
479 |
480 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test");
481 | editor.moveRight();
482 | editor.moveLeft();
483 | editor.insertText("foo");
484 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder tesfoot");
485 |
486 | simulateTabKeyEvent();
487 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {");
488 | editor.insertText("test");
489 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder testvar quicksort = function () {");
490 | editor.moveLeft();
491 | editor.insertText("foo");
492 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder tesfootvar quicksort = function () {");
493 | });
494 | });
495 |
496 | describe("when the backspace is press within the bounds of the current tab stop", () => {
497 | it("should not terminate the snippet", () => {
498 | editor.setCursorScreenPosition([0, 0]);
499 | editor.insertText('t8');
500 | simulateTabKeyEvent();
501 |
502 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test");
503 | editor.moveRight();
504 | editor.backspace();
505 | editor.insertText("foo");
506 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder tesfoo");
507 |
508 | simulateTabKeyEvent();
509 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {");
510 | editor.insertText("test");
511 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder testvar quicksort = function () {");
512 | editor.backspace();
513 | editor.insertText("foo");
514 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder tesfoovar quicksort = function () {");
515 | });
516 | });
517 | });
518 |
519 | describe("when the snippet contains hard tabs", () => {
520 | describe("when the edit session is in soft-tabs mode", () => {
521 | it("translates hard tabs in the snippet to the appropriate number of spaces", () => {
522 | expect(editor.getSoftTabs()).toBeTruthy();
523 | editor.insertText("t3");
524 | simulateTabKeyEvent();
525 | expect(editor.lineTextForBufferRow(1)).toBe(" line 2");
526 | expect(editor.getCursorBufferPosition()).toEqual([1, 8]);
527 | });
528 | });
529 |
530 | describe("when the edit session is in hard-tabs mode", () => {
531 | it("inserts hard tabs in the snippet directly", () => {
532 | editor.setSoftTabs(false);
533 | editor.insertText("t3");
534 | simulateTabKeyEvent();
535 | expect(editor.lineTextForBufferRow(1)).toBe("\tline 2");
536 | expect(editor.getCursorBufferPosition()).toEqual([1, 7]);
537 | });
538 | });
539 | });
540 |
541 | describe("when the snippet prefix is indented", () => {
542 | describe("when the snippet spans a single line", () => {
543 | it("does not indent the next line", () => {
544 | editor.setCursorScreenPosition([2, Infinity]);
545 | editor.insertText(' t1');
546 | atom.commands.dispatch(editorElement, 'snippets:expand');
547 | expect(editor.lineTextForBufferRow(3)).toBe(" var pivot = items.shift(), current, left = [], right = [];");
548 | });
549 | });
550 |
551 | describe("when the snippet spans multiple lines", () => {
552 | it("indents the subsequent lines of the snippet to be even with the start of the first line", () => {
553 | expect(editor.getSoftTabs()).toBeTruthy();
554 | editor.setCursorScreenPosition([2, Infinity]);
555 | editor.insertText(' t3');
556 | atom.commands.dispatch(editorElement, 'snippets:expand');
557 | expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items; line 1");
558 | expect(editor.lineTextForBufferRow(3)).toBe(" line 2");
559 | expect(editor.getCursorBufferPosition()).toEqual([3, 12]);
560 | });
561 | });
562 | });
563 |
564 | describe("when the snippet spans multiple lines", () => {
565 | beforeEach(() => {
566 | editor.update({autoIndent: true});
567 | // editor.update() returns a Promise that never gets resolved, so we
568 | // need to return undefined to avoid a timeout in the spec.
569 | // TODO: Figure out why `editor.update({autoIndent: true})` never gets resolved.
570 | });
571 |
572 | it("places tab stops correctly", () => {
573 | expect(editor.getSoftTabs()).toBeTruthy();
574 | editor.setCursorScreenPosition([2, Infinity]);
575 | editor.insertText(' t3');
576 | atom.commands.dispatch(editorElement, 'snippets:expand');
577 | expect(editor.getCursorBufferPosition()).toEqual([3, 12]);
578 | atom.commands.dispatch(editorElement, 'snippets:next-tab-stop');
579 | expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
580 | });
581 |
582 | it("indents the subsequent lines of the snippet based on the indent level before the snippet is inserted", () => {
583 | editor.setCursorScreenPosition([2, Infinity]);
584 | editor.insertNewline();
585 | editor.insertText('t4b');
586 | atom.commands.dispatch(editorElement, 'snippets:expand');
587 |
588 | expect(editor.lineTextForBufferRow(3)).toBe(" = line 1 {"); // 4 + 1 spaces (because the tab stop is invisible)
589 | expect(editor.lineTextForBufferRow(4)).toBe(" line 2");
590 | expect(editor.lineTextForBufferRow(5)).toBe(" }");
591 | expect(editor.getCursorBufferPosition()).toEqual([3, 4]);
592 | });
593 |
594 | it("does not change the relative positioning of the tab stops when inserted multiple times", () => {
595 | editor.setCursorScreenPosition([2, Infinity]);
596 | editor.insertNewline();
597 | editor.insertText('t4');
598 | atom.commands.dispatch(editorElement, 'snippets:expand');
599 |
600 | expect(editor.getSelectedBufferRange()).toEqual([[3, 9], [3, 10]]);
601 | atom.commands.dispatch(editorElement, 'snippets:next-tab-stop');
602 | expect(editor.getSelectedBufferRange()).toEqual([[4, 6], [4, 13]]);
603 |
604 | editor.insertText('t4');
605 | atom.commands.dispatch(editorElement, 'snippets:expand');
606 |
607 | expect(editor.getSelectedBufferRange()).toEqual([[4, 11], [4, 12]]);
608 | atom.commands.dispatch(editorElement, 'snippets:next-tab-stop');
609 | expect(editor.getSelectedBufferRange()).toEqual([[5, 8], [5, 15]]);
610 |
611 | editor.setText(''); // Clear editor
612 | editor.insertText('t4');
613 | atom.commands.dispatch(editorElement, 'snippets:expand');
614 |
615 | expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 6]]);
616 | atom.commands.dispatch(editorElement, 'snippets:next-tab-stop');
617 | expect(editor.getSelectedBufferRange()).toEqual([[1, 2], [1, 9]]);
618 | });
619 | });
620 |
621 | describe("when multiple snippets match the prefix", () => {
622 | it("expands the snippet that is the longest match for the prefix", () => {
623 | editor.insertText('t113');
624 | expect(editor.getCursorScreenPosition()).toEqual([0, 4]);
625 |
626 | simulateTabKeyEvent();
627 | expect(editor.lineTextForBufferRow(0)).toBe("t113 var quicksort = function () {");
628 | expect(editor.getCursorScreenPosition()).toEqual([0, 6]);
629 |
630 | editor.undo();
631 | editor.undo();
632 |
633 | editor.insertText("tt1");
634 | expect(editor.getCursorScreenPosition()).toEqual([0, 3]);
635 |
636 | simulateTabKeyEvent();
637 | expect(editor.lineTextForBufferRow(0)).toBe("this is another testvar quicksort = function () {");
638 | expect(editor.getCursorScreenPosition()).toEqual([0, 20]);
639 |
640 | editor.undo();
641 | editor.undo();
642 |
643 | editor.insertText("@t1");
644 | expect(editor.getCursorScreenPosition()).toEqual([0, 3]);
645 |
646 | simulateTabKeyEvent();
647 | expect(editor.lineTextForBufferRow(0)).toBe("@this is a testvar quicksort = function () {");
648 | expect(editor.getCursorScreenPosition()).toEqual([0, 15]);
649 | });
650 | });
651 | });
652 |
653 | describe("when the word preceding the cursor ends with a snippet prefix", () => {
654 | it("inserts a tab as normal", () => {
655 | editor.insertText("t1t1t1");
656 | simulateTabKeyEvent();
657 | expect(editor.lineTextForBufferRow(0)).toBe("t1t1t1 var quicksort = function () {");
658 | });
659 | });
660 |
661 | describe("when the letters preceding the cursor don't match a snippet", () => {
662 | it("inserts a tab as normal", () => {
663 | editor.insertText("xxte");
664 | expect(editor.getCursorScreenPosition()).toEqual([0, 4]);
665 |
666 | simulateTabKeyEvent();
667 | expect(editor.lineTextForBufferRow(0)).toBe("xxte var quicksort = function () {");
668 | expect(editor.getCursorScreenPosition()).toEqual([0, 6]);
669 | });
670 | });
671 |
672 | describe("when text is selected", () => {
673 | it("inserts a tab as normal", () => {
674 | editor.insertText("t1");
675 | editor.setSelectedBufferRange([[0, 0], [0, 2]]);
676 |
677 | simulateTabKeyEvent();
678 | expect(editor.lineTextForBufferRow(0)).toBe(" t1var quicksort = function () {");
679 | expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 4]]);
680 | });
681 | });
682 |
683 | describe("when a previous snippet expansion has just been undone", () => {
684 | describe("when the tab stops appear in the middle of the snippet", () => {
685 | it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => {
686 | editor.insertText('t6\n');
687 | editor.setCursorBufferPosition([0, 2]);
688 | simulateTabKeyEvent();
689 | expect(editor.lineTextForBufferRow(0)).toBe("first line");
690 | editor.undo();
691 | expect(editor.lineTextForBufferRow(0)).toBe("t6");
692 | simulateTabKeyEvent();
693 | expect(editor.lineTextForBufferRow(0)).toBe("first line");
694 | });
695 | });
696 |
697 | describe("when the tab stops appear at the beginning and then the end of snippet", () => {
698 | it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => {
699 | editor.insertText('t6b\n');
700 | editor.setCursorBufferPosition([0, 3]);
701 | simulateTabKeyEvent();
702 | expect(editor.lineTextForBufferRow(0)).toBe("expanded");
703 | editor.undo();
704 | expect(editor.lineTextForBufferRow(0)).toBe("t6b");
705 | simulateTabKeyEvent();
706 | expect(editor.lineTextForBufferRow(0)).toBe("expanded");
707 | expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
708 | });
709 | });
710 |
711 | describe("when the tab stops appear at the end and then the beginning of snippet", () => {
712 | it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => {
713 | editor.insertText('t6c\n');
714 | editor.setCursorBufferPosition([0, 3]);
715 | simulateTabKeyEvent();
716 | expect(editor.lineTextForBufferRow(0)).toBe("expanded");
717 | editor.undo();
718 | expect(editor.lineTextForBufferRow(0)).toBe("t6c");
719 | simulateTabKeyEvent();
720 | expect(editor.lineTextForBufferRow(0)).toBe("expanded");
721 | expect(editor.getCursorBufferPosition()).toEqual([0, 8]);
722 | });
723 | });
724 | });
725 |
726 | describe("when the prefix contains non-word characters", () => {
727 | it("selects the non-word characters as part of the prefix", () => {
728 | editor.insertText("@unique");
729 | expect(editor.getCursorScreenPosition()).toEqual([0, 7]);
730 |
731 | simulateTabKeyEvent();
732 | expect(editor.lineTextForBufferRow(0)).toBe("@unique seevar quicksort = function () {");
733 | expect(editor.getCursorScreenPosition()).toEqual([0, 11]);
734 |
735 | editor.setCursorBufferPosition([10, 0]);
736 | editor.insertText("'@unique");
737 |
738 | simulateTabKeyEvent();
739 | expect(editor.lineTextForBufferRow(10)).toBe("'@unique see");
740 | expect(editor.getCursorScreenPosition()).toEqual([10, 12]);
741 | });
742 |
743 | it("does not select the whitespace before the prefix", () => {
744 | editor.insertText("a; @unique");
745 | expect(editor.getCursorScreenPosition()).toEqual([0, 10]);
746 |
747 | simulateTabKeyEvent();
748 | expect(editor.lineTextForBufferRow(0)).toBe("a; @unique seevar quicksort = function () {");
749 | expect(editor.getCursorScreenPosition()).toEqual([0, 14]);
750 | });
751 | });
752 |
753 | describe("when snippet contains tabstops with or without placeholder", () => {
754 | it("should create two markers", () => {
755 | editor.setCursorScreenPosition([0, 0]);
756 | editor.insertText('t8');
757 | simulateTabKeyEvent();
758 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test");
759 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {");
760 |
761 | expect(editor.getSelectedBufferRange()).toEqual([[0, 17], [0, 21]]);
762 |
763 | simulateTabKeyEvent();
764 | expect(editor.getSelectedBufferRange()).toEqual([[1, 20], [1, 20]]);
765 | });
766 | });
767 |
768 | describe("when snippet contains multi-caret tabstops with or without placeholder", () => {
769 | it("should create two markers", () => {
770 | editor.setCursorScreenPosition([0, 0]);
771 | editor.insertText('t9');
772 | simulateTabKeyEvent();
773 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test");
774 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {");
775 | editor.insertText('hello');
776 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder hello");
777 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder hellovar quicksort = function () {");
778 | });
779 |
780 | it("terminates the snippet when cursors are destroyed", () => {
781 | editor.setCursorScreenPosition([0, 0]);
782 | editor.insertText('t9b');
783 | simulateTabKeyEvent();
784 | editor.getCursors()[0].destroy();
785 | editor.getCursorBufferPosition();
786 | simulateTabKeyEvent();
787 |
788 | expect(editor.lineTextForBufferRow(1)).toEqual("without placeholder ");
789 | });
790 |
791 | it("terminates the snippet expansion if a new cursor moves outside the bounds of the tab stops", () => {
792 | editor.setCursorScreenPosition([0, 0]);
793 | editor.insertText('t9b');
794 | simulateTabKeyEvent();
795 | editor.insertText('test');
796 |
797 | editor.getCursors()[0].destroy();
798 | editor.moveDown(); // this should destroy the previous expansion
799 | editor.moveToBeginningOfLine();
800 |
801 | // this should insert whitespace instead of going through tabstops of the previous destroyed snippet
802 | simulateTabKeyEvent();
803 | expect(editor.lineTextForBufferRow(2).indexOf(" second")).toBe(0);
804 | });
805 |
806 | it("moves to the second tabstop after a multi-caret tabstop", () => {
807 | editor.setCursorScreenPosition([0, 0]);
808 | editor.insertText('t9b');
809 | simulateTabKeyEvent();
810 | editor.insertText('line 1');
811 |
812 | simulateTabKeyEvent();
813 | editor.insertText('line 2');
814 |
815 | simulateTabKeyEvent();
816 | editor.insertText('line 3');
817 |
818 | expect(editor.lineTextForBufferRow(2).indexOf("line 2 ")).toBe(-1);
819 | });
820 |
821 | it("mirrors input properly when a tabstop's placeholder refers to another tabstop", () => {
822 | editor.setText('t17');
823 | editor.setCursorScreenPosition([0, 3]);
824 | simulateTabKeyEvent();
825 | editor.insertText("foo");
826 | expect(editor.getText()).toBe("console.log('uh foo', foo);");
827 | simulateTabKeyEvent();
828 | editor.insertText("bar");
829 | expect(editor.getText()).toBe("console.log('bar', foo);");
830 | });
831 | });
832 |
833 | describe("when the snippet contains tab stops with transformations", () => {
834 | it("transforms the text typed into the first tab stop before setting it in the transformed tab stop", () => {
835 | editor.setText('t12');
836 | editor.setCursorScreenPosition([0, 3]);
837 | simulateTabKeyEvent();
838 | expect(editor.getText()).toBe("[b][/b]");
839 | editor.insertText('img src');
840 | expect(editor.getText()).toBe("[img src][/img]");
841 | });
842 |
843 | it("bundles the transform mutations along with the original manual mutation for the purposes of undo and redo", () => {
844 | editor.setText('t12');
845 | editor.setCursorScreenPosition([0, 3]);
846 | simulateTabKeyEvent();
847 | editor.insertText('i');
848 | expect(editor.getText()).toBe("[i][/i]");
849 |
850 | editor.insertText('mg src');
851 | expect(editor.getText()).toBe("[img src][/img]");
852 |
853 | editor.undo();
854 | expect(editor.getText()).toBe("[i][/i]");
855 |
856 | editor.redo();
857 | expect(editor.getText()).toBe("[img src][/img]");
858 | });
859 |
860 | it("can pick the right insertion to use as the primary even if a transformed insertion occurs first in the snippet", () => {
861 | editor.setText('t16');
862 | editor.setCursorScreenPosition([0, 3]);
863 | simulateTabKeyEvent();
864 | expect(editor.lineTextForBufferRow(0)).toBe("& Q & q");
865 | expect(editor.getCursorBufferPosition()).toEqual([0, 7]);
866 |
867 | editor.insertText('rst');
868 | expect(editor.lineTextForBufferRow(0)).toBe("& RST & rst");
869 | });
870 |
871 | it("silently ignores a tab stop without a non-transformed insertion to use as the primary", () => {
872 | editor.setText('t15');
873 | editor.setCursorScreenPosition([0, 3]);
874 | simulateTabKeyEvent();
875 | editor.insertText('a');
876 | expect(editor.lineTextForBufferRow(0)).toBe(" & a");
877 | expect(editor.getCursorBufferPosition()).toEqual([0, 4]);
878 | });
879 | });
880 |
881 | describe("when the snippet contains mirrored tab stops and tab stops with transformations", () => {
882 | it("adds cursors for the mirrors but not the transformations", () => {
883 | editor.setText('t13');
884 | editor.setCursorScreenPosition([0, 3]);
885 | simulateTabKeyEvent();
886 | expect(editor.getCursors().length).toBe(2);
887 | expect(editor.getText()).toBe(`\
888 | placeholder
889 | PLACEHOLDER
890 | \
891 | `
892 | );
893 |
894 | editor.insertText('foo');
895 |
896 | expect(editor.getText()).toBe(`\
897 | foo
898 | FOO
899 | foo\
900 | `
901 | );
902 | });
903 | });
904 |
905 | describe("when the snippet contains multiple tab stops, some with transformations and some without", () => {
906 | it("does not get confused", () => {
907 | editor.setText('t14');
908 | editor.setCursorScreenPosition([0, 3]);
909 | simulateTabKeyEvent();
910 | expect(editor.getCursors().length).toBe(2);
911 | expect(editor.getText()).toBe("placeholder PLACEHOLDER ANOTHER another ");
912 | simulateTabKeyEvent();
913 | expect(editor.getCursors().length).toBe(2);
914 | editor.insertText('FOO');
915 | expect(editor.getText()).toBe("placeholder PLACEHOLDER FOO foo FOO");
916 | });
917 | });
918 |
919 | 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", () => {
920 | it("terminates the snippet upon such a cursor move", () => {
921 | editor.setText('t18');
922 | editor.setCursorScreenPosition([0, 3]);
923 | simulateTabKeyEvent();
924 | expect(editor.getText()).toBe("// \n// ");
925 | expect(editor.getCursorBufferPosition()).toEqual([0, 3]);
926 | editor.insertText('wat');
927 | expect(editor.getText()).toBe("// wat\n// ===");
928 | // Move the cursor down one line, then up one line. This puts the cursor
929 | // back in its previous position, but the snippet should no longer be
930 | // active, so when we type more text, it should not be mirrored.
931 | editor.setCursorScreenPosition([1, 6]);
932 | editor.setCursorScreenPosition([0, 6]);
933 | editor.insertText('wat');
934 | expect(editor.getText()).toBe("// watwat\n// ===");
935 | });
936 | });
937 |
938 | describe("when the snippet has two adjacent tab stops", () => {
939 | it("ensures insertions are treated as part of the active tab stop", () => {
940 | editor.setText('t19');
941 | editor.setCursorScreenPosition([0, 3]);
942 | simulateTabKeyEvent();
943 | expect(editor.getText()).toBe('barbaz');
944 | expect(
945 | editor.getSelectedBufferRange()
946 | ).toEqual([
947 | [0, 0],
948 | [0, 3]
949 | ]);
950 | editor.insertText('w');
951 | expect(editor.getText()).toBe('wbaz');
952 | editor.insertText('at');
953 | expect(editor.getText()).toBe('watbaz');
954 | simulateTabKeyEvent();
955 | expect(
956 | editor.getSelectedBufferRange()
957 | ).toEqual([
958 | [0, 3],
959 | [0, 6]
960 | ]);
961 | editor.insertText('foo');
962 | expect(editor.getText()).toBe('watfoo');
963 | });
964 | });
965 |
966 | describe("when the snippet has a placeholder with a tabstop mirror at its edge", () => {
967 | it("allows the associated marker to include the inserted text", () => {
968 | editor.setText('t20');
969 | editor.setCursorScreenPosition([0, 3]);
970 | simulateTabKeyEvent();
971 | expect(editor.getText()).toBe('foobarbaz ');
972 | expect(editor.getCursors().length).toBe(2);
973 | let selections = editor.getSelections();
974 | expect(selections[0].getBufferRange()).toEqual([[0, 0], [0, 3]]);
975 | expect(selections[1].getBufferRange()).toEqual([[0, 10], [0, 10]]);
976 | editor.insertText('nah');
977 | expect(editor.getText()).toBe('nahbarbaz nah');
978 | simulateTabKeyEvent();
979 | editor.insertText('meh');
980 | simulateTabKeyEvent();
981 | editor.insertText('yea');
982 | expect(editor.getText()).toBe('nahmehyea');
983 | });
984 | });
985 |
986 | describe("when the snippet contains tab stops with an index >= 10", () => {
987 | it("parses and orders the indices correctly", () => {
988 | editor.setText('t10');
989 | editor.setCursorScreenPosition([0, 3]);
990 | simulateTabKeyEvent();
991 | expect(editor.getText()).toBe("hello large indices");
992 | expect(editor.getCursorBufferPosition()).toEqual([0, 19]);
993 | simulateTabKeyEvent();
994 | expect(editor.getCursorBufferPosition()).toEqual([0, 5]);
995 | simulateTabKeyEvent();
996 | expect(editor.getSelectedBufferRange()).toEqual([[0, 6], [0, 11]]);
997 | });
998 | });
999 |
1000 | describe("when there are multiple cursors", () => {
1001 | describe("when the cursors share a common snippet prefix", () => {
1002 | it("expands the snippet for all cursors and allows simultaneous editing", () => {
1003 | editor.insertText('t9');
1004 | editor.setCursorBufferPosition([12, 2]);
1005 | editor.insertText(' t9');
1006 | editor.addCursorAtBufferPosition([0, 2]);
1007 | simulateTabKeyEvent();
1008 |
1009 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test");
1010 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {");
1011 | expect(editor.lineTextForBufferRow(13)).toBe("}; with placeholder test");
1012 | expect(editor.lineTextForBufferRow(14)).toBe("without placeholder ");
1013 |
1014 | editor.insertText('hello');
1015 | expect(editor.lineTextForBufferRow(0)).toBe("with placeholder hello");
1016 | expect(editor.lineTextForBufferRow(1)).toBe("without placeholder hellovar quicksort = function () {");
1017 | expect(editor.lineTextForBufferRow(13)).toBe("}; with placeholder hello");
1018 | expect(editor.lineTextForBufferRow(14)).toBe("without placeholder hello");
1019 | });
1020 |
1021 | it("applies transformations identically to single-expansion mode", () => {
1022 | editor.setText('t14\nt14');
1023 | editor.setCursorBufferPosition([1, 3]);
1024 | editor.addCursorAtBufferPosition([0, 3]);
1025 | simulateTabKeyEvent();
1026 |
1027 | expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another ");
1028 | expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another ");
1029 |
1030 | editor.insertText("testing");
1031 |
1032 | expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another ");
1033 | expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another ");
1034 |
1035 | simulateTabKeyEvent();
1036 | editor.insertText("AGAIN");
1037 |
1038 | expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN");
1039 | expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN");
1040 | });
1041 |
1042 | it("bundles transform-induced mutations into a single history entry along with their triggering edit, even across multiple snippets", () => {
1043 | editor.setText('t14\nt14');
1044 | editor.setCursorBufferPosition([1, 3]);
1045 | editor.addCursorAtBufferPosition([0, 3]);
1046 | simulateTabKeyEvent();
1047 |
1048 | expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another ");
1049 | expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another ");
1050 |
1051 | editor.insertText("testing");
1052 |
1053 | expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another ");
1054 | expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another ");
1055 |
1056 | simulateTabKeyEvent();
1057 | editor.insertText("AGAIN");
1058 |
1059 | expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN");
1060 | expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN");
1061 |
1062 | editor.undo();
1063 | expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another ");
1064 | expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another ");
1065 |
1066 | editor.undo();
1067 | expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another ");
1068 | expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another ");
1069 |
1070 | editor.redo();
1071 | expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another ");
1072 | expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another ");
1073 |
1074 | editor.redo();
1075 | expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN");
1076 | expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN");
1077 | });
1078 |
1079 | describe("when there are many tabstops", () => {
1080 | it("moves the cursors between the tab stops for their corresponding snippet when tab and shift-tab are pressed", () => {
1081 | editor.addCursorAtBufferPosition([7, 5]);
1082 | editor.addCursorAtBufferPosition([12, 2]);
1083 | editor.insertText('t11');
1084 | simulateTabKeyEvent();
1085 |
1086 | const cursors = editor.getCursors();
1087 | expect(cursors.length).toEqual(3);
1088 |
1089 | expect(cursors[0].getBufferPosition()).toEqual([0, 3]);
1090 | expect(cursors[1].getBufferPosition()).toEqual([7, 8]);
1091 | expect(cursors[2].getBufferPosition()).toEqual([12, 5]);
1092 | expect(cursors[0].selection.isEmpty()).toBe(true);
1093 | expect(cursors[1].selection.isEmpty()).toBe(true);
1094 | expect(cursors[2].selection.isEmpty()).toBe(true);
1095 |
1096 | simulateTabKeyEvent();
1097 | expect(cursors[0].getBufferPosition()).toEqual([0, 7]);
1098 | expect(cursors[1].getBufferPosition()).toEqual([7, 12]);
1099 | expect(cursors[2].getBufferPosition()).toEqual([12, 9]);
1100 | expect(cursors[0].selection.isEmpty()).toBe(false);
1101 | expect(cursors[1].selection.isEmpty()).toBe(false);
1102 | expect(cursors[2].selection.isEmpty()).toBe(false);
1103 | expect(cursors[0].selection.getText()).toEqual('two');
1104 | expect(cursors[1].selection.getText()).toEqual('two');
1105 | expect(cursors[2].selection.getText()).toEqual('two');
1106 |
1107 | simulateTabKeyEvent();
1108 | expect(cursors[0].getBufferPosition()).toEqual([0, 13]);
1109 | expect(cursors[1].getBufferPosition()).toEqual([7, 18]);
1110 | expect(cursors[2].getBufferPosition()).toEqual([12, 15]);
1111 | expect(cursors[0].selection.isEmpty()).toBe(true);
1112 | expect(cursors[1].selection.isEmpty()).toBe(true);
1113 | expect(cursors[2].selection.isEmpty()).toBe(true);
1114 |
1115 | simulateTabKeyEvent();
1116 | expect(cursors[0].getBufferPosition()).toEqual([0, 0]);
1117 | expect(cursors[1].getBufferPosition()).toEqual([7, 5]);
1118 | expect(cursors[2].getBufferPosition()).toEqual([12, 2]);
1119 | expect(cursors[0].selection.isEmpty()).toBe(true);
1120 | expect(cursors[1].selection.isEmpty()).toBe(true);
1121 | expect(cursors[2].selection.isEmpty()).toBe(true);
1122 | });
1123 | });
1124 | });
1125 |
1126 | describe("when the cursors do not share common snippet prefixes", () => {
1127 | it("inserts tabs as normal", () => {
1128 | editor.insertText('t9');
1129 | editor.setCursorBufferPosition([12, 2]);
1130 | editor.insertText(' t8');
1131 | editor.addCursorAtBufferPosition([0, 2]);
1132 | simulateTabKeyEvent();
1133 | expect(editor.lineTextForBufferRow(0)).toBe("t9 var quicksort = function () {");
1134 | expect(editor.lineTextForBufferRow(12)).toBe("}; t8 ");
1135 | });
1136 | });
1137 |
1138 | describe("when a snippet is triggered within an existing snippet expansion", () => {
1139 | it("ignores the snippet expansion and goes to the next tab stop", () => {
1140 | editor.addCursorAtBufferPosition([7, 5]);
1141 | editor.addCursorAtBufferPosition([12, 2]);
1142 | editor.insertText('t11');
1143 | simulateTabKeyEvent();
1144 | simulateTabKeyEvent();
1145 |
1146 | editor.insertText('t1');
1147 | simulateTabKeyEvent();
1148 |
1149 | const cursors = editor.getCursors();
1150 | expect(cursors.length).toEqual(3);
1151 |
1152 | expect(cursors[0].getBufferPosition()).toEqual([0, 12]);
1153 | expect(cursors[1].getBufferPosition()).toEqual([7, 17]);
1154 | expect(cursors[2].getBufferPosition()).toEqual([12, 14]);
1155 | expect(cursors[0].selection.isEmpty()).toBe(true);
1156 | expect(cursors[1].selection.isEmpty()).toBe(true);
1157 | expect(cursors[2].selection.isEmpty()).toBe(true);
1158 | expect(editor.lineTextForBufferRow(0)).toBe("one t1 threevar quicksort = function () {");
1159 | expect(editor.lineTextForBufferRow(7)).toBe(" }one t1 three");
1160 | expect(editor.lineTextForBufferRow(12)).toBe("};one t1 three");
1161 | });
1162 | });
1163 | });
1164 |
1165 | describe("when the editor is not a pane item (regression)", () => {
1166 | it("handles tab stops correctly", () => {
1167 | editor = new TextEditor();
1168 | atom.grammars.assignLanguageMode(editor, 'source.js');
1169 | editorElement = editor.getElement();
1170 |
1171 | editor.insertText('t2');
1172 | simulateTabKeyEvent();
1173 | editor.insertText('ABC');
1174 | expect(editor.getText()).toContain('go here first:(ABC)');
1175 |
1176 | editor.undo();
1177 | editor.undo();
1178 | expect(editor.getText()).toBe('t2');
1179 | simulateTabKeyEvent();
1180 | editor.insertText('ABC');
1181 | expect(editor.getText()).toContain('go here first:(ABC)');
1182 | });
1183 | });
1184 | });
1185 |
1186 | describe("when atom://.atom/snippets is opened", () => {
1187 | it("opens ~/.atom/snippets.cson", () => {
1188 | jasmine.unspy(Snippets, 'getUserSnippetsPath');
1189 | atom.workspace.destroyActivePaneItem();
1190 | const configDirPath = temp.mkdirSync('atom-config-dir-');
1191 | spyOn(atom, 'getConfigDirPath').andReturn(configDirPath);
1192 | atom.workspace.open('atom://.atom/snippets');
1193 |
1194 | waitsFor(() => atom.workspace.getActiveTextEditor() != null);
1195 |
1196 | runs(() => {
1197 | expect(atom.workspace.getActiveTextEditor().getURI()).toBe(path.join(configDirPath, 'snippets.cson'));
1198 | });
1199 | });
1200 | });
1201 |
1202 | describe("snippet insertion API", () => {
1203 | it("will automatically parse snippet definition and replace selection", () => {
1204 | editor.setSelectedBufferRange([[0, 4], [0, 13]]);
1205 | Snippets.insert("hello ${1:world}", editor);
1206 |
1207 | expect(editor.lineTextForBufferRow(0)).toBe("var hello world = function () {");
1208 | expect(editor.getSelectedBufferRange()).toEqual([[0, 10], [0, 15]]);
1209 | });
1210 | });
1211 |
1212 | describe("when the 'snippets:available' command is triggered", () => {
1213 | let availableSnippetsView = null;
1214 |
1215 | beforeEach(() => {
1216 | Snippets.add(__filename, {
1217 | ".source.js": {
1218 | "test": {
1219 | prefix: "test",
1220 | body: "${1:Test pass you will}, young "
1221 | },
1222 |
1223 | "challenge": {
1224 | prefix: "chal",
1225 | body: "$1: ${2:To pass this challenge}"
1226 | }
1227 | }
1228 | }
1229 | );
1230 |
1231 | delete Snippets.availableSnippetsView;
1232 |
1233 | atom.commands.dispatch(editorElement, "snippets:available");
1234 |
1235 | waitsFor(() => atom.workspace.getModalPanels().length === 1);
1236 |
1237 | runs(() => {
1238 | availableSnippetsView = atom.workspace.getModalPanels()[0].getItem();
1239 | });
1240 | });
1241 |
1242 | it("renders a select list of all available snippets", () => {
1243 | expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe('test');
1244 | expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe('test');
1245 | expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe('${1:Test pass you will}, young ');
1246 |
1247 | availableSnippetsView.selectListView.selectNext();
1248 |
1249 | expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe('chal');
1250 | expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe('challenge');
1251 | expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe('$1: ${2:To pass this challenge}');
1252 | });
1253 |
1254 | it("writes the selected snippet to the editor as snippet", () => {
1255 | availableSnippetsView.selectListView.confirmSelection();
1256 |
1257 | expect(editor.getCursorScreenPosition()).toEqual([0, 18]);
1258 | expect(editor.getSelectedText()).toBe('Test pass you will');
1259 | expect(editor.lineTextForBufferRow(0)).toBe('Test pass you will, young var quicksort = function () {');
1260 | });
1261 |
1262 | it("closes the dialog when triggered again", () => {
1263 | atom.commands.dispatch(availableSnippetsView.selectListView.refs.queryEditor.element, 'snippets:available');
1264 | expect(atom.workspace.getModalPanels().length).toBe(0);
1265 | });
1266 | });
1267 | });
1268 |
--------------------------------------------------------------------------------