diff --git a/spec/body-parser-spec.coffee b/spec/body-parser-spec.js similarity index 52% rename from spec/body-parser-spec.coffee rename to spec/body-parser-spec.js index 2f8a28e7..75623f2c 100644 --- a/spec/body-parser-spec.coffee +++ b/spec/body-parser-spec.js @@ -1,13 +1,12 @@ -BodyParser = require '../lib/snippet-body-parser' - -describe "Snippet Body Parser", -> - it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", -> - bodyTree = BodyParser.parse """ - the quick brown $1fox ${2:jumped ${3:over} - }the ${4:lazy} dog - """ - - expect(bodyTree).toEqual [ +const BodyParser = require('../lib/snippet-body-parser'); + +describe("Snippet Body Parser", () => { + it("breaks a snippet body into lines, with each line containing tab stops at the appropriate position", () => { + const bodyTree = BodyParser.parse( + "the quick brown $1fox ${2:jumped ${3:over}\n" + + "}the ${4:lazy} dog" + ); + expect(bodyTree).toEqual([ "the quick brown ", {index: 1, content: []}, "fox ", @@ -18,31 +17,31 @@ describe "Snippet Body Parser", -> {index: 3, content: ["over"]}, "\n" ], - } - "the " + }, + "the ", {index: 4, content: ["lazy"]}, " dog" - ] - - it "removes interpolated variables in placeholder text (we don't currently support it)", -> - bodyTree = BodyParser.parse """ - module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} - """ - - expect(bodyTree).toEqual [ + ]); + }); + + it("removes interpolated variables in placeholder text (we don't currently support it)", () => { + const bodyTree = BodyParser.parse( + "module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}}" + ); + expect(bodyTree).toEqual([ "module ", { "index": 1, "content": ["ActiveRecord::", ""] } - ] - - it "skips escaped tabstops", -> - bodyTree = BodyParser.parse """ - snippet $1 escaped \\$2 \\\\$3 - """ - - expect(bodyTree).toEqual [ + ]); + }); + + it("skips escaped tabstops", () => { + const bodyTree = BodyParser.parse( + "snippet $1 escaped \\$2 \\\\$3" + ); + expect(bodyTree).toEqual([ "snippet ", { index: 1, @@ -53,26 +52,27 @@ describe "Snippet Body Parser", -> index: 3, content: [] } - ] - - it "includes escaped right-braces", -> - bodyTree = BodyParser.parse """ - snippet ${1:{\\}} - """ - - expect(bodyTree).toEqual [ + ]); + }); + + it("includes escaped right-braces", () => { + const bodyTree = BodyParser.parse( + "snippet ${1:{\\}}" + ); + expect(bodyTree).toEqual([ "snippet ", { index: 1, content: ["{}"] } - ] - - it "parses a snippet with transformations", -> - bodyTree = BodyParser.parse """ - <${1:p}>$0 - """ - expect(bodyTree).toEqual [ + ]); + }); + + it("parses a snippet with transformations", () => { + const bodyTree = BodyParser.parse( + "<${1:p}>$0" + ); + expect(bodyTree).toEqual([ '<', {index: 1, content: ['p']}, '>', @@ -80,14 +80,14 @@ describe "Snippet Body Parser", -> '' - ] - - it "parses a snippet with multiple tab stops with transformations", -> - bodyTree = BodyParser.parse """ - ${1:placeholder} ${1/(.)/\\u$1/} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2 - """ - - expect(bodyTree).toEqual [ + ]); + }); + + it("parses a snippet with multiple tab stops with transformations", () => { + const bodyTree = BodyParser.parse( + "${1:placeholder} ${1/(.)/\\u$1/} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2" + ); + expect(bodyTree).toEqual([ {index: 1, content: ['placeholder']}, ' ', { @@ -119,15 +119,15 @@ describe "Snippet Body Parser", -> }, ' ', {index: 2, content: []}, - ] - + ]); +}); - it "parses a snippet with transformations and mirrors", -> - bodyTree = BodyParser.parse """ - ${1:placeholder}\n${1/(.)/\\u$1/}\n$1 - """ - expect(bodyTree).toEqual [ + it("parses a snippet with transformations and mirrors", () => { + const bodyTree = BodyParser.parse( + "${1:placeholder}\n\${1/(.)/\\u$1/}\n$1" + ); + expect(bodyTree).toEqual([ {index: 1, content: ['placeholder']}, '\n', { @@ -143,14 +143,14 @@ describe "Snippet Body Parser", -> }, '\n', {index: 1, content: []} - ] - - it "parses a snippet with a format string and case-control flags", -> - bodyTree = BodyParser.parse """ - <${1:p}>$0 - """ - - expect(bodyTree).toEqual [ + ]); + }); + + it("parses a snippet with a format string and case-control flags", () => { + const bodyTree = BodyParser.parse( + "<${1:p}>$0" + ); + expect(bodyTree).toEqual([ '<', {index: 1, content: ['p']}, '>', @@ -169,16 +169,16 @@ describe "Snippet Body Parser", -> } }, '>' - ] - - it "parses a snippet with an escaped forward slash in a transform", -> - # Annoyingly, a forward slash needs to be double-backslashed just like the - # other escapes. - bodyTree = BodyParser.parse """ - <${1:p}>$0 - """ - - expect(bodyTree).toEqual [ + ]); + }); + + it("parses a snippet with an escaped forward slash in a transform", () => { + // Annoyingly, a forward slash needs to be double-backslashed just like the + // other escapes. + const bodyTree = BodyParser.parse( + "<${1:p}>$0" + ); + expect(bodyTree).toEqual([ '<', {index: 1, content: ['p']}, '>', @@ -197,14 +197,14 @@ describe "Snippet Body Parser", -> } }, '>' - ] - - it "parses a snippet with a placeholder that mirrors another tab stop's content", -> - bodyTree = BodyParser.parse """ - $4console.${3:log}('${2:$1}', $1);$0 - """ - - expect(bodyTree).toEqual [ + ]); + }); + + it("parses a snippet with a placeholder that mirrors another tab stop's content", () => { + const bodyTree = BodyParser.parse( + "$4console.${3:log}('${2:$1}', $1);$0" + ); + expect(bodyTree).toEqual([ {index: 4, content: []}, 'console.', {index: 3, content: ['log']}, @@ -218,14 +218,14 @@ describe "Snippet Body Parser", -> {index: 1, content: []}, ');', {index: 0, content: []} - ] - - it "parses a snippet with a placeholder that mixes text and tab stop references", -> - bodyTree = BodyParser.parse """ - $4console.${3:log}('${2:uh $1}', $1);$0 - """ - - expect(bodyTree).toEqual [ + ]); + }); + + it("parses a snippet with a placeholder that mixes text and tab stop references", () => { + const bodyTree = BodyParser.parse( + "$4console.${3:log}('${2:uh $1}', $1);$0" + ); + expect(bodyTree).toEqual([ {index: 4, content: []}, 'console.', {index: 3, content: ['log']}, @@ -240,4 +240,6 @@ describe "Snippet Body Parser", -> {index: 1, content: []}, ');', {index: 0, content: []} - ] + ]); + }); +}); diff --git a/spec/snippet-loading-spec.coffee b/spec/snippet-loading-spec.coffee deleted file mode 100644 index 12a6c2aa..00000000 --- a/spec/snippet-loading-spec.coffee +++ /dev/null @@ -1,268 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -temp = require('temp').track() - -describe "Snippet Loading", -> - [configDirPath, snippetsService] = [] - - beforeEach -> - configDirPath = temp.mkdirSync('atom-config-dir-') - spyOn(atom, 'getConfigDirPath').andReturn configDirPath - - spyOn(console, 'warn') - spyOn(atom.notifications, 'addError') if atom.notifications? - - spyOn(atom.packages, 'getLoadedPackages').andReturn [ - atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-snippets')) - atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-broken-snippets')), - ] - - afterEach -> - waitsForPromise -> - Promise.resolve(atom.packages.deactivatePackages('snippets')) - runs -> - jasmine.unspy(atom.packages, 'getLoadedPackages') - - activateSnippetsPackage = -> - waitsForPromise -> - atom.packages.activatePackage("snippets").then ({mainModule}) -> - snippetsService = mainModule.provideSnippets() - mainModule.loaded = false - - waitsFor "all snippets to load", 3000, -> - snippetsService.bundledSnippetsLoaded() - - it "loads the bundled snippet template snippets", -> - activateSnippetsPackage() - - runs -> - jsonSnippet = snippetsService.snippetsForScopes(['.source.json'])['snip'] - expect(jsonSnippet.name).toBe 'Atom Snippet' - expect(jsonSnippet.prefix).toBe 'snip' - expect(jsonSnippet.body).toContain '"prefix":' - expect(jsonSnippet.body).toContain '"body":' - expect(jsonSnippet.tabStopList.length).toBeGreaterThan(0) - - csonSnippet = snippetsService.snippetsForScopes(['.source.coffee'])['snip'] - expect(csonSnippet.name).toBe 'Atom Snippet' - expect(csonSnippet.prefix).toBe 'snip' - expect(csonSnippet.body).toContain "'prefix':" - expect(csonSnippet.body).toContain "'body':" - expect(csonSnippet.tabStopList.length).toBeGreaterThan(0) - - it "loads non-hidden snippet files from atom packages with snippets directories", -> - activateSnippetsPackage() - - runs -> - snippet = snippetsService.snippetsForScopes(['.test'])['test'] - expect(snippet.prefix).toBe 'test' - expect(snippet.body).toBe 'testing 123' - - snippet = snippetsService.snippetsForScopes(['.test'])['testd'] - expect(snippet.prefix).toBe 'testd' - expect(snippet.body).toBe 'testing 456' - expect(snippet.description).toBe 'a description' - expect(snippet.descriptionMoreURL).toBe 'http://google.com' - - snippet = snippetsService.snippetsForScopes(['.test'])['testlabelleft'] - expect(snippet.prefix).toBe 'testlabelleft' - expect(snippet.body).toBe 'testing 456' - expect(snippet.leftLabel).toBe 'a label' - - snippet = snippetsService.snippetsForScopes(['.test'])['testhtmllabels'] - expect(snippet.prefix).toBe 'testhtmllabels' - expect(snippet.body).toBe 'testing 456' - expect(snippet.leftLabelHTML).toBe 'Label' - expect(snippet.rightLabelHTML).toBe 'Label' - - it "logs a warning if package snippets files cannot be parsed", -> - activateSnippetsPackage() - - runs -> - # Warn about invalid-file, but don't even try to parse a hidden file - expect(console.warn.calls.length).toBeGreaterThan 0 - expect(console.warn.mostRecentCall.args[0]).toMatch(/Error reading.*package-with-broken-snippets/) - - describe "::loadPackageSnippets(callback)", -> - beforeEach -> - # simulate a list of packages where the javascript core package is returned at the end - atom.packages.getLoadedPackages.andReturn [ - atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-snippets')) - atom.packages.loadPackage('language-javascript') - ] - - it "allows other packages to override core packages' snippets", -> - waitsForPromise -> - atom.packages.activatePackage("language-javascript") - - activateSnippetsPackage() - - runs -> - snippet = snippetsService.snippetsForScopes(['.source.js'])['log'] - expect(snippet.body).toBe "from-a-community-package" - - describe "::onDidLoadSnippets(callback)", -> - it "invokes listeners when all snippets are loaded", -> - loadedCallback = null - - waitsFor "package to activate", (done) -> - atom.packages.activatePackage("snippets").then ({mainModule}) -> - mainModule.onDidLoadSnippets(loadedCallback = jasmine.createSpy('onDidLoadSnippets callback')) - done() - - waitsFor "onDidLoad callback to be called", -> loadedCallback.callCount > 0 - - describe "when ~/.atom/snippets.json exists", -> - beforeEach -> - fs.writeFileSync path.join(configDirPath, 'snippets.json'), """ - { - ".foo": { - "foo snippet": { - "prefix": "foo", - "body": "bar1" - } - } - } - """ - activateSnippetsPackage() - - it "loads the snippets from that file", -> - snippet = null - - waitsFor -> - snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] - - runs -> - expect(snippet.name).toBe 'foo snippet' - expect(snippet.prefix).toBe "foo" - expect(snippet.body).toBe "bar1" - - describe "when that file changes", -> - it "reloads the snippets", -> - fs.writeFileSync path.join(configDirPath, 'snippets.json'), """ - { - ".foo": { - "foo snippet": { - "prefix": "foo", - "body": "bar2" - } - } - } - """ - - waitsFor "snippets to be changed", -> - snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] - snippet?.body is 'bar2' - - runs -> - fs.writeFileSync path.join(configDirPath, 'snippets.json'), "" - - waitsFor "snippets to be removed", -> - not snippetsService.snippetsForScopes(['.foo'])['foo'] - - describe "when ~/.atom/snippets.cson exists", -> - beforeEach -> - fs.writeFileSync path.join(configDirPath, 'snippets.cson'), """ - ".foo": - "foo snippet": - "prefix": "foo" - "body": "bar1" - """ - activateSnippetsPackage() - - it "loads the snippets from that file", -> - snippet = null - - waitsFor -> - snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] - - runs -> - expect(snippet.name).toBe 'foo snippet' - expect(snippet.prefix).toBe "foo" - expect(snippet.body).toBe "bar1" - - describe "when that file changes", -> - it "reloads the snippets", -> - fs.writeFileSync path.join(configDirPath, 'snippets.cson'), """ - ".foo": - "foo snippet": - "prefix": "foo" - "body": "bar2" - """ - - waitsFor "snippets to be changed", -> - snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] - snippet?.body is 'bar2' - - runs -> - fs.writeFileSync path.join(configDirPath, 'snippets.cson'), "" - - waitsFor "snippets to be removed", -> - snippet = snippetsService.snippetsForScopes(['.foo'])['foo'] - not snippet? - - it "notifies the user when the user snippets file cannot be loaded", -> - fs.writeFileSync path.join(configDirPath, 'snippets.cson'), """ - ".junk"::: - """ - - activateSnippetsPackage() - - runs -> - expect(console.warn).toHaveBeenCalled() - expect(atom.notifications.addError).toHaveBeenCalled() if atom.notifications? - - describe "packages-with-snippets-disabled feature", -> - it "disables no snippets if the config option is empty", -> - originalConfig = atom.config.get('core.packagesWithSnippetsDisabled') - atom.config.set('core.packagesWithSnippetsDisabled', []) - - activateSnippetsPackage() - runs -> - snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']) - expect(Object.keys(snippets).length).toBe 1 - atom.config.set('core.packagesWithSnippetsDisabled', originalConfig) - - it "still includes a disabled package's snippets in the list of unparsed snippets", -> - originalConfig = atom.config.get('core.packagesWithSnippetsDisabled') - atom.config.set('core.packagesWithSnippetsDisabled', []) - - activateSnippetsPackage() - runs -> - atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']) - allSnippets = snippetsService.getUnparsedSnippets() - scopedSnippet = allSnippets.find (s) -> - s.selectorString is '.package-with-snippets-unique-scope' - expect(scopedSnippet).not.toBe undefined - originalConfig = atom.config.get('core.packagesWithSnippetsDisabled') - - it "never loads a package's snippets when that package is disabled in config", -> - originalConfig = atom.config.get('core.packagesWithSnippetsDisabled') - atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']) - - activateSnippetsPackage() - runs -> - snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']) - expect(Object.keys(snippets).length).toBe 0 - atom.config.set('core.packagesWithSnippetsDisabled', originalConfig) - - it "unloads and/or reloads snippets from a package if the config option is changed after activation", -> - originalConfig = atom.config.get('core.packagesWithSnippetsDisabled') - atom.config.set('core.packagesWithSnippetsDisabled', []) - - activateSnippetsPackage() - runs -> - snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']) - expect(Object.keys(snippets).length).toBe 1 - - # Disable it. - atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']) - snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']) - expect(Object.keys(snippets).length).toBe 0 - - # Re-enable it. - atom.config.set('core.packagesWithSnippetsDisabled', []) - snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']) - expect(Object.keys(snippets).length).toBe 1 - - atom.config.set('core.packagesWithSnippetsDisabled', originalConfig) diff --git a/spec/snippet-loading-spec.js b/spec/snippet-loading-spec.js new file mode 100644 index 00000000..2781f13d --- /dev/null +++ b/spec/snippet-loading-spec.js @@ -0,0 +1,310 @@ +const path = require('path'); +const fs = require('fs-plus'); +const temp = require('temp').track(); + +describe("Snippet Loading", () => { + let configDirPath, snippetsService; + + beforeEach(() => { + configDirPath = temp.mkdirSync('atom-config-dir-'); + spyOn(atom, 'getConfigDirPath').andReturn(configDirPath); + + spyOn(console, 'warn'); + if (atom.notifications !== undefined) { spyOn(atom.notifications, 'addError'); } + + spyOn(atom.packages, 'getLoadedPackages').andReturn([ + atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-snippets')), + atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-broken-snippets')), + ]); + }); + + afterEach(() => { + waitsForPromise(() => Promise.resolve(atom.packages.deactivatePackages('snippets'))); + runs(() => jasmine.unspy(atom.packages, 'getLoadedPackages')); + }); + + const activateSnippetsPackage = () => { + waitsForPromise(() => atom.packages.activatePackage("snippets").then(({mainModule}) => { + snippetsService = mainModule.provideSnippets(); + mainModule.loaded = false; + })); + waitsFor("all snippets to load", 3000, () => snippetsService.bundledSnippetsLoaded()); + }; + + it("loads the bundled snippet template snippets", () => { + activateSnippetsPackage(); + runs(() => { + const jsonSnippet = snippetsService.snippetsForScopes(['.source.json'])['snip']; + expect(jsonSnippet.name).toBe('Atom Snippet'); + expect(jsonSnippet.prefix).toBe('snip'); + expect(jsonSnippet.body).toContain('"prefix":'); + expect(jsonSnippet.body).toContain('"body":'); + expect(jsonSnippet.tabStopList.length).toBeGreaterThan(0); + + const csonSnippet = snippetsService.snippetsForScopes(['.source.coffee'])['snip']; + expect(csonSnippet.name).toBe('Atom Snippet'); + expect(csonSnippet.prefix).toBe('snip'); + expect(csonSnippet.body).toContain("'prefix':"); + expect(csonSnippet.body).toContain("'body':"); + expect(csonSnippet.tabStopList.length).toBeGreaterThan(0); + }); + }); + + it("loads non-hidden snippet files from atom packages with snippets directories", () => { + activateSnippetsPackage(); + runs(() => { + let snippet = snippetsService.snippetsForScopes(['.test'])['test']; + expect(snippet.prefix).toBe('test'); + expect(snippet.body).toBe('testing 123'); + + snippet = snippetsService.snippetsForScopes(['.test'])['testd']; + expect(snippet.prefix).toBe('testd'); + expect(snippet.body).toBe('testing 456'); + expect(snippet.description).toBe('a description'); + expect(snippet.descriptionMoreURL).toBe('http://google.com'); + + snippet = snippetsService.snippetsForScopes(['.test'])['testlabelleft']; + expect(snippet.prefix).toBe('testlabelleft'); + expect(snippet.body).toBe('testing 456'); + expect(snippet.leftLabel).toBe('a label'); + + snippet = snippetsService.snippetsForScopes(['.test'])['testhtmllabels']; + expect(snippet.prefix).toBe('testhtmllabels'); + expect(snippet.body).toBe('testing 456'); + expect(snippet.leftLabelHTML).toBe('Label'); + expect(snippet.rightLabelHTML).toBe('Label'); + }); + }); + + it("logs a warning if package snippets files cannot be parsed", () => { + activateSnippetsPackage(); + runs(() => { + // Warn about invalid-file, but don't even try to parse a hidden file + expect(console.warn.calls.length).toBeGreaterThan(0); + expect(console.warn.mostRecentCall.args[0]).toMatch(/Error reading.*package-with-broken-snippets/); + }); + }); + + describe("::loadPackageSnippets(callback)", () => { + beforeEach(() => {// simulate a list of packages where the javascript core package is returned at the end + atom.packages.getLoadedPackages.andReturn([ + atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-snippets')), + atom.packages.loadPackage('language-javascript') + ]) + }); + + it("allows other packages to override core packages' snippets", () => { + waitsForPromise(() => atom.packages.activatePackage("language-javascript")); + activateSnippetsPackage(); + runs(() => { + const snippet = snippetsService.snippetsForScopes(['.source.js'])['log']; + expect(snippet.body).toBe("from-a-community-package"); + }); + }); + }); + + describe("::onDidLoadSnippets(callback)", () => { + it("invokes listeners when all snippets are loaded", () => { + let loadedCallback = null; + waitsFor("package to activate", done => { + atom.packages.activatePackage("snippets").then(({mainModule}) => { + loadedCallback = jasmine.createSpy('onDidLoadSnippets callback'); + mainModule.onDidLoadSnippets(loadedCallback); + done(); + }); + }); + waitsFor("onDidLoad callback to be called", () => loadedCallback.callCount > 0); + }); + }); + + describe("when ~/.atom/snippets.json exists", () => { + beforeEach(() => { + fs.writeFileSync(path.join(configDirPath, 'snippets.json'), `\ +{ + ".foo": { + "foo snippet": { + "prefix": "foo", + "body": "bar1" + } + } +}\ +` + ); + activateSnippetsPackage(); + }); + + it("loads the snippets from that file", () => { + let snippet = null; + waitsFor(() => { + snippet = snippetsService.snippetsForScopes(['.foo'])['foo']; + return snippet; + }); + runs(() => { + expect(snippet.name).toBe('foo snippet'); + expect(snippet.prefix).toBe("foo"); + expect(snippet.body).toBe("bar1"); + }); + }); + + describe("when that file changes", () => { + it("reloads the snippets", () => { + fs.writeFileSync(path.join(configDirPath, 'snippets.json'), `\ +{ +".foo": { + "foo snippet": { + "prefix": "foo", + "body": "bar2" + } +} +}\ +` + ); + + waitsFor("snippets to be changed", () => { + const snippet = snippetsService.snippetsForScopes(['.foo'])['foo']; + return snippet && snippet.body === 'bar2'; + }); + + runs(() => { + fs.writeFileSync(path.join(configDirPath, 'snippets.json'), ""); + }); + + waitsFor("snippets to be removed", () => !snippetsService.snippetsForScopes(['.foo'])['foo']); + }); + }); + }); + + describe("when ~/.atom/snippets.cson exists", () => { + beforeEach(() => { + fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), `\ +".foo": + "foo snippet": + "prefix": "foo" + "body": "bar1"\ +` + ); + return activateSnippetsPackage(); + }); + + it("loads the snippets from that file", () => { + let snippet = null; + waitsFor(() => { + snippet = snippetsService.snippetsForScopes(['.foo'])['foo']; + return snippet; + }); + + runs(() => { + expect(snippet.name).toBe('foo snippet'); + expect(snippet.prefix).toBe("foo"); + expect(snippet.body).toBe("bar1"); + }); + }); + + describe("when that file changes", () => { + it("reloads the snippets", () => { + fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), `\ +".foo": + "foo snippet": + "prefix": "foo" + "body": "bar2"\ +` + ); + + waitsFor("snippets to be changed", () => { + const snippet = snippetsService.snippetsForScopes(['.foo'])['foo']; + return snippet && snippet.body === 'bar2'; + }); + + runs(() => { + fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), ""); + }); + + waitsFor("snippets to be removed", () => { + const snippet = snippetsService.snippetsForScopes(['.foo'])['foo']; + return !snippet; + }); + }); + }); + }); + + it("notifies the user when the user snippets file cannot be loaded", () => { + fs.writeFileSync(path.join(configDirPath, 'snippets.cson'), `\ +".junk":::\ +` + ); + + activateSnippetsPackage(); + + runs(() => { + expect(console.warn).toHaveBeenCalled(); + if (atom.notifications != null) { + expect(atom.notifications.addError).toHaveBeenCalled(); + } + }); + }); + + describe("packages-with-snippets-disabled feature", () => { + it("disables no snippets if the config option is empty", () => { + const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled'); + atom.config.set('core.packagesWithSnippetsDisabled', []); + + activateSnippetsPackage(); + runs(() => { + const snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(1); + atom.config.set('core.packagesWithSnippetsDisabled', originalConfig); + }); + }); + + it("still includes a disabled package's snippets in the list of unparsed snippets", () => { + const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled'); + atom.config.set('core.packagesWithSnippetsDisabled', []); + + activateSnippetsPackage(); + runs(() => { + atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']); + const allSnippets = snippetsService.getUnparsedSnippets(); + const scopedSnippet = allSnippets.find(s => s.selectorString === '.package-with-snippets-unique-scope'); + expect(scopedSnippet).not.toBe(undefined); + + // NOTE: Original seems to have been incorrectly reassigning to originalConfig + atom.config.set('core.packagesWithSnippetsDisabled', originalConfig); + }); + }); + + it("never loads a package's snippets when that package is disabled in config", () => { + const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled'); + atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']); + + activateSnippetsPackage(); + runs(() => { + const snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(0); + atom.config.set('core.packagesWithSnippetsDisabled', originalConfig); + }); + }); + + it("unloads and/or reloads snippets from a package if the config option is changed after activation", () => { + const originalConfig = atom.config.get('core.packagesWithSnippetsDisabled'); + atom.config.set('core.packagesWithSnippetsDisabled', []); + + activateSnippetsPackage(); + runs(() => { + let snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(1); + + // Disable it. + atom.config.set('core.packagesWithSnippetsDisabled', ['package-with-snippets']); + snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(0); + + // Re-enable it. + atom.config.set('core.packagesWithSnippetsDisabled', []); + snippets = snippetsService.snippetsForScopes(['.package-with-snippets-unique-scope']); + expect(Object.keys(snippets).length).toBe(1); + + atom.config.set('core.packagesWithSnippetsDisabled', originalConfig); + }); + }); + }); +}); diff --git a/spec/snippets-spec.coffee b/spec/snippets-spec.coffee deleted file mode 100644 index dd00a5d1..00000000 --- a/spec/snippets-spec.coffee +++ /dev/null @@ -1,1072 +0,0 @@ -path = require 'path' -temp = require('temp').track() -Snippets = require '../lib/snippets' -{TextEditor} = require 'atom' - -describe "Snippets extension", -> - [editorElement, editor] = [] - - simulateTabKeyEvent = ({shift}={}) -> - event = atom.keymaps.constructor.buildKeydownEvent('tab', {shift, target: editorElement}) - atom.keymaps.handleKeyboardEvent(event) - - beforeEach -> - spyOn(Snippets, 'loadAll') - spyOn(Snippets, 'getUserSnippetsPath').andReturn('') - - waitsForPromise -> - atom.workspace.open('sample.js') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - atom.packages.activatePackage('snippets') - - runs -> - editor = atom.workspace.getActiveTextEditor() - editorElement = atom.views.getView(editor) - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackage('snippets') - - describe "provideSnippets interface", -> - snippetsInterface = null - - beforeEach -> - snippetsInterface = Snippets.provideSnippets() - - describe "bundledSnippetsLoaded", -> - it "indicates the loaded state of the bundled snippets", -> - expect(snippetsInterface.bundledSnippetsLoaded()).toBe false - Snippets.doneLoading() - expect(snippetsInterface.bundledSnippetsLoaded()).toBe true - - it "resets the loaded state after snippets is deactivated", -> - expect(snippetsInterface.bundledSnippetsLoaded()).toBe false - Snippets.doneLoading() - expect(snippetsInterface.bundledSnippetsLoaded()).toBe true - - waitsForPromise -> atom.packages.deactivatePackage('snippets') - waitsForPromise -> atom.packages.activatePackage('snippets') - - runs -> - expect(snippetsInterface.bundledSnippetsLoaded()).toBe false - Snippets.doneLoading() - expect(snippetsInterface.bundledSnippetsLoaded()).toBe true - - describe "insertSnippet", -> - it "can insert a snippet", -> - editor.setSelectedBufferRange([[0, 4], [0, 13]]) - snippetsInterface.insertSnippet("hello ${1:world}", editor) - expect(editor.lineTextForBufferRow(0)).toBe "var hello world = function () {" - - it "returns false for snippetToExpandUnderCursor if getSnippets returns {}", -> - snippets = atom.packages.getActivePackage('snippets').mainModule - expect(snippets.snippetToExpandUnderCursor(editor)).toEqual false - - it "ignores invalid snippets in the config", -> - snippets = atom.packages.getActivePackage('snippets').mainModule - - invalidSnippets = null - spyOn(snippets.scopedPropertyStore, 'getPropertyValue').andCallFake -> invalidSnippets - expect(snippets.getSnippets(editor)).toEqual {} - - invalidSnippets = 'test' - expect(snippets.getSnippets(editor)).toEqual {} - - invalidSnippets = [] - expect(snippets.getSnippets(editor)).toEqual {} - - invalidSnippets = 3 - expect(snippets.getSnippets(editor)).toEqual {} - - invalidSnippets = {a: null} - expect(snippets.getSnippets(editor)).toEqual {} - - describe "when null snippets are present", -> - beforeEach -> - Snippets.add __filename, - '.source.js': - "some snippet": - prefix: "t1" - body: "this is a test" - - '.source.js .nope': - "some snippet": - prefix: "t1" - body: null - - it "overrides the less-specific defined snippet", -> - snippets = Snippets.provideSnippets() - expect(snippets.snippetsForScopes(['.source.js'])['t1']).toBeTruthy() - expect(snippets.snippetsForScopes(['.source.js .nope.not-today'])['t1']).toBeFalsy() - - describe "when 'tab' is triggered on the editor", -> - beforeEach -> - Snippets.add __filename, - ".source.js": - "without tab stops": - prefix: "t1" - body: "this is a test" - - "with only an end tab stop": - prefix: "t1a" - body: "something $0 strange" - - "overlapping prefix": - prefix: "tt1" - body: "this is another test" - - "special chars": - prefix: "@unique" - body: "@unique see" - - "tab stops": - prefix: "t2" - body: """ - go here next:($2) and finally go here:($0) - go here first:($1) - - """ - - "indented second line": - prefix: "t3" - body: """ - line 1 - \tline 2$1 - $2 - """ - - "multiline with indented placeholder tabstop": - prefix: "t4" - body: """ - line ${1:1} - ${2:body...} - """ - - "multiline starting with tabstop": - prefix: "t4b" - body: """ - $1 = line 1 { - line 2 - } - """ - - "nested tab stops": - prefix: "t5" - body: '${1:"${2:key}"}: ${3:value}' - - "caused problems with undo": - prefix: "t6" - body: """ - first line$1 - ${2:placeholder ending second line} - """ - - "tab stops at beginning and then end of snippet": - prefix: "t6b" - body: "$1expanded$0" - - "tab stops at end and then beginning of snippet": - prefix: "t6c" - body: "$0expanded$1" - - "contains empty lines": - prefix: "t7" - body: """ - first line $1 - - - fourth line after blanks $2 - """ - "with/without placeholder": - prefix: "t8" - body: """ - with placeholder ${1:test} - without placeholder ${2} - """ - - "multi-caret": - prefix: "t9" - body: """ - with placeholder ${1:test} - without placeholder $1 - """ - - "multi-caret-multi-tabstop": - prefix: "t9b" - body: """ - with placeholder ${1:test} - without placeholder $1 - second tabstop $2 - third tabstop $3 - """ - - "large indices": - prefix: "t10" - body: """ - hello${10} ${11:large} indices${1} - """ - - "no body": - prefix: "bad1" - - "number body": - prefix: "bad2" - body: 100 - - "many tabstops": - prefix: "t11" - body: """ - $0one${1} ${2:two} three${3} - """ - - "simple transform": - prefix: "t12" - body: """ - [${1:b}][/${1/[ ]+.*$//}] - """ - "transform with non-transforming mirrors": - prefix: "t13" - body: """ - ${1:placeholder}\n${1/(.)/\\u$1/}\n$1 - """ - "multiple tab stops, some with transforms and some without": - prefix: "t14" - body: """ - ${1:placeholder} ${1/(.)/\\u$1/} $1 ${2:ANOTHER} ${2/^(.*)$/\\L$1/} $2 - """ - "has a transformed tab stop without a corresponding ordinary tab stop": - prefix: 't15' - body: """ - ${1/(.)/\\u$1/} & $2 - """ - "has a transformed tab stop that occurs before the corresponding ordinary tab stop": - prefix: 't16' - body: """ - & ${1/(.)/\\u$1/} & ${1:q} - """ - "has a placeholder that mirrors another tab stop's content": - prefix: 't17' - body: "$4console.${3:log}('${2:uh $1}', $1);$0" - "has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step": - prefix: 't18' - body: '// $1\n// ${1/./=/}' - - it "parses snippets once, reusing cached ones on subsequent queries", -> - spyOn(Snippets, "getBodyParser").andCallThrough() - - editor.insertText("t1") - simulateTabKeyEvent() - - expect(Snippets.getBodyParser).toHaveBeenCalled() - expect(editor.lineTextForBufferRow(0)).toBe "this is a testvar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 14] - - Snippets.getBodyParser.reset() - - editor.setText("") - editor.insertText("t1") - simulateTabKeyEvent() - - expect(Snippets.getBodyParser).not.toHaveBeenCalled() - expect(editor.lineTextForBufferRow(0)).toBe "this is a test" - expect(editor.getCursorScreenPosition()).toEqual [0, 14] - - Snippets.getBodyParser.reset() - - Snippets.add __filename, - ".source.js": - "invalidate previous snippet": - prefix: "t1" - body: "new snippet" - - editor.setText("") - editor.insertText("t1") - simulateTabKeyEvent() - - expect(Snippets.getBodyParser).toHaveBeenCalled() - expect(editor.lineTextForBufferRow(0)).toBe "new snippet" - expect(editor.getCursorScreenPosition()).toEqual [0, 11] - - describe "when the snippet body is invalid or missing", -> - it "does not register the snippet", -> - editor.setText('') - editor.insertText('bad1') - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.getText()).toBe 'bad1' - - editor.setText('') - editor.setText('bad2') - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.getText()).toBe 'bad2' - - describe "when the letters preceding the cursor trigger a snippet", -> - describe "when the snippet contains no tab stops", -> - it "replaces the prefix with the snippet text and places the cursor at its end", -> - editor.insertText("t1") - expect(editor.getCursorScreenPosition()).toEqual [0, 2] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "this is a testvar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 14] - - it "inserts a real tab the next time a tab is pressed after the snippet is expanded", -> - editor.insertText("t1") - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "this is a testvar quicksort = function () {" - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "this is a test var quicksort = function () {" - - describe "when the snippet contains tab stops", -> - it "places the cursor at the first tab-stop, and moves the cursor in response to 'next-tab-stop' events", -> - markerCountBefore = editor.getMarkerCount() - editor.setCursorScreenPosition([2, 0]) - editor.insertText('t2') - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(2)).toBe "go here next:() and finally go here:()" - expect(editor.lineTextForBufferRow(3)).toBe "go here first:()" - expect(editor.lineTextForBufferRow(4)).toBe " if (items.length <= 1) return items;" - expect(editor.getSelectedBufferRange()).toEqual [[3, 15], [3, 15]] - - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[2, 14], [2, 14]] - editor.insertText 'abc' - - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[2, 40], [2, 40]] - - # tab backwards - simulateTabKeyEvent(shift: true) - expect(editor.getSelectedBufferRange()).toEqual [[2, 14], [2, 17]] # should highlight text typed at tab stop - - simulateTabKeyEvent(shift: true) - expect(editor.getSelectedBufferRange()).toEqual [[3, 15], [3, 15]] - - # shift-tab on first tab-stop does nothing - simulateTabKeyEvent(shift: true) - expect(editor.getCursorScreenPosition()).toEqual [3, 15] - - # tab through all tab stops, then tab on last stop to terminate snippet - simulateTabKeyEvent() - simulateTabKeyEvent() - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(2)).toBe "go here next:(abc) and finally go here:( )" - expect(editor.getMarkerCount()).toBe markerCountBefore - - describe "when tab stops are nested", -> - it "destroys the inner tab stop if the outer tab stop is modified", -> - editor.setText('') - editor.insertText 't5' - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.lineTextForBufferRow(0)).toBe '"key": value' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 5]] - editor.insertText("foo") - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 10]] - - describe "when the only tab stop is an end stop", -> - it "terminates the snippet immediately after moving the cursor to the end stop", -> - editor.setText('') - editor.insertText 't1a' - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "something strange" - expect(editor.getCursorBufferPosition()).toEqual [0, 10] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "something strange" - expect(editor.getCursorBufferPosition()).toEqual [0, 12] - - describe "when tab stops are separated by blank lines", -> - it "correctly places the tab stops (regression)", -> - editor.setText('') - editor.insertText 't7' - atom.commands.dispatch editorElement, 'snippets:expand' - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getCursorBufferPosition()).toEqual [3, 25] - - describe "when the cursor is moved beyond the bounds of the current tab stop", -> - it "terminates the snippet", -> - editor.setCursorScreenPosition([2, 0]) - editor.insertText('t2') - simulateTabKeyEvent() - - editor.moveUp() - editor.moveLeft() - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(2)).toBe "go here next:( ) and finally go here:()" - expect(editor.getCursorBufferPosition()).toEqual [2, 16] - - # test we can terminate with shift-tab - editor.setCursorScreenPosition([4, 0]) - editor.insertText('t2') - simulateTabKeyEvent() - simulateTabKeyEvent() - - editor.moveRight() - simulateTabKeyEvent(shift: true) - expect(editor.getCursorBufferPosition()).toEqual [4, 15] - - describe "when the cursor is moved within the bounds of the current tab stop", -> - it "should not terminate the snippet", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t8') - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - editor.moveRight() - editor.moveLeft() - editor.insertText("foo") - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder tesfoot" - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - editor.insertText("test") - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder testvar quicksort = function () {" - editor.moveLeft() - editor.insertText("foo") - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder tesfootvar quicksort = function () {" - - describe "when the backspace is press within the bounds of the current tab stop", -> - it "should not terminate the snippet", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t8') - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - editor.moveRight() - editor.backspace() - editor.insertText("foo") - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder tesfoo" - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - editor.insertText("test") - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder testvar quicksort = function () {" - editor.backspace() - editor.insertText("foo") - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder tesfoovar quicksort = function () {" - - describe "when the snippet contains hard tabs", -> - describe "when the edit session is in soft-tabs mode", -> - it "translates hard tabs in the snippet to the appropriate number of spaces", -> - expect(editor.getSoftTabs()).toBeTruthy() - editor.insertText("t3") - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(1)).toBe " line 2" - expect(editor.getCursorBufferPosition()).toEqual [1, 8] - - describe "when the edit session is in hard-tabs mode", -> - it "inserts hard tabs in the snippet directly", -> - editor.setSoftTabs(false) - editor.insertText("t3") - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(1)).toBe "\tline 2" - expect(editor.getCursorBufferPosition()).toEqual [1, 7] - - describe "when the snippet prefix is indented", -> - describe "when the snippet spans a single line", -> - it "does not indent the next line", -> - editor.setCursorScreenPosition([2, Infinity]) - editor.insertText ' t1' - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when the snippet spans multiple lines", -> - it "indents the subsequent lines of the snippet to be even with the start of the first line", -> - expect(editor.getSoftTabs()).toBeTruthy() - editor.setCursorScreenPosition([2, Infinity]) - editor.insertText ' t3' - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items; line 1" - expect(editor.lineTextForBufferRow(3)).toBe " line 2" - expect(editor.getCursorBufferPosition()).toEqual [3, 12] - - describe "when the snippet spans multiple lines", -> - beforeEach -> - editor.update({autoIndent: true}) - # editor.update() returns a Promise that never gets resolved, so we - # need to return undefined to avoid a timeout in the spec. - # TODO: Figure out why `editor.update({autoIndent: true})` never gets resolved. - return - - it "places tab stops correctly", -> - expect(editor.getSoftTabs()).toBeTruthy() - editor.setCursorScreenPosition([2, Infinity]) - editor.insertText ' t3' - atom.commands.dispatch editorElement, 'snippets:expand' - expect(editor.getCursorBufferPosition()).toEqual [3, 12] - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getCursorBufferPosition()).toEqual [4, 4] - - it "indents the subsequent lines of the snippet based on the indent level before the snippet is inserted", -> - editor.setCursorScreenPosition([2, Infinity]) - editor.insertNewline() - editor.insertText 't4b' - atom.commands.dispatch editorElement, 'snippets:expand' - - expect(editor.lineTextForBufferRow(3)).toBe " = line 1 {" # 4 + 1 spaces (because the tab stop is invisible) - expect(editor.lineTextForBufferRow(4)).toBe " line 2" - expect(editor.lineTextForBufferRow(5)).toBe " }" - expect(editor.getCursorBufferPosition()).toEqual [3, 4] - - it "does not change the relative positioning of the tab stops when inserted multiple times", -> - editor.setCursorScreenPosition([2, Infinity]) - editor.insertNewline() - editor.insertText 't4' - atom.commands.dispatch editorElement, 'snippets:expand' - - expect(editor.getSelectedBufferRange()).toEqual [[3, 9], [3, 10]] - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getSelectedBufferRange()).toEqual [[4, 6], [4, 13]] - - editor.insertText 't4' - atom.commands.dispatch editorElement, 'snippets:expand' - - expect(editor.getSelectedBufferRange()).toEqual [[4, 11], [4, 12]] - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getSelectedBufferRange()).toEqual [[5, 8], [5, 15]] - - editor.setText('') # Clear editor - editor.insertText 't4' - atom.commands.dispatch editorElement, 'snippets:expand' - - expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 6]] - atom.commands.dispatch editorElement, 'snippets:next-tab-stop' - expect(editor.getSelectedBufferRange()).toEqual [[1, 2], [1, 9]] - - describe "when multiple snippets match the prefix", -> - it "expands the snippet that is the longest match for the prefix", -> - editor.insertText('t113') - expect(editor.getCursorScreenPosition()).toEqual [0, 4] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "t113 var quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 6] - - editor.undo() - editor.undo() - - editor.insertText("tt1") - expect(editor.getCursorScreenPosition()).toEqual [0, 3] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "this is another testvar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 20] - - editor.undo() - editor.undo() - - editor.insertText("@t1") - expect(editor.getCursorScreenPosition()).toEqual [0, 3] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "@this is a testvar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 15] - - describe "when the word preceding the cursor ends with a snippet prefix", -> - it "inserts a tab as normal", -> - editor.insertText("t1t1t1") - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "t1t1t1 var quicksort = function () {" - - describe "when the letters preceding the cursor don't match a snippet", -> - it "inserts a tab as normal", -> - editor.insertText("xxte") - expect(editor.getCursorScreenPosition()).toEqual [0, 4] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "xxte var quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 6] - - describe "when text is selected", -> - it "inserts a tab as normal", -> - editor.insertText("t1") - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe " t1var quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 4]] - - describe "when a previous snippet expansion has just been undone", -> - describe "when the tab stops appear in the middle of the snippet", -> - it "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", -> - editor.insertText 't6\n' - editor.setCursorBufferPosition [0, 2] - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "first line" - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe "t6" - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "first line" - - describe "when the tab stops appear at the beginning and then the end of snippet", -> - it "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", -> - editor.insertText 't6b\n' - editor.setCursorBufferPosition [0, 3] - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "expanded" - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe "t6b" - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "expanded" - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - describe "when the tab stops appear at the end and then the beginning of snippet", -> - it "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", -> - editor.insertText 't6c\n' - editor.setCursorBufferPosition [0, 3] - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "expanded" - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe "t6c" - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "expanded" - expect(editor.getCursorBufferPosition()).toEqual([0, 8]) - - describe "when the prefix contains non-word characters", -> - it "selects the non-word characters as part of the prefix", -> - editor.insertText("@unique") - expect(editor.getCursorScreenPosition()).toEqual [0, 7] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "@unique seevar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 11] - - editor.setCursorBufferPosition [10, 0] - editor.insertText("'@unique") - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(10)).toBe "'@unique see" - expect(editor.getCursorScreenPosition()).toEqual [10, 12] - - it "does not select the whitespace before the prefix", -> - editor.insertText("a; @unique") - expect(editor.getCursorScreenPosition()).toEqual [0, 10] - - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "a; @unique seevar quicksort = function () {" - expect(editor.getCursorScreenPosition()).toEqual [0, 14] - - describe "when snippet contains tabstops with or without placeholder", -> - it "should create two markers", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t8') - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - - expect(editor.getSelectedBufferRange()).toEqual [[0, 17], [0, 21]] - - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[1, 20], [1, 20]] - - describe "when snippet contains multi-caret tabstops with or without placeholder", -> - it "should create two markers", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t9') - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - editor.insertText('hello') - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder hello" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder hellovar quicksort = function () {" - - it "terminates the snippet when cursors are destroyed", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t9b') - simulateTabKeyEvent() - editor.getCursors()[0].destroy() - editor.getCursorBufferPosition() - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(1)).toEqual("without placeholder ") - - it "terminates the snippet expansion if a new cursor moves outside the bounds of the tab stops", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t9b') - simulateTabKeyEvent() - editor.insertText('test') - - editor.getCursors()[0].destroy() - editor.moveDown() # this should destroy the previous expansion - editor.moveToBeginningOfLine() - - # this should insert whitespace instead of going through tabstops of the previous destroyed snippet - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(2).indexOf(" second")).toBe 0 - - it "moves to the second tabstop after a multi-caret tabstop", -> - editor.setCursorScreenPosition([0, 0]) - editor.insertText('t9b') - simulateTabKeyEvent() - editor.insertText('line 1') - - simulateTabKeyEvent() - editor.insertText('line 2') - - simulateTabKeyEvent() - editor.insertText('line 3') - - expect(editor.lineTextForBufferRow(2).indexOf("line 2 ")).toBe -1 - - it "mirrors input properly when a tabstop's placeholder refers to another tabstop", -> - editor.setText('t17') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - editor.insertText("foo") - expect(editor.getText()).toBe "console.log('uh foo', foo);" - simulateTabKeyEvent() - editor.insertText("bar") - expect(editor.getText()).toBe "console.log('bar', foo);" - - describe "when the snippet contains tab stops with transformations", -> - it "transforms the text typed into the first tab stop before setting it in the transformed tab stop", -> - editor.setText('t12') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getText()).toBe("[b][/b]") - editor.insertText('img src') - expect(editor.getText()).toBe("[img src][/img]") - - it "bundles the transform mutations along with the original manual mutation for the purposes of undo and redo", -> - editor.setText('t12') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - editor.insertText('i') - expect(editor.getText()).toBe("[i][/i]") - - editor.insertText('mg src') - expect(editor.getText()).toBe("[img src][/img]") - - editor.undo() - expect(editor.getText()).toBe("[i][/i]") - - editor.redo() - expect(editor.getText()).toBe("[img src][/img]") - - it "can pick the right insertion to use as the primary even if a transformed insertion occurs first in the snippet", -> - editor.setText('t16') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe("& Q & q") - expect(editor.getCursorBufferPosition()).toEqual([0, 7]) - - editor.insertText('rst') - expect(editor.lineTextForBufferRow(0)).toBe("& RST & rst") - - it "silently ignores a tab stop without a non-transformed insertion to use as the primary", -> - editor.setText('t15') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - editor.insertText('a') - expect(editor.lineTextForBufferRow(0)).toBe(" & a") - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe "when the snippet contains mirrored tab stops and tab stops with transformations", -> - it "adds cursors for the mirrors but not the transformations", -> - editor.setText('t13') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getCursors().length).toBe(2) - expect(editor.getText()).toBe """ - placeholder - PLACEHOLDER - - """ - - editor.insertText('foo') - - expect(editor.getText()).toBe """ - foo - FOO - foo - """ - - describe "when the snippet contains multiple tab stops, some with transformations and some without", -> - it "does not get confused", -> - editor.setText('t14') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getCursors().length).toBe(2) - expect(editor.getText()).toBe "placeholder PLACEHOLDER ANOTHER another " - simulateTabKeyEvent() - expect(editor.getCursors().length).toBe(2) - editor.insertText('FOO') - expect(editor.getText()).toBe """ - placeholder PLACEHOLDER FOO foo FOO - """ - - describe "when the snippet has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step", -> - it "terminates the snippet upon such a cursor move", -> - editor.setText('t18') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getText()).toBe("// \n// ") - expect(editor.getCursorBufferPosition()).toEqual [0, 3] - editor.insertText('wat') - expect(editor.getText()).toBe("// wat\n// ===") - # Move the cursor down one line, then up one line. This puts the cursor - # back in its previous position, but the snippet should no longer be - # active, so when we type more text, it should not be mirrored. - editor.setCursorScreenPosition([1, 6]) - editor.setCursorScreenPosition([0, 6]) - editor.insertText('wat') - expect(editor.getText()).toBe("// watwat\n// ===") - - - describe "when the snippet contains tab stops with an index >= 10", -> - it "parses and orders the indices correctly", -> - editor.setText('t10') - editor.setCursorScreenPosition([0, 3]) - simulateTabKeyEvent() - expect(editor.getText()).toBe "hello large indices" - expect(editor.getCursorBufferPosition()).toEqual [0, 19] - simulateTabKeyEvent() - expect(editor.getCursorBufferPosition()).toEqual [0, 5] - simulateTabKeyEvent() - expect(editor.getSelectedBufferRange()).toEqual [[0, 6], [0, 11]] - - describe "when there are multiple cursors", -> - describe "when the cursors share a common snippet prefix", -> - it "expands the snippet for all cursors and allows simultaneous editing", -> - editor.insertText('t9') - editor.setCursorBufferPosition([12, 2]) - editor.insertText(' t9') - editor.addCursorAtBufferPosition([0, 2]) - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder test" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder var quicksort = function () {" - expect(editor.lineTextForBufferRow(13)).toBe "}; with placeholder test" - expect(editor.lineTextForBufferRow(14)).toBe "without placeholder " - - editor.insertText('hello') - expect(editor.lineTextForBufferRow(0)).toBe "with placeholder hello" - expect(editor.lineTextForBufferRow(1)).toBe "without placeholder hellovar quicksort = function () {" - expect(editor.lineTextForBufferRow(13)).toBe "}; with placeholder hello" - expect(editor.lineTextForBufferRow(14)).toBe "without placeholder hello" - - it "applies transformations identically to single-expansion mode", -> - editor.setText('t14\nt14') - editor.setCursorBufferPosition([1, 3]) - editor.addCursorAtBufferPosition([0, 3]) - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "placeholder PLACEHOLDER ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "placeholder PLACEHOLDER ANOTHER another " - - editor.insertText "testing" - - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing ANOTHER another " - - simulateTabKeyEvent() - editor.insertText "AGAIN" - - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing AGAIN again AGAIN" - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing AGAIN again AGAIN" - - it "bundles transform-induced mutations into a single history entry along with their triggering edit, even across multiple snippets", -> - editor.setText('t14\nt14') - editor.setCursorBufferPosition([1, 3]) - editor.addCursorAtBufferPosition([0, 3]) - simulateTabKeyEvent() - - expect(editor.lineTextForBufferRow(0)).toBe "placeholder PLACEHOLDER ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "placeholder PLACEHOLDER ANOTHER another " - - editor.insertText "testing" - - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing ANOTHER another " - - simulateTabKeyEvent() - editor.insertText "AGAIN" - - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing AGAIN again AGAIN" - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing AGAIN again AGAIN" - - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing ANOTHER another " - - editor.undo() - expect(editor.lineTextForBufferRow(0)).toBe "placeholder PLACEHOLDER ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "placeholder PLACEHOLDER ANOTHER another " - - editor.redo() - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing ANOTHER another " - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing ANOTHER another " - - editor.redo() - expect(editor.lineTextForBufferRow(0)).toBe "testing TESTING testing AGAIN again AGAIN" - expect(editor.lineTextForBufferRow(1)).toBe "testing TESTING testing AGAIN again AGAIN" - - describe "when there are many tabstops", -> - it "moves the cursors between the tab stops for their corresponding snippet when tab and shift-tab are pressed", -> - editor.addCursorAtBufferPosition([7, 5]) - editor.addCursorAtBufferPosition([12, 2]) - editor.insertText('t11') - simulateTabKeyEvent() - - cursors = editor.getCursors() - expect(cursors.length).toEqual 3 - - expect(cursors[0].getBufferPosition()).toEqual [0, 3] - expect(cursors[1].getBufferPosition()).toEqual [7, 8] - expect(cursors[2].getBufferPosition()).toEqual [12, 5] - expect(cursors[0].selection.isEmpty()).toBe true - expect(cursors[1].selection.isEmpty()).toBe true - expect(cursors[2].selection.isEmpty()).toBe true - - simulateTabKeyEvent() - expect(cursors[0].getBufferPosition()).toEqual [0, 7] - expect(cursors[1].getBufferPosition()).toEqual [7, 12] - expect(cursors[2].getBufferPosition()).toEqual [12, 9] - expect(cursors[0].selection.isEmpty()).toBe false - expect(cursors[1].selection.isEmpty()).toBe false - expect(cursors[2].selection.isEmpty()).toBe false - expect(cursors[0].selection.getText()).toEqual 'two' - expect(cursors[1].selection.getText()).toEqual 'two' - expect(cursors[2].selection.getText()).toEqual 'two' - - simulateTabKeyEvent() - expect(cursors[0].getBufferPosition()).toEqual [0, 13] - expect(cursors[1].getBufferPosition()).toEqual [7, 18] - expect(cursors[2].getBufferPosition()).toEqual [12, 15] - expect(cursors[0].selection.isEmpty()).toBe true - expect(cursors[1].selection.isEmpty()).toBe true - expect(cursors[2].selection.isEmpty()).toBe true - - simulateTabKeyEvent() - expect(cursors[0].getBufferPosition()).toEqual [0, 0] - expect(cursors[1].getBufferPosition()).toEqual [7, 5] - expect(cursors[2].getBufferPosition()).toEqual [12, 2] - expect(cursors[0].selection.isEmpty()).toBe true - expect(cursors[1].selection.isEmpty()).toBe true - expect(cursors[2].selection.isEmpty()).toBe true - - describe "when the cursors do not share common snippet prefixes", -> - it "inserts tabs as normal", -> - editor.insertText('t9') - editor.setCursorBufferPosition([12, 2]) - editor.insertText(' t8') - editor.addCursorAtBufferPosition([0, 2]) - simulateTabKeyEvent() - expect(editor.lineTextForBufferRow(0)).toBe "t9 var quicksort = function () {" - expect(editor.lineTextForBufferRow(12)).toBe "}; t8 " - - describe "when a snippet is triggered within an existing snippet expansion", -> - it "ignores the snippet expansion and goes to the next tab stop", -> - editor.addCursorAtBufferPosition([7, 5]) - editor.addCursorAtBufferPosition([12, 2]) - editor.insertText('t11') - simulateTabKeyEvent() - simulateTabKeyEvent() - - editor.insertText('t1') - simulateTabKeyEvent() - - cursors = editor.getCursors() - expect(cursors.length).toEqual 3 - - expect(cursors[0].getBufferPosition()).toEqual [0, 12] - expect(cursors[1].getBufferPosition()).toEqual [7, 17] - expect(cursors[2].getBufferPosition()).toEqual [12, 14] - expect(cursors[0].selection.isEmpty()).toBe true - expect(cursors[1].selection.isEmpty()).toBe true - expect(cursors[2].selection.isEmpty()).toBe true - expect(editor.lineTextForBufferRow(0)).toBe "one t1 threevar quicksort = function () {" - expect(editor.lineTextForBufferRow(7)).toBe " }one t1 three" - expect(editor.lineTextForBufferRow(12)).toBe "};one t1 three" - - describe "when the editor is not a pane item (regression)", -> - it "handles tab stops correctly", -> - editor = new TextEditor() - atom.grammars.assignLanguageMode(editor, 'source.js') - editorElement = editor.getElement() - - editor.insertText('t2') - simulateTabKeyEvent() - editor.insertText('ABC') - expect(editor.getText()).toContain('go here first:(ABC)') - - editor.undo() - editor.undo() - expect(editor.getText()).toBe('t2') - simulateTabKeyEvent() - editor.insertText('ABC') - expect(editor.getText()).toContain('go here first:(ABC)') - - describe "when atom://.atom/snippets is opened", -> - it "opens ~/.atom/snippets.cson", -> - jasmine.unspy(Snippets, 'getUserSnippetsPath') - atom.workspace.destroyActivePaneItem() - configDirPath = temp.mkdirSync('atom-config-dir-') - spyOn(atom, 'getConfigDirPath').andReturn configDirPath - atom.workspace.open('atom://.atom/snippets') - - waitsFor -> - atom.workspace.getActiveTextEditor()? - - runs -> - expect(atom.workspace.getActiveTextEditor().getURI()).toBe path.join(configDirPath, 'snippets.cson') - - describe "snippet insertion API", -> - it "will automatically parse snippet definition and replace selection", -> - editor.setSelectedBufferRange([[0, 4], [0, 13]]) - Snippets.insert("hello ${1:world}", editor) - - expect(editor.lineTextForBufferRow(0)).toBe "var hello world = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 10], [0, 15]] - - describe "when the 'snippets:available' command is triggered", -> - availableSnippetsView = null - - beforeEach -> - Snippets.add __filename, - ".source.js": - "test": - prefix: "test" - body: "${1:Test pass you will}, young " - - "challenge": - prefix: "chal" - body: "$1: ${2:To pass this challenge}" - - delete Snippets.availableSnippetsView - - atom.commands.dispatch(editorElement, "snippets:available") - - waitsFor -> - atom.workspace.getModalPanels().length is 1 - - runs -> - availableSnippetsView = atom.workspace.getModalPanels()[0].getItem() - - it "renders a select list of all available snippets", -> - expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe 'test' - expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe 'test' - expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe '${1:Test pass you will}, young ' - - availableSnippetsView.selectListView.selectNext() - - expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe 'chal' - expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe 'challenge' - expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe '$1: ${2:To pass this challenge}' - - it "writes the selected snippet to the editor as snippet", -> - availableSnippetsView.selectListView.confirmSelection() - - expect(editor.getCursorScreenPosition()).toEqual [0, 18] - expect(editor.getSelectedText()).toBe 'Test pass you will' - expect(editor.lineTextForBufferRow(0)).toBe 'Test pass you will, young var quicksort = function () {' - - it "closes the dialog when triggered again", -> - atom.commands.dispatch availableSnippetsView.selectListView.refs.queryEditor.element, 'snippets:available' - expect(atom.workspace.getModalPanels().length).toBe 0 diff --git a/spec/snippets-spec.js b/spec/snippets-spec.js new file mode 100644 index 00000000..345e1b9d --- /dev/null +++ b/spec/snippets-spec.js @@ -0,0 +1,1148 @@ +const path = require('path'); +const temp = require('temp').track(); +const Snippets = require('../lib/snippets'); +const {TextEditor} = require('atom'); + +describe("Snippets extension", () => { + let editorElement, editor; + + const simulateTabKeyEvent = (param = {}) => { + const {shift} = param; + const event = atom.keymaps.constructor.buildKeydownEvent('tab', {shift, target: editorElement}); + atom.keymaps.handleKeyboardEvent(event); + }; + + beforeEach(() => { + spyOn(Snippets, 'loadAll'); + spyOn(Snippets, 'getUserSnippetsPath').andReturn(''); + + waitsForPromise(() => atom.workspace.open('sample.js')); + + waitsForPromise(() => atom.packages.activatePackage('language-javascript')); + + waitsForPromise(() => atom.packages.activatePackage('snippets')); + + runs(() => { + editor = atom.workspace.getActiveTextEditor(); + editorElement = atom.views.getView(editor); + }); + }); + + afterEach(() => waitsForPromise(() => atom.packages.deactivatePackage('snippets'))); + + describe("provideSnippets interface", () => { + let snippetsInterface = null; + + beforeEach(() => snippetsInterface = Snippets.provideSnippets()); + + describe("bundledSnippetsLoaded", () => { + it("indicates the loaded state of the bundled snippets", () => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false); + Snippets.doneLoading(); + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true); + }); + + it("resets the loaded state after snippets is deactivated", () => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false); + Snippets.doneLoading(); + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true); + + waitsForPromise(() => atom.packages.deactivatePackage('snippets')); + waitsForPromise(() => atom.packages.activatePackage('snippets')); + + runs(() => { + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(false); + Snippets.doneLoading(); + expect(snippetsInterface.bundledSnippetsLoaded()).toBe(true); + }); + }); + }); + + describe("insertSnippet", () => { + it("can insert a snippet", () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]); + snippetsInterface.insertSnippet("hello ${1:world}", editor); + expect(editor.lineTextForBufferRow(0)).toBe("var hello world = function () {"); + }); + }); + }); + + it("returns false for snippetToExpandUnderCursor if getSnippets returns {}", () => { + const snippets = atom.packages.getActivePackage('snippets').mainModule; + expect(snippets.snippetToExpandUnderCursor(editor)).toEqual(false); + }); + + it("ignores invalid snippets in the config", () => { + const snippets = atom.packages.getActivePackage('snippets').mainModule; + + let invalidSnippets = null; + spyOn(snippets.scopedPropertyStore, 'getPropertyValue').andCallFake(() => invalidSnippets); + expect(snippets.getSnippets(editor)).toEqual({}); + + invalidSnippets = 'test'; + expect(snippets.getSnippets(editor)).toEqual({}); + + invalidSnippets = []; + expect(snippets.getSnippets(editor)).toEqual({}); + + invalidSnippets = 3; + expect(snippets.getSnippets(editor)).toEqual({}); + + invalidSnippets = {a: null}; + expect(snippets.getSnippets(editor)).toEqual({}); + }); + + describe("when null snippets are present", () => { + beforeEach(() => { + Snippets.add(__filename, { + ".source.js": { + "some snippet": { + prefix: "t1", + body: "this is a test" + } + }, + ".source.js .nope": { + "some snippet": { + prefix: "t1", + body: null + } + } + }); + }); + + it("overrides the less-specific defined snippet", () => { + const snippets = Snippets.provideSnippets(); + expect(snippets.snippetsForScopes(['.source.js'])['t1']).toBeTruthy(); + expect(snippets.snippetsForScopes(['.source.js .nope.not-today'])['t1']).toBeFalsy(); + }); + }); + + describe("when 'tab' is triggered on the editor", () => { + beforeEach(() => { + Snippets.add(__filename, { + ".source.js": { + "without tab stops": { + prefix: "t1", + body: "this is a test" + }, + "with only an end tab stop": { + prefix: "t1a", + body: "something $0 strange" + }, + "overlapping prefix": { + prefix: "tt1", + body: "this is another test" + }, + "special chars": { + prefix: "@unique", + body: "@unique see" + }, + "tab stops": { + prefix: "t2", + body: `\ +go here next:($2) and finally go here:($0) +go here first:($1) +\ +` + }, + "indented second line": { + prefix: "t3", + body: `\ +line 1 +\tline 2$1 +$2\ +` + }, + "multiline with indented placeholder tabstop": { + prefix: "t4", + body: `\ +line \${1:1} + \${2:body...}\ +` + }, + "multiline starting with tabstop": { + prefix: "t4b", + body: `\ +$1 = line 1 { + line 2 +}\ +` + }, + "nested tab stops": { + prefix: "t5", + body: '${1:"${2:key}"}: ${3:value}' + }, + "caused problems with undo": { + prefix: "t6", + body: `\ +first line$1 + \${2:placeholder ending second line}\ +` + }, + "contains empty lines": { + prefix: "t7", + body: `\ +first line $1 + + +fourth line after blanks $2\ +` + }, + "with/without placeholder": { + prefix: "t8", + body: `\ +with placeholder \${1:test} +without placeholder \${2}\ +` + }, + "multi-caret": { + prefix: "t9", + body: `\ +with placeholder \${1:test} +without placeholder $1\ +` + }, + "multi-caret-multi-tabstop": { + prefix: "t9b", + body: `\ +with placeholder \${1:test} +without placeholder $1 +second tabstop $2 +third tabstop $3\ +` + }, + "large indices": { + prefix: "t10", + body: `\ +hello\${10} \${11:large} indices\${1}\ +` + }, + "no body": { + prefix: "bad1" + }, + "number body": { + prefix: "bad2", + body: 100 + }, + "many tabstops": { + prefix: "t11", + body: `\ +$0one\${1} \${2:two} three\${3}\ +` + }, + "simple transform": { + prefix: "t12", + body: `\ +[\${1:b}][/\${1/[ ]+.*$//}]\ +` + }, + "transform with non-transforming mirrors": { + prefix: "t13", + body: `\ +\${1:placeholder}\n\${1/(.)/\\u$1/}\n$1\ +` + }, + "multiple tab stops, some with transforms and some without": { + prefix: "t14", + body: `\ +\${1:placeholder} \${1/(.)/\\u$1/} $1 \${2:ANOTHER} \${2/^(.*)$/\\L$1/} $2\ +` + }, + "has a transformed tab stop without a corresponding ordinary tab stop": { + prefix: 't15', + body: `\ +\${1/(.)/\\u$1/} & $2\ +` + }, + "has a transformed tab stop that occurs before the corresponding ordinary tab stop": { + prefix: 't16', + body: `\ +& \${1/(.)/\\u$1/} & \${1:q}\ +` + }, + "has a placeholder that mirrors another tab stop's content": { + prefix: 't17', + body: "$4console.${3:log}('${2:uh $1}', $1);$0" + }, + "has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step": { + prefix: 't18', + body: '// $1\n// ${1/./=/}' + } + } + }); + }); + + it("parses snippets once, reusing cached ones on subsequent queries", () => { + spyOn(Snippets, "getBodyParser").andCallThrough(); + + editor.insertText("t1"); + simulateTabKeyEvent(); + + expect(Snippets.getBodyParser).toHaveBeenCalled(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 14]); + + Snippets.getBodyParser.reset(); + + editor.setText(""); + editor.insertText("t1"); + simulateTabKeyEvent(); + + expect(Snippets.getBodyParser).not.toHaveBeenCalled(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a test"); + expect(editor.getCursorScreenPosition()).toEqual([0, 14]); + + Snippets.getBodyParser.reset(); + + Snippets.add(__filename, { + ".source.js": { + "invalidate previous snippet": { + prefix: "t1", + body: "new snippet" + } + } + }); + + editor.setText(""); + editor.insertText("t1"); + simulateTabKeyEvent(); + + expect(Snippets.getBodyParser).toHaveBeenCalled(); + expect(editor.lineTextForBufferRow(0)).toBe("new snippet"); + expect(editor.getCursorScreenPosition()).toEqual([0, 11]); + }); + + describe("when the snippet body is invalid or missing", () => { + it("does not register the snippet", () => { + editor.setText(''); + editor.insertText('bad1'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.getText()).toBe('bad1'); + + editor.setText(''); + editor.setText('bad2'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.getText()).toBe('bad2'); + }) + }); + + describe("when the letters preceding the cursor trigger a snippet", () => { + describe("when the snippet contains no tab stops", () => { + it("replaces the prefix with the snippet text and places the cursor at its end", () => { + editor.insertText("t1"); + expect(editor.getCursorScreenPosition()).toEqual([0, 2]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 14]); + }); + + it("inserts a real tab the next time a tab is pressed after the snippet is expanded", () => { + editor.insertText("t1"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a testvar quicksort = function () {"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("this is a test var quicksort = function () {"); + }); + }); + + describe("when the snippet contains tab stops", () => { + it("places the cursor at the first tab-stop, and moves the cursor in response to 'next-tab-stop' events", () => { + const markerCountBefore = editor.getMarkerCount(); + editor.setCursorScreenPosition([2, 0]); + editor.insertText('t2'); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(2)).toBe("go here next:() and finally go here:()"); + expect(editor.lineTextForBufferRow(3)).toBe("go here first:()"); + expect(editor.lineTextForBufferRow(4)).toBe(" if (items.length <= 1) return items;"); + expect(editor.getSelectedBufferRange()).toEqual([[3, 15], [3, 15]]); + + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[2, 14], [2, 14]]); + editor.insertText('abc'); + + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[2, 40], [2, 40]]); + + // tab backwards + simulateTabKeyEvent({shift: true}); + expect(editor.getSelectedBufferRange()).toEqual([[2, 14], [2, 17]]); // should highlight text typed at tab stop + + simulateTabKeyEvent({shift: true}); + expect(editor.getSelectedBufferRange()).toEqual([[3, 15], [3, 15]]); + + // shift-tab on first tab-stop does nothing + simulateTabKeyEvent({shift: true}); + expect(editor.getCursorScreenPosition()).toEqual([3, 15]); + + // tab through all tab stops, then tab on last stop to terminate snippet + simulateTabKeyEvent(); + simulateTabKeyEvent(); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(2)).toBe("go here next:(abc) and finally go here:( )"); + expect(editor.getMarkerCount()).toBe(markerCountBefore); + }); + + describe("when tab stops are nested", () => { + it("destroys the inner tab stop if the outer tab stop is modified", () => { + editor.setText(''); + editor.insertText('t5'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.lineTextForBufferRow(0)).toBe('"key": value'); + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 5]]); + editor.insertText("foo"); + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 10]]); + }); + }); + + describe("when the only tab stop is an end stop", () => { + it("terminates the snippet immediately after moving the cursor to the end stop", () => { + editor.setText(''); + editor.insertText('t1a'); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("something strange"); + expect(editor.getCursorBufferPosition()).toEqual([0, 10]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("something strange"); + expect(editor.getCursorBufferPosition()).toEqual([0, 12]); + }); + }); + + describe("when tab stops are separated by blank lines", () => { + it("correctly places the tab stops (regression)", () => { + editor.setText(''); + editor.insertText('t7'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getCursorBufferPosition()).toEqual([3, 25]); + }); + }); + + describe("when the cursor is moved beyond the bounds of the current tab stop", () => { + it("terminates the snippet", () => { + editor.setCursorScreenPosition([2, 0]); + editor.insertText('t2'); + simulateTabKeyEvent(); + + editor.moveUp(); + editor.moveLeft(); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(2)).toBe("go here next:( ) and finally go here:()"); + expect(editor.getCursorBufferPosition()).toEqual([2, 16]); + + // test we can terminate with shift-tab + editor.setCursorScreenPosition([4, 0]); + editor.insertText('t2'); + simulateTabKeyEvent(); + simulateTabKeyEvent(); + + editor.moveRight(); + simulateTabKeyEvent({shift: true}); + expect(editor.getCursorBufferPosition()).toEqual([4, 15]); + }); + }); + + describe("when the cursor is moved within the bounds of the current tab stop", () => { + it("should not terminate the snippet", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t8'); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + editor.moveRight(); + editor.moveLeft(); + editor.insertText("foo"); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder tesfoot"); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + editor.insertText("test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder testvar quicksort = function () {"); + editor.moveLeft(); + editor.insertText("foo"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder tesfootvar quicksort = function () {"); + }); + }); + + describe("when the backspace is press within the bounds of the current tab stop", () => { + it("should not terminate the snippet", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t8'); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + editor.moveRight(); + editor.backspace(); + editor.insertText("foo"); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder tesfoo"); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + editor.insertText("test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder testvar quicksort = function () {"); + editor.backspace(); + editor.insertText("foo"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder tesfoovar quicksort = function () {"); + }); + }); + }); + + describe("when the snippet contains hard tabs", () => { + describe("when the edit session is in soft-tabs mode", () => { + it("translates hard tabs in the snippet to the appropriate number of spaces", () => { + expect(editor.getSoftTabs()).toBeTruthy(); + editor.insertText("t3"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toBe(" line 2"); + expect(editor.getCursorBufferPosition()).toEqual([1, 8]); + }); + }); + + describe("when the edit session is in hard-tabs mode", () => { + it("inserts hard tabs in the snippet directly", () => { + editor.setSoftTabs(false); + editor.insertText("t3"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toBe("\tline 2"); + expect(editor.getCursorBufferPosition()).toEqual([1, 7]); + }); + }); + }); + + describe("when the snippet prefix is indented", () => { + describe("when the snippet spans a single line", () => { + it("does not indent the next line", () => { + editor.setCursorScreenPosition([2, Infinity]); + editor.insertText(' t1'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.lineTextForBufferRow(3)).toBe(" var pivot = items.shift(), current, left = [], right = [];"); + }); + }); + + describe("when the snippet spans multiple lines", () => { + it("indents the subsequent lines of the snippet to be even with the start of the first line", () => { + expect(editor.getSoftTabs()).toBeTruthy(); + editor.setCursorScreenPosition([2, Infinity]); + editor.insertText(' t3'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items; line 1"); + expect(editor.lineTextForBufferRow(3)).toBe(" line 2"); + expect(editor.getCursorBufferPosition()).toEqual([3, 12]); + }); + }); + }); + + describe("when the snippet spans multiple lines", () => { + beforeEach(() => { + editor.update({autoIndent: true}); + }); + + it("places tab stops correctly", () => { + expect(editor.getSoftTabs()).toBeTruthy(); + editor.setCursorScreenPosition([2, Infinity]); + editor.insertText(' t3'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + expect(editor.getCursorBufferPosition()).toEqual([3, 12]); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getCursorBufferPosition()).toEqual([4, 4]); + }); + + it("indents the subsequent lines of the snippet based on the indent level before the snippet is inserted", () => { + editor.setCursorScreenPosition([2, Infinity]); + editor.insertNewline(); + editor.insertText('t4b'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + + expect(editor.lineTextForBufferRow(3)).toBe(" = line 1 {"); // 4 + 1 spaces (because the tab stop is invisible) + expect(editor.lineTextForBufferRow(4)).toBe(" line 2"); + expect(editor.lineTextForBufferRow(5)).toBe(" }"); + expect(editor.getCursorBufferPosition()).toEqual([3, 4]); + }); + + it("does not change the relative positioning of the tab stops when inserted multiple times", () => { + editor.setCursorScreenPosition([2, Infinity]); + editor.insertNewline(); + editor.insertText('t4'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + + expect(editor.getSelectedBufferRange()).toEqual([[3, 9], [3, 10]]); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getSelectedBufferRange()).toEqual([[4, 6], [4, 13]]); + + editor.insertText('t4'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + + expect(editor.getSelectedBufferRange()).toEqual([[4, 11], [4, 12]]); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getSelectedBufferRange()).toEqual([[5, 8], [5, 15]]); + + editor.setText(''); // Clear editor + editor.insertText('t4'); + atom.commands.dispatch(editorElement, 'snippets:expand'); + + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 6]]); + atom.commands.dispatch(editorElement, 'snippets:next-tab-stop'); + expect(editor.getSelectedBufferRange()).toEqual([[1, 2], [1, 9]]); + }); + }); + + describe("when multiple snippets match the prefix", () => { + it("expands the snippet that is the longest match for the prefix", () => { + editor.insertText('t113'); + expect(editor.getCursorScreenPosition()).toEqual([0, 4]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("t113 var quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 6]); + + editor.undo(); + editor.undo(); + + editor.insertText("tt1"); + expect(editor.getCursorScreenPosition()).toEqual([0, 3]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("this is another testvar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 20]); + + editor.undo(); + editor.undo(); + + editor.insertText("@t1"); + expect(editor.getCursorScreenPosition()).toEqual([0, 3]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("@this is a testvar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 15]); + }); + }); + }); + + describe("when the word preceding the cursor ends with a snippet prefix", () => { + it("inserts a tab as normal", () => { + editor.insertText("t1t1t1"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("t1t1t1 var quicksort = function () {"); + }); + }); + + describe("when the letters preceding the cursor don't match a snippet", () => { + it("inserts a tab as normal", () => { + editor.insertText("xxte"); + expect(editor.getCursorScreenPosition()).toEqual([0, 4]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("xxte var quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 6]); + }); + }); + + describe("when text is selected", () => { + it("inserts a tab as normal", () => { + editor.insertText("t1"); + editor.setSelectedBufferRange([[0, 0], [0, 2]]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe(" t1var quicksort = function () {"); + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 4]]); + }); + }); + + describe("when a previous snippet expansion has just been undone", () => { + it("expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", () => { + editor.insertText('t6\n'); + editor.setCursorBufferPosition([0, 2]); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("first line"); + editor.undo(); + expect(editor.lineTextForBufferRow(0)).toBe("t6"); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("first line"); + }); + }); + + describe("when the prefix contains non-word characters", () => { + it("selects the non-word characters as part of the prefix", () => { + editor.insertText("@unique"); + expect(editor.getCursorScreenPosition()).toEqual([0, 7]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("@unique seevar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 11]); + + editor.setCursorBufferPosition([10, 0]); + editor.insertText("'@unique"); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(10)).toBe("'@unique see"); + expect(editor.getCursorScreenPosition()).toEqual([10, 12]); + }); + + it("does not select the whitespace before the prefix", () => { + editor.insertText("a; @unique"); + expect(editor.getCursorScreenPosition()).toEqual([0, 10]); + + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("a; @unique seevar quicksort = function () {"); + expect(editor.getCursorScreenPosition()).toEqual([0, 14]); + }); + }); + + describe("when snippet contains tabstops with or without placeholder", () => { + it("should create two markers", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t8'); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + + expect(editor.getSelectedBufferRange()).toEqual([[0, 17], [0, 21]]); + + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[1, 20], [1, 20]]); + }); + }); + + describe("when snippet contains multi-caret tabstops with or without placeholder", () => { + it("should create two markers", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t9'); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + editor.insertText('hello'); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder hello"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder hellovar quicksort = function () {"); + }); + + it("terminates the snippet when cursors are destroyed", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t9b'); + simulateTabKeyEvent(); + editor.getCursors()[0].destroy(); + editor.getCursorBufferPosition(); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(1)).toEqual("without placeholder "); + }); + + it("terminates the snippet expansion if a new cursor moves outside the bounds of the tab stops", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t9b'); + simulateTabKeyEvent(); + editor.insertText('test'); + + editor.getCursors()[0].destroy(); + editor.moveDown(); // this should destroy the previous expansion + editor.moveToBeginningOfLine(); + + // this should insert whitespace instead of going through tabstops of the previous destroyed snippet + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(2).indexOf(" second")).toBe(0); + }); + + it("moves to the second tabstop after a multi-caret tabstop", () => { + editor.setCursorScreenPosition([0, 0]); + editor.insertText('t9b'); + simulateTabKeyEvent(); + editor.insertText('line 1'); + + simulateTabKeyEvent(); + editor.insertText('line 2'); + + simulateTabKeyEvent(); + editor.insertText('line 3'); + + expect(editor.lineTextForBufferRow(2).indexOf("line 2 ")).toBe(-1); + }); + + it("mirrors input properly when a tabstop's placeholder refers to another tabstop", () => { + editor.setText('t17'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + editor.insertText("foo"); + expect(editor.getText()).toBe("console.log('uh foo', foo);"); + simulateTabKeyEvent(); + editor.insertText("bar"); + expect(editor.getText()).toBe("console.log('bar', foo);"); + }); + }); + + describe("when the snippet contains tab stops with transformations", () => { + it("transforms the text typed into the first tab stop before setting it in the transformed tab stop", () => { + editor.setText('t12'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("[b][/b]"); + editor.insertText('img src'); + expect(editor.getText()).toBe("[img src][/img]"); + }); + + it("bundles the transform mutations along with the original manual mutation for the purposes of undo and redo", () => { + editor.setText('t12'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + editor.insertText('i'); + expect(editor.getText()).toBe("[i][/i]"); + + editor.insertText('mg src'); + expect(editor.getText()).toBe("[img src][/img]"); + + editor.undo(); + expect(editor.getText()).toBe("[i][/i]"); + + editor.redo(); + expect(editor.getText()).toBe("[img src][/img]"); + }); + + it("can pick the right insertion to use as the primary even if a transformed insertion occurs first in the snippet", () => { + editor.setText('t16'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("& Q & q"); + expect(editor.getCursorBufferPosition()).toEqual([0, 7]); + + editor.insertText('rst'); + expect(editor.lineTextForBufferRow(0)).toBe("& RST & rst"); + }); + + it("silently ignores a tab stop without a non-transformed insertion to use as the primary", () => { + editor.setText('t15'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + editor.insertText('a'); + expect(editor.lineTextForBufferRow(0)).toBe(" & a"); + expect(editor.getCursorBufferPosition()).toEqual([0, 4]); + }); + }); + + describe("when the snippet contains mirrored tab stops and tab stops with transformations", () => { + it("adds cursors for the mirrors but not the transformations", () => { + editor.setText('t13'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getCursors().length).toBe(2); + expect(editor.getText()).toBe("placeholder\nPLACEHOLDER\n"); + editor.insertText('foo'); + expect(editor.getText()).toBe("foo\nFOO\nfoo"); + }); + }); + + describe("when the snippet contains multiple tab stops, some with transformations and some without", () => { + it("does not get confused", () => { + editor.setText('t14'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getCursors().length).toBe(2); + expect(editor.getText()).toBe("placeholder PLACEHOLDER ANOTHER another "); + simulateTabKeyEvent(); + expect(editor.getCursors().length).toBe(2); + editor.insertText('FOO'); + expect(editor.getText()).toBe("placeholder PLACEHOLDER FOO foo FOO"); + }); + }); + + describe("when the snippet has a transformed tab stop such that it is possible to move the cursor between the ordinary tab stop and its transformed version without an intermediate step", () => { + it("terminates the snippet upon such a cursor move", () => { + editor.setText('t18'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("// \n// "); + expect(editor.getCursorBufferPosition()).toEqual([0, 3]); + editor.insertText('wat'); + expect(editor.getText()).toBe("// wat\n// ==="); + // Move the cursor down one line, then up one line. This puts the cursor + // back in its previous position, but the snippet should no longer be + // active, so when we type more text, it should not be mirrored. + editor.setCursorScreenPosition([1, 6]); + editor.setCursorScreenPosition([0, 6]); + editor.insertText('wat'); + expect(editor.getText()).toBe("// watwat\n// ==="); + }); + }); + + + describe("when the snippet contains tab stops with an index >= 10", () => { + it("parses and orders the indices correctly", () => { + editor.setText('t10'); + editor.setCursorScreenPosition([0, 3]); + simulateTabKeyEvent(); + expect(editor.getText()).toBe("hello large indices"); + expect(editor.getCursorBufferPosition()).toEqual([0, 19]); + simulateTabKeyEvent(); + expect(editor.getCursorBufferPosition()).toEqual([0, 5]); + simulateTabKeyEvent(); + expect(editor.getSelectedBufferRange()).toEqual([[0, 6], [0, 11]]); + }); + }); + + describe("when there are multiple cursors", () => { + describe("when the cursors share a common snippet prefix", () => { + it("expands the snippet for all cursors and allows simultaneous editing", () => { + editor.insertText('t9'); + editor.setCursorBufferPosition([12, 2]); + editor.insertText(' t9'); + editor.addCursorAtBufferPosition([0, 2]); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder test"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder var quicksort = function () {"); + expect(editor.lineTextForBufferRow(13)).toBe("}; with placeholder test"); + expect(editor.lineTextForBufferRow(14)).toBe("without placeholder "); + + editor.insertText('hello'); + expect(editor.lineTextForBufferRow(0)).toBe("with placeholder hello"); + expect(editor.lineTextForBufferRow(1)).toBe("without placeholder hellovar quicksort = function () {"); + expect(editor.lineTextForBufferRow(13)).toBe("}; with placeholder hello"); + expect(editor.lineTextForBufferRow(14)).toBe("without placeholder hello"); + }); + + it("applies transformations identically to single-expansion mode", () => { + editor.setText('t14\nt14'); + editor.setCursorBufferPosition([1, 3]); + editor.addCursorAtBufferPosition([0, 3]); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another "); + + editor.insertText("testing"); + + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another "); + + simulateTabKeyEvent(); + editor.insertText("AGAIN"); + + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN"); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN"); + }); + + it("bundles transform-induced mutations into a single history entry along with their triggering edit, even across multiple snippets", () => { + editor.setText('t14\nt14'); + editor.setCursorBufferPosition([1, 3]); + editor.addCursorAtBufferPosition([0, 3]); + simulateTabKeyEvent(); + + expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another "); + + editor.insertText("testing"); + + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another "); + + simulateTabKeyEvent(); + editor.insertText("AGAIN"); + + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN"); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN"); + + editor.undo(); + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another "); + + editor.undo(); + expect(editor.lineTextForBufferRow(0)).toBe("placeholder PLACEHOLDER ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("placeholder PLACEHOLDER ANOTHER another "); + + editor.redo(); + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing ANOTHER another "); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing ANOTHER another "); + + editor.redo(); + expect(editor.lineTextForBufferRow(0)).toBe("testing TESTING testing AGAIN again AGAIN"); + expect(editor.lineTextForBufferRow(1)).toBe("testing TESTING testing AGAIN again AGAIN"); + }); + + describe("when there are many tabstops", () => { + it("moves the cursors between the tab stops for their corresponding snippet when tab and shift-tab are pressed", () => { + editor.addCursorAtBufferPosition([7, 5]); + editor.addCursorAtBufferPosition([12, 2]); + editor.insertText('t11'); + simulateTabKeyEvent(); + + const cursors = editor.getCursors(); + expect(cursors.length).toEqual(3); + + expect(cursors[0].getBufferPosition()).toEqual([0, 3]); + expect(cursors[1].getBufferPosition()).toEqual([7, 8]); + expect(cursors[2].getBufferPosition()).toEqual([12, 5]); + expect(cursors[0].selection.isEmpty()).toBe(true); + expect(cursors[1].selection.isEmpty()).toBe(true); + expect(cursors[2].selection.isEmpty()).toBe(true); + + simulateTabKeyEvent(); + expect(cursors[0].getBufferPosition()).toEqual([0, 7]); + expect(cursors[1].getBufferPosition()).toEqual([7, 12]); + expect(cursors[2].getBufferPosition()).toEqual([12, 9]); + expect(cursors[0].selection.isEmpty()).toBe(false); + expect(cursors[1].selection.isEmpty()).toBe(false); + expect(cursors[2].selection.isEmpty()).toBe(false); + expect(cursors[0].selection.getText()).toEqual('two'); + expect(cursors[1].selection.getText()).toEqual('two'); + expect(cursors[2].selection.getText()).toEqual('two'); + + simulateTabKeyEvent(); + expect(cursors[0].getBufferPosition()).toEqual([0, 13]); + expect(cursors[1].getBufferPosition()).toEqual([7, 18]); + expect(cursors[2].getBufferPosition()).toEqual([12, 15]); + expect(cursors[0].selection.isEmpty()).toBe(true); + expect(cursors[1].selection.isEmpty()).toBe(true); + expect(cursors[2].selection.isEmpty()).toBe(true); + + simulateTabKeyEvent(); + expect(cursors[0].getBufferPosition()).toEqual([0, 0]); + expect(cursors[1].getBufferPosition()).toEqual([7, 5]); + expect(cursors[2].getBufferPosition()).toEqual([12, 2]); + expect(cursors[0].selection.isEmpty()).toBe(true); + expect(cursors[1].selection.isEmpty()).toBe(true); + expect(cursors[2].selection.isEmpty()).toBe(true); + }); + }); + }); + + describe("when the cursors do not share common snippet prefixes", () => { + it("inserts tabs as normal", () => { + editor.insertText('t9'); + editor.setCursorBufferPosition([12, 2]); + editor.insertText(' t8'); + editor.addCursorAtBufferPosition([0, 2]); + simulateTabKeyEvent(); + expect(editor.lineTextForBufferRow(0)).toBe("t9 var quicksort = function () {"); + expect(editor.lineTextForBufferRow(12)).toBe("}; t8 "); + }); + }); + + describe("when a snippet is triggered within an existing snippet expansion", () => { + it("ignores the snippet expansion and goes to the next tab stop", () => { + editor.addCursorAtBufferPosition([7, 5]); + editor.addCursorAtBufferPosition([12, 2]); + editor.insertText('t11'); + simulateTabKeyEvent(); + simulateTabKeyEvent(); + + editor.insertText('t1'); + simulateTabKeyEvent(); + + const cursors = editor.getCursors(); + expect(cursors.length).toEqual(3); + + expect(cursors[0].getBufferPosition()).toEqual([0, 12]); + expect(cursors[1].getBufferPosition()).toEqual([7, 17]); + expect(cursors[2].getBufferPosition()).toEqual([12, 14]); + expect(cursors[0].selection.isEmpty()).toBe(true); + expect(cursors[1].selection.isEmpty()).toBe(true); + expect(cursors[2].selection.isEmpty()).toBe(true); + expect(editor.lineTextForBufferRow(0)).toBe("one t1 threevar quicksort = function () {"); + expect(editor.lineTextForBufferRow(7)).toBe(" }one t1 three"); + expect(editor.lineTextForBufferRow(12)).toBe("};one t1 three"); + }); + }); + }); + + describe("when the editor is not a pane item (regression)", () => { + it("handles tab stops correctly", () => { + editor = new TextEditor(); + atom.grammars.assignLanguageMode(editor, 'source.js'); + editorElement = editor.getElement(); + + editor.insertText('t2'); + simulateTabKeyEvent(); + editor.insertText('ABC'); + expect(editor.getText()).toContain('go here first:(ABC)'); + + editor.undo(); + editor.undo(); + expect(editor.getText()).toBe('t2'); + simulateTabKeyEvent(); + editor.insertText('ABC'); + expect(editor.getText()).toContain('go here first:(ABC)'); + }); + }); + }); + + describe("when atom://.atom/snippets is opened", () => { + it("opens ~/.atom/snippets.cson", () => { + jasmine.unspy(Snippets, 'getUserSnippetsPath'); + atom.workspace.destroyActivePaneItem(); + const configDirPath = temp.mkdirSync('atom-config-dir-'); + spyOn(atom, 'getConfigDirPath').andReturn(configDirPath); + atom.workspace.open('atom://.atom/snippets'); + + waitsFor(() => atom.workspace.getActiveTextEditor() != null); + + runs(() => { + expect(atom.workspace.getActiveTextEditor().getURI()).toBe(path.join(configDirPath, 'snippets.cson')); + }); + }); + }); + + describe("snippet insertion API", () => { + it("will automatically parse snippet definition and replace selection", () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]); + Snippets.insert("hello ${1:world}", editor); + + expect(editor.lineTextForBufferRow(0)).toBe("var hello world = function () {"); + expect(editor.getSelectedBufferRange()).toEqual([[0, 10], [0, 15]]); + }); + }); + + describe("when the 'snippets:available' command is triggered", () => { + let availableSnippetsView = null; + + beforeEach(() => { + Snippets.add(__filename, { + ".source.js": { + "test": { + prefix: "test", + body: "${1:Test pass you will}, young " + }, + + "challenge": { + prefix: "chal", + body: "$1: ${2:To pass this challenge}" + } + } + }); + + delete Snippets.availableSnippetsView; + + atom.commands.dispatch(editorElement, "snippets:available"); + + waitsFor(() => atom.workspace.getModalPanels().length === 1); + + runs(() => { + availableSnippetsView = atom.workspace.getModalPanels()[0].getItem(); + }); + }); + + it("renders a select list of all available snippets", () => { + expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe('test'); + expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe('test'); + expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe('${1:Test pass you will}, young '); + + availableSnippetsView.selectListView.selectNext(); + + expect(availableSnippetsView.selectListView.getSelectedItem().prefix).toBe('chal'); + expect(availableSnippetsView.selectListView.getSelectedItem().name).toBe('challenge'); + expect(availableSnippetsView.selectListView.getSelectedItem().bodyText).toBe('$1: ${2:To pass this challenge}'); + }); + + it("writes the selected snippet to the editor as snippet", () => { + availableSnippetsView.selectListView.confirmSelection(); + + expect(editor.getCursorScreenPosition()).toEqual([0, 18]); + expect(editor.getSelectedText()).toBe('Test pass you will'); + expect(editor.lineTextForBufferRow(0)).toBe('Test pass you will, young var quicksort = function () {'); + }); + + it("closes the dialog when triggered again", () => { + atom.commands.dispatch(availableSnippetsView.selectListView.refs.queryEditor.element, 'snippets:available'); + expect(atom.workspace.getModalPanels().length).toBe(0); + }); + }); +});