mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 10:17:11 +03:00
Merge branch 'snippets' into dev
Conflicts: src/app/package.coffee src/packages/snippets/src/snippets.coffee
This commit is contained in:
commit
2c4da1b8dd
44
.atom/snippets/coffee.cson
Normal file
44
.atom/snippets/coffee.cson
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
".source.coffee":
|
||||||
|
"Describe block":
|
||||||
|
prefix: "de"
|
||||||
|
body: """
|
||||||
|
describe "${1:description}", ->
|
||||||
|
${2:body}
|
||||||
|
"""
|
||||||
|
"It block":
|
||||||
|
prefix: "i"
|
||||||
|
body: """
|
||||||
|
it "$1", ->
|
||||||
|
$2
|
||||||
|
"""
|
||||||
|
"Before each":
|
||||||
|
prefix: "be"
|
||||||
|
body: """
|
||||||
|
beforeEach ->
|
||||||
|
$1
|
||||||
|
"""
|
||||||
|
"After each":
|
||||||
|
prefix: "af"
|
||||||
|
body: """
|
||||||
|
afterEach ->
|
||||||
|
$1
|
||||||
|
"""
|
||||||
|
"Expectation":
|
||||||
|
prefix: "ex"
|
||||||
|
body: "expect($1).to$2"
|
||||||
|
"Console log":
|
||||||
|
prefix: "log"
|
||||||
|
body: "console.log $1"
|
||||||
|
"Range array":
|
||||||
|
prefix: "ra"
|
||||||
|
body: "[[$1, $2], [$3, $4]]"
|
||||||
|
"Point array":
|
||||||
|
prefix: "pt"
|
||||||
|
body: "[$1, $2]"
|
||||||
|
|
||||||
|
"Key-value pair":
|
||||||
|
prefix: ":"
|
||||||
|
body: '${1:"${2:key}"}: ${3:value}'
|
||||||
|
"Create Jasmine spy":
|
||||||
|
prefix: "pt"
|
||||||
|
body: 'jasmine.createSpy("${1:description}")$2'
|
@ -1,34 +0,0 @@
|
|||||||
snippet de "Describe block"
|
|
||||||
describe "${1:description}", ->
|
|
||||||
${2:body}
|
|
||||||
endsnippet
|
|
||||||
|
|
||||||
snippet i "It block"
|
|
||||||
it "$1", ->
|
|
||||||
$2
|
|
||||||
endsnippet
|
|
||||||
|
|
||||||
snippet be "Before each"
|
|
||||||
beforeEach ->
|
|
||||||
$1
|
|
||||||
endsnippet
|
|
||||||
|
|
||||||
snippet ex "Expectation"
|
|
||||||
expect($1).to$2
|
|
||||||
endsnippet
|
|
||||||
|
|
||||||
snippet log "Console log"
|
|
||||||
console.log $1
|
|
||||||
endsnippet
|
|
||||||
|
|
||||||
snippet ra "Range array"
|
|
||||||
[[$1, $2], [$3, $4]]
|
|
||||||
endsnippet
|
|
||||||
|
|
||||||
snippet pt "Point array"
|
|
||||||
[$1, $2]
|
|
||||||
endsnippet
|
|
||||||
|
|
||||||
snippet spy "Jasmine spy"
|
|
||||||
jasmine.createSpy("${1:description}")$2
|
|
||||||
endsnippet
|
|
4
spec/fixtures/packages/package-with-snippets/snippets/test.cson
vendored
Normal file
4
spec/fixtures/packages/package-with-snippets/snippets/test.cson
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
".test":
|
||||||
|
"Test Snippet":
|
||||||
|
prefix: "test"
|
||||||
|
body: "testing 123"
|
@ -13,11 +13,12 @@ TokenizedBuffer = require 'tokenized-buffer'
|
|||||||
fs = require 'fs'
|
fs = require 'fs'
|
||||||
require 'window'
|
require 'window'
|
||||||
requireStylesheet "jasmine.css"
|
requireStylesheet "jasmine.css"
|
||||||
require.paths.unshift(require.resolve('fixtures/packages'))
|
fixturePackagesPath = require.resolve('fixtures/packages')
|
||||||
|
require.paths.unshift(fixturePackagesPath)
|
||||||
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
|
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
|
||||||
|
|
||||||
# Load TextMate bundles, which specs rely on (but not other packages)
|
# Load TextMate bundles, which specs rely on (but not other packages)
|
||||||
atom.loadPackages(atom.getAvailableTextMateBundles())
|
atom.loadTextMatePackages()
|
||||||
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
window.fixturesProject = new Project(require.resolve('fixtures'))
|
window.fixturesProject = new Project(require.resolve('fixtures'))
|
||||||
@ -29,6 +30,7 @@ beforeEach ->
|
|||||||
|
|
||||||
# reset config before each spec; don't load or save from/to `config.json`
|
# reset config before each spec; don't load or save from/to `config.json`
|
||||||
window.config = new Config()
|
window.config = new Config()
|
||||||
|
config.packageDirPaths.unshift(fixturePackagesPath)
|
||||||
spyOn(config, 'load')
|
spyOn(config, 'load')
|
||||||
spyOn(config, 'save')
|
spyOn(config, 'save')
|
||||||
config.set "editor.fontSize", 16
|
config.set "editor.fontSize", 16
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
Range = require 'range'
|
Range = require 'range'
|
||||||
|
EventEmitter = require 'event-emitter'
|
||||||
|
Subscriber = require 'subscriber'
|
||||||
|
_ = require 'underscore'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class AnchorRange
|
class AnchorRange
|
||||||
@ -6,11 +9,14 @@ class AnchorRange
|
|||||||
end: null
|
end: null
|
||||||
buffer: null
|
buffer: null
|
||||||
editSession: null # optional
|
editSession: null # optional
|
||||||
|
destroyed: false
|
||||||
|
|
||||||
constructor: (bufferRange, @buffer, @editSession) ->
|
constructor: (bufferRange, @buffer, @editSession) ->
|
||||||
bufferRange = Range.fromObject(bufferRange)
|
bufferRange = Range.fromObject(bufferRange)
|
||||||
@startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreChangesStartingOnAnchor: true)
|
@startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreChangesStartingOnAnchor: true)
|
||||||
@endAnchor = @buffer.addAnchorAtPosition(bufferRange.end)
|
@endAnchor = @buffer.addAnchorAtPosition(bufferRange.end)
|
||||||
|
@subscribe @startAnchor, 'destroyed', => @destroy()
|
||||||
|
@subscribe @endAnchor, 'destroyed', => @destroy()
|
||||||
|
|
||||||
getBufferRange: ->
|
getBufferRange: ->
|
||||||
new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition())
|
new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition())
|
||||||
@ -22,7 +28,14 @@ class AnchorRange
|
|||||||
@getBufferRange().containsPoint(bufferPosition)
|
@getBufferRange().containsPoint(bufferPosition)
|
||||||
|
|
||||||
destroy: ->
|
destroy: ->
|
||||||
|
return if @destroyed
|
||||||
|
@unsubscribe()
|
||||||
@startAnchor.destroy()
|
@startAnchor.destroy()
|
||||||
@endAnchor.destroy()
|
@endAnchor.destroy()
|
||||||
@buffer.removeAnchorRange(this)
|
@buffer.removeAnchorRange(this)
|
||||||
@editSession?.removeAnchorRange(this)
|
@editSession?.removeAnchorRange(this)
|
||||||
|
@destroyed = true
|
||||||
|
@trigger 'destroyed'
|
||||||
|
|
||||||
|
_.extend(AnchorRange.prototype, EventEmitter)
|
||||||
|
_.extend(AnchorRange.prototype, Subscriber)
|
||||||
|
@ -10,6 +10,7 @@ class Anchor
|
|||||||
screenPosition: null
|
screenPosition: null
|
||||||
ignoreChangesStartingOnAnchor: false
|
ignoreChangesStartingOnAnchor: false
|
||||||
strong: false
|
strong: false
|
||||||
|
destroyed: false
|
||||||
|
|
||||||
constructor: (@buffer, options = {}) ->
|
constructor: (@buffer, options = {}) ->
|
||||||
{ @editSession, @ignoreChangesStartingOnAnchor, @strong } = options
|
{ @editSession, @ignoreChangesStartingOnAnchor, @strong } = options
|
||||||
@ -81,8 +82,10 @@ class Anchor
|
|||||||
@setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false, autoscroll: options.autoscroll)
|
@setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false, autoscroll: options.autoscroll)
|
||||||
|
|
||||||
destroy: ->
|
destroy: ->
|
||||||
|
return if @destroyed
|
||||||
@buffer.removeAnchor(this)
|
@buffer.removeAnchor(this)
|
||||||
@editSession?.removeAnchor(this)
|
@editSession?.removeAnchor(this)
|
||||||
|
@destroyed = true
|
||||||
@trigger 'destroyed'
|
@trigger 'destroyed'
|
||||||
|
|
||||||
_.extend(Anchor.prototype, EventEmitter)
|
_.extend(Anchor.prototype, EventEmitter)
|
||||||
|
@ -9,15 +9,18 @@ class AtomPackage extends Package
|
|||||||
constructor: (@name) ->
|
constructor: (@name) ->
|
||||||
super
|
super
|
||||||
@keymapsDirPath = fs.join(@path, 'keymaps')
|
@keymapsDirPath = fs.join(@path, 'keymaps')
|
||||||
if @requireModule
|
|
||||||
@module = require(@path)
|
|
||||||
@module.name = @name
|
|
||||||
|
|
||||||
load: ->
|
load: ->
|
||||||
@loadMetadata()
|
try
|
||||||
@loadKeymaps()
|
if @requireModule
|
||||||
@loadStylesheets()
|
@module = require(@path)
|
||||||
rootView.activatePackage(@name, @module) if @module
|
@module.name = @name
|
||||||
|
@loadMetadata()
|
||||||
|
@loadKeymaps()
|
||||||
|
@loadStylesheets()
|
||||||
|
rootView.activatePackage(@name, @module) if @module
|
||||||
|
catch e
|
||||||
|
console.warn "Failed to load package named '#{@name}'", e.stack
|
||||||
|
|
||||||
loadMetadata: ->
|
loadMetadata: ->
|
||||||
if metadataPath = fs.resolveExtension(fs.join(@path, "package"), ['cson', 'json'])
|
if metadataPath = fs.resolveExtension(fs.join(@path, "package"), ['cson', 'json'])
|
||||||
|
@ -12,7 +12,23 @@ _.extend atom,
|
|||||||
|
|
||||||
pendingBrowserProcessCallbacks: {}
|
pendingBrowserProcessCallbacks: {}
|
||||||
|
|
||||||
getAvailablePackages: ->
|
loadPackages: ->
|
||||||
|
pack.load() for pack in @getPackages()
|
||||||
|
|
||||||
|
getPackages: ->
|
||||||
|
@getPackageNames().map (name) -> Package.build(name)
|
||||||
|
|
||||||
|
loadTextMatePackages: ->
|
||||||
|
pack.load() for pack in @getTextMatePackages()
|
||||||
|
|
||||||
|
getTextMatePackages: ->
|
||||||
|
@getPackages().filter (pack) -> pack instanceof TextMatePackage
|
||||||
|
|
||||||
|
loadPackage: (name) ->
|
||||||
|
Package.build(name).load()
|
||||||
|
|
||||||
|
getPackageNames: ->
|
||||||
|
disabledPackages = config.get("core.disabledPackages") ? []
|
||||||
allPackageNames = []
|
allPackageNames = []
|
||||||
for packageDirPath in config.packageDirPaths
|
for packageDirPath in config.packageDirPaths
|
||||||
packageNames = fs.list(packageDirPath)
|
packageNames = fs.list(packageDirPath)
|
||||||
@ -20,17 +36,7 @@ _.extend atom,
|
|||||||
.map((packagePath) -> fs.base(packagePath))
|
.map((packagePath) -> fs.base(packagePath))
|
||||||
allPackageNames.push(packageNames...)
|
allPackageNames.push(packageNames...)
|
||||||
_.unique(allPackageNames)
|
_.unique(allPackageNames)
|
||||||
|
.filter (name) -> not _.contains(disabledPackages, name)
|
||||||
getAvailableTextMateBundles: ->
|
|
||||||
@getAvailablePackages().filter (packageName) => TextMatePackage.testName(packageName)
|
|
||||||
|
|
||||||
loadPackages: (packageNames=@getAvailablePackages()) ->
|
|
||||||
disabledPackages = config.get("core.disabledPackages") ? []
|
|
||||||
for packageName in packageNames
|
|
||||||
@loadPackage(packageName) unless _.contains(disabledPackages, packageName)
|
|
||||||
|
|
||||||
loadPackage: (name) ->
|
|
||||||
Package.load(name)
|
|
||||||
|
|
||||||
loadThemes: ->
|
loadThemes: ->
|
||||||
themeNames = config.get("core.themes") ? ['IR_Black']
|
themeNames = config.get("core.themes") ? ['IR_Black']
|
||||||
|
@ -2,18 +2,13 @@ fs = require 'fs'
|
|||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class Package
|
class Package
|
||||||
@load: (name) ->
|
@build: (name) ->
|
||||||
AtomPackage = require 'atom-package'
|
AtomPackage = require 'atom-package'
|
||||||
TextMatePackage = require 'text-mate-package'
|
TextMatePackage = require 'text-mate-package'
|
||||||
|
if TextMatePackage.testName(name)
|
||||||
try
|
new TextMatePackage(name)
|
||||||
if TextMatePackage.testName(name)
|
else
|
||||||
new TextMatePackage(name).load()
|
new AtomPackage(name)
|
||||||
else
|
|
||||||
new AtomPackage(name).load()
|
|
||||||
catch e
|
|
||||||
console.warn "Failed to load package named '#{name}'", e.stack
|
|
||||||
|
|
||||||
|
|
||||||
name: null
|
name: null
|
||||||
path: null
|
path: null
|
||||||
@ -29,10 +24,3 @@ class Package
|
|||||||
else
|
else
|
||||||
@requireModule = true
|
@requireModule = true
|
||||||
@path = fs.directory(@path)
|
@path = fs.directory(@path)
|
||||||
|
|
||||||
load: ->
|
|
||||||
for grammar in @getGrammars()
|
|
||||||
syntax.addGrammar(grammar)
|
|
||||||
|
|
||||||
for { selector, properties } in @getScopedProperties()
|
|
||||||
syntax.addProperties(selector, properties)
|
|
||||||
|
@ -23,6 +23,16 @@ class TextMatePackage extends Package
|
|||||||
@preferencesPath = fs.join(@path, "Preferences")
|
@preferencesPath = fs.join(@path, "Preferences")
|
||||||
@syntaxesPath = fs.join(@path, "Syntaxes")
|
@syntaxesPath = fs.join(@path, "Syntaxes")
|
||||||
|
|
||||||
|
load: ->
|
||||||
|
try
|
||||||
|
for grammar in @getGrammars()
|
||||||
|
syntax.addGrammar(grammar)
|
||||||
|
|
||||||
|
for { selector, properties } in @getScopedProperties()
|
||||||
|
syntax.addProperties(selector, properties)
|
||||||
|
catch e
|
||||||
|
console.warn "Failed to load package named '#{@name}'", e.stack
|
||||||
|
|
||||||
getGrammars: ->
|
getGrammars: ->
|
||||||
return @grammars if @grammars
|
return @grammars if @grammars
|
||||||
@grammars = []
|
@grammars = []
|
||||||
|
@ -1,39 +1,15 @@
|
|||||||
{
|
bodyContent = content:(tabStop / bodyContentText)* { return content; }
|
||||||
var Snippet = require('snippets/src/snippet');
|
bodyContentText = text:bodyContentChar+ { return text.join(''); }
|
||||||
var Point = require('point');
|
bodyContentChar = !tabStop char:. { return char; }
|
||||||
}
|
|
||||||
|
|
||||||
snippets = snippets:snippet+ ws? {
|
placeholderContent = content:(tabStop / placeholderContentText)* { return content; }
|
||||||
var snippetsByPrefix = {};
|
placeholderContentText = text:placeholderContentChar+ { return text.join(''); }
|
||||||
snippets.forEach(function(snippet) {
|
placeholderContentChar = !tabStop char:[^}] { return char; }
|
||||||
snippetsByPrefix[snippet.prefix] = snippet
|
|
||||||
});
|
|
||||||
return snippetsByPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
snippet = ws? start ws prefix:prefix ws description:string bodyPosition:beforeBody body:body end {
|
|
||||||
return new Snippet({ bodyPosition: bodyPosition, prefix: prefix, description: description, body: body });
|
|
||||||
}
|
|
||||||
|
|
||||||
start = 'snippet'
|
|
||||||
prefix = prefix:[A-Za-z0-9_]+ { return prefix.join(''); }
|
|
||||||
string = ['] body:[^']* ['] { return body.join(''); }
|
|
||||||
/ ["] body:[^"]* ["] { return body.join(''); }
|
|
||||||
|
|
||||||
beforeBody = [ ]* '\n' { return new Point(line, 0); } // return start position of body: body begins on next line, so don't subtract 1 from line
|
|
||||||
|
|
||||||
body = bodyLine+
|
|
||||||
bodyLine = content:(tabStop / bodyText)* '\n' { return content; }
|
|
||||||
bodyText = text:bodyChar+ { return text.join(''); }
|
|
||||||
bodyChar = !(end / tabStop) char:[^\n] { return char; }
|
|
||||||
tabStop = simpleTabStop / tabStopWithPlaceholder
|
tabStop = simpleTabStop / tabStopWithPlaceholder
|
||||||
simpleTabStop = '$' index:[0-9]+ {
|
simpleTabStop = '$' index:[0-9]+ {
|
||||||
return { index: parseInt(index), placeholderText: '' };
|
return { index: parseInt(index), content: [] };
|
||||||
}
|
}
|
||||||
tabStopWithPlaceholder = '${' index:[0-9]+ ':' placeholderText:[^}]* '}' {
|
tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' {
|
||||||
return { index: parseInt(index), placeholderText: placeholderText.join('') };
|
return { index: parseInt(index), content: content };
|
||||||
}
|
}
|
||||||
|
|
||||||
end = 'endsnippet'
|
|
||||||
ws = ([ \n] / comment)+
|
|
||||||
comment = '#' [^\n]*
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
Snippets = require 'snippets'
|
Snippets = require 'snippets'
|
||||||
|
Snippet = require 'snippets/src/snippet'
|
||||||
RootView = require 'root-view'
|
RootView = require 'root-view'
|
||||||
Buffer = require 'buffer'
|
Buffer = require 'buffer'
|
||||||
Editor = require 'editor'
|
Editor = require 'editor'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
fs = require 'fs'
|
fs = require 'fs'
|
||||||
|
AtomPackage = require 'atom-package'
|
||||||
|
TextMatePackage = require 'text-mate-package'
|
||||||
|
|
||||||
describe "Snippets extension", ->
|
describe "Snippets extension", ->
|
||||||
[buffer, editor] = []
|
[buffer, editor] = []
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
rootView = new RootView(require.resolve('fixtures/sample.js'))
|
rootView = new RootView(require.resolve('fixtures/sample.js'))
|
||||||
|
spyOn(AtomPackage.prototype, 'loadSnippets')
|
||||||
|
spyOn(TextMatePackage.prototype, 'loadSnippets')
|
||||||
atom.loadPackage("snippets")
|
atom.loadPackage("snippets")
|
||||||
editor = rootView.getActiveEditor()
|
editor = rootView.getActiveEditor()
|
||||||
buffer = editor.getBuffer()
|
buffer = editor.getBuffer()
|
||||||
@ -17,36 +22,50 @@ describe "Snippets extension", ->
|
|||||||
|
|
||||||
afterEach ->
|
afterEach ->
|
||||||
rootView.remove()
|
rootView.remove()
|
||||||
|
delete window.snippets
|
||||||
|
|
||||||
describe "when 'tab' is triggered on the editor", ->
|
describe "when 'tab' is triggered on the editor", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
Snippets.evalSnippets 'js', """
|
snippets.add
|
||||||
snippet t1 "Snippet without tab stops"
|
".source.js":
|
||||||
this is a test
|
"without tab stops":
|
||||||
endsnippet
|
prefix: "t1"
|
||||||
|
body: "this is a test"
|
||||||
|
|
||||||
snippet t2 "With tab stops"
|
"tab stops":
|
||||||
go here next:($2) and finally go here:($3)
|
prefix: "t2"
|
||||||
go here first:($1)
|
body: """
|
||||||
|
go here next:($2) and finally go here:($0)
|
||||||
|
go here first:($1)
|
||||||
|
|
||||||
endsnippet
|
"""
|
||||||
|
|
||||||
snippet t3 "With indented second line"
|
"indented second line":
|
||||||
line 1
|
prefix: "t3"
|
||||||
line 2$1
|
body: """
|
||||||
|
line 1
|
||||||
|
line 2$1
|
||||||
|
|
||||||
endsnippet
|
"""
|
||||||
|
|
||||||
snippet t4 "With tab stop placeholders"
|
"tab stop placeholders":
|
||||||
go here ${1:first} and then here ${2:second}
|
prefix: "t4"
|
||||||
|
body: """
|
||||||
|
go here ${1:first
|
||||||
|
think a while}, and then here ${2:second}
|
||||||
|
|
||||||
endsnippet
|
"""
|
||||||
|
|
||||||
snippet t5 "Caused problems with undo"
|
"nested tab stops":
|
||||||
first line$1
|
prefix: "t5"
|
||||||
${2:placeholder ending second line}
|
body: '${1:"${2:key}"}: ${3:value}'
|
||||||
endsnippet
|
|
||||||
"""
|
"caused problems with undo":
|
||||||
|
prefix: "t6"
|
||||||
|
body: """
|
||||||
|
first line$1
|
||||||
|
${2:placeholder ending second line}
|
||||||
|
"""
|
||||||
|
|
||||||
describe "when the letters preceding the cursor trigger a snippet", ->
|
describe "when the letters preceding the cursor trigger a snippet", ->
|
||||||
describe "when the snippet contains no tab stops", ->
|
describe "when the snippet contains no tab stops", ->
|
||||||
@ -98,8 +117,22 @@ describe "Snippets extension", ->
|
|||||||
it "auto-fills the placeholder text and highlights it when navigating to that tab stop", ->
|
it "auto-fills the placeholder text and highlights it when navigating to that tab stop", ->
|
||||||
editor.insertText 't4'
|
editor.insertText 't4'
|
||||||
editor.trigger 'snippets:expand'
|
editor.trigger 'snippets:expand'
|
||||||
expect(buffer.lineForRow(0)).toBe 'go here first and then here second'
|
expect(buffer.lineForRow(0)).toBe 'go here first'
|
||||||
expect(editor.getSelectedBufferRange()).toEqual [[0, 8], [0, 13]]
|
expect(buffer.lineForRow(1)).toBe 'think a while, and then here second'
|
||||||
|
expect(editor.getSelectedBufferRange()).toEqual [[0, 8], [1, 13]]
|
||||||
|
editor.trigger keydownEvent('tab', target: editor[0])
|
||||||
|
expect(editor.getSelectedBufferRange()).toEqual [[1, 29], [1, 35]]
|
||||||
|
|
||||||
|
describe "when tab stops are nested", ->
|
||||||
|
it "destroys the inner tab stop if the outer tab stop is modified", ->
|
||||||
|
buffer.setText('')
|
||||||
|
editor.insertText 't5'
|
||||||
|
editor.trigger 'snippets:expand'
|
||||||
|
expect(buffer.lineForRow(0)).toBe '"key": value'
|
||||||
|
expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 5]]
|
||||||
|
editor.insertText("foo")
|
||||||
|
editor.trigger keydownEvent('tab', target: editor[0])
|
||||||
|
expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 10]]
|
||||||
|
|
||||||
describe "when the cursor is moved beyond the bounds of a tab stop", ->
|
describe "when the cursor is moved beyond the bounds of a tab stop", ->
|
||||||
it "terminates the snippet", ->
|
it "terminates the snippet", ->
|
||||||
@ -152,92 +185,68 @@ describe "Snippets extension", ->
|
|||||||
|
|
||||||
describe "when a previous snippet expansion has just been undone", ->
|
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", ->
|
it "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", ->
|
||||||
editor.insertText 't5\n'
|
editor.insertText 't6\n'
|
||||||
editor.setCursorBufferPosition [0, 2]
|
editor.setCursorBufferPosition [0, 2]
|
||||||
editor.trigger keydownEvent('tab', target: editor[0])
|
editor.trigger keydownEvent('tab', target: editor[0])
|
||||||
expect(buffer.lineForRow(0)).toBe "first line"
|
expect(buffer.lineForRow(0)).toBe "first line"
|
||||||
editor.undo()
|
editor.undo()
|
||||||
expect(buffer.lineForRow(0)).toBe "t5"
|
expect(buffer.lineForRow(0)).toBe "t6"
|
||||||
editor.trigger keydownEvent('tab', target: editor[0])
|
editor.trigger keydownEvent('tab', target: editor[0])
|
||||||
expect(buffer.lineForRow(0)).toBe "first line"
|
expect(buffer.lineForRow(0)).toBe "first line"
|
||||||
|
|
||||||
describe "when a snippet expansion is undone and redone", ->
|
describe "when a snippet expansion is undone and redone", ->
|
||||||
it "recreates the snippet's tab stops", ->
|
it "recreates the snippet's tab stops", ->
|
||||||
editor.insertText ' t5\n'
|
editor.insertText ' t6\n'
|
||||||
editor.setCursorBufferPosition [0, 6]
|
editor.setCursorBufferPosition [0, 6]
|
||||||
editor.trigger keydownEvent('tab', target: editor[0])
|
editor.trigger keydownEvent('tab', target: editor[0])
|
||||||
expect(buffer.lineForRow(0)).toBe " first line"
|
expect(buffer.lineForRow(0)).toBe " first line"
|
||||||
editor.undo()
|
editor.undo()
|
||||||
editor.redo()
|
editor.redo()
|
||||||
|
|
||||||
expect(editor.getCursorBufferPosition()).toEqual [0, 14]
|
expect(editor.getCursorBufferPosition()).toEqual [0, 14]
|
||||||
editor.trigger keydownEvent('tab', target: editor[0])
|
editor.trigger keydownEvent('tab', target: editor[0])
|
||||||
expect(editor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]]
|
expect(editor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]]
|
||||||
|
|
||||||
it "restores tabs stops in active edit session even when the initial expansion was in a different edit session", ->
|
describe "snippet loading", ->
|
||||||
anotherEditor = editor.splitRight()
|
it "loads snippets from all atom packages with a snippets directory", ->
|
||||||
|
jasmine.unspy(AtomPackage.prototype, 'loadSnippets')
|
||||||
|
snippets.loadAll()
|
||||||
|
|
||||||
editor.insertText ' t5\n'
|
expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet
|
||||||
editor.setCursorBufferPosition [0, 6]
|
|
||||||
editor.trigger keydownEvent('tab', target: editor[0])
|
|
||||||
expect(buffer.lineForRow(0)).toBe " first line"
|
|
||||||
editor.undo()
|
|
||||||
|
|
||||||
anotherEditor.redo()
|
it "loads snippets from all TextMate packages with snippets", ->
|
||||||
expect(anotherEditor.getCursorBufferPosition()).toEqual [0, 14]
|
jasmine.unspy(TextMatePackage.prototype, 'loadSnippets')
|
||||||
anotherEditor.trigger keydownEvent('tab', target: anotherEditor[0])
|
snippets.loadAll()
|
||||||
expect(anotherEditor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]]
|
|
||||||
|
|
||||||
describe ".loadSnippetsFile(path)", ->
|
snippet = syntax.getProperty(['.source.js'], 'snippets.fun')
|
||||||
it "loads the snippets in the given file", ->
|
expect(snippet.constructor).toBe Snippet
|
||||||
spyOn(fs, 'read').andReturn """
|
expect(snippet.prefix).toBe 'fun'
|
||||||
snippet t1 "Test snippet 1"
|
expect(snippet.name).toBe 'Function'
|
||||||
this is a test 1
|
expect(snippet.body).toBe """
|
||||||
endsnippet
|
function function_name (argument) {
|
||||||
|
\t// body...
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Snippets.loadSnippetsFile('/tmp/foo/js.snippets')
|
|
||||||
expect(fs.read).toHaveBeenCalledWith('/tmp/foo/js.snippets')
|
|
||||||
|
|
||||||
editor.insertText("t1")
|
|
||||||
editor.trigger 'snippets:expand'
|
|
||||||
expect(buffer.lineForRow(0)).toBe "this is a test 1var quicksort = function () {"
|
|
||||||
|
|
||||||
describe "Snippets parser", ->
|
describe "Snippets parser", ->
|
||||||
it "can parse multiple snippets", ->
|
it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", ->
|
||||||
snippets = Snippets.snippetsParser.parse """
|
bodyTree = Snippets.parser.parse """
|
||||||
snippet t1 "Test snippet 1"
|
the quick brown $1fox ${2:jumped ${3:over}
|
||||||
this is a test 1
|
}the ${4:lazy} dog
|
||||||
endsnippet
|
|
||||||
|
|
||||||
snippet t2 "Test snippet 2"
|
|
||||||
this is a test 2
|
|
||||||
endsnippet
|
|
||||||
"""
|
|
||||||
expect(_.keys(snippets).length).toBe 2
|
|
||||||
snippet = snippets['t1']
|
|
||||||
expect(snippet.prefix).toBe 't1'
|
|
||||||
expect(snippet.description).toBe "Test snippet 1"
|
|
||||||
expect(snippet.body).toBe "this is a test 1"
|
|
||||||
|
|
||||||
snippet = snippets['t2']
|
|
||||||
expect(snippet.prefix).toBe 't2'
|
|
||||||
expect(snippet.description).toBe "Test snippet 2"
|
|
||||||
expect(snippet.body).toBe "this is a test 2"
|
|
||||||
|
|
||||||
it "can parse snippets with tabstops", ->
|
|
||||||
snippets = Snippets.snippetsParser.parse """
|
|
||||||
# this line intentially left blank.
|
|
||||||
snippet t1 "Snippet with tab stops"
|
|
||||||
go here next:($2) and finally go here:($3)
|
|
||||||
go here first:($1)
|
|
||||||
endsnippet
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
snippet = snippets['t1']
|
expect(bodyTree).toEqual [
|
||||||
expect(snippet.body).toBe """
|
"the quick brown ",
|
||||||
go here next:() and finally go here:()
|
{ index: 1, content: [] },
|
||||||
go here first:()
|
"fox ",
|
||||||
"""
|
{
|
||||||
|
index: 2,
|
||||||
expect(snippet.tabStops).toEqual [[[1, 15], [1, 15]], [[0, 14], [0, 14]], [[0, 37], [0, 37]]]
|
content: [
|
||||||
|
"jumped ",
|
||||||
|
{ index: 3, content: ["over"]},
|
||||||
|
"\n"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
"the "
|
||||||
|
{ index: 4, content: ["lazy"] },
|
||||||
|
" dog"
|
||||||
|
]
|
||||||
|
27
src/packages/snippets/src/package-extensions.coffee
Normal file
27
src/packages/snippets/src/package-extensions.coffee
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
AtomPackage = require 'atom-package'
|
||||||
|
TextMatePackage = require 'text-mate-package'
|
||||||
|
fs = require 'fs'
|
||||||
|
|
||||||
|
AtomPackage.prototype.loadSnippets = ->
|
||||||
|
snippetsDirPath = fs.join(@path, 'snippets')
|
||||||
|
if fs.exists(snippetsDirPath)
|
||||||
|
for snippetsPath in fs.list(snippetsDirPath)
|
||||||
|
snippets.load(snippetsPath)
|
||||||
|
|
||||||
|
TextMatePackage.prototype.loadSnippets = ->
|
||||||
|
snippetsDirPath = fs.join(@path, 'Snippets')
|
||||||
|
if fs.exists(snippetsDirPath)
|
||||||
|
tmSnippets = fs.list(snippetsDirPath).map (snippetPath) -> fs.readPlist(snippetPath)
|
||||||
|
snippets.add(@translateSnippets(tmSnippets))
|
||||||
|
|
||||||
|
TextMatePackage.prototype.translateSnippets = (tmSnippets) ->
|
||||||
|
atomSnippets = {}
|
||||||
|
for { scope, name, content, tabTrigger } in tmSnippets
|
||||||
|
if scope
|
||||||
|
scope = TextMatePackage.cssSelectorFromScopeSelector(scope)
|
||||||
|
else
|
||||||
|
scope = '*'
|
||||||
|
|
||||||
|
snippetsForScope = (atomSnippets[scope] ?= {})
|
||||||
|
snippetsForScope[name] = { prefix: tabTrigger, body: content }
|
||||||
|
atomSnippets
|
@ -1,33 +1,44 @@
|
|||||||
|
Subscriber = require 'subscriber'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class SnippetExpansion
|
class SnippetExpansion
|
||||||
|
snippet: null
|
||||||
tabStopAnchorRanges: null
|
tabStopAnchorRanges: null
|
||||||
settingTabStop: false
|
settingTabStop: false
|
||||||
|
|
||||||
constructor: (snippet, @editSession) ->
|
constructor: (@snippet, @editSession) ->
|
||||||
@editSession.selectToBeginningOfWord()
|
@editSession.selectToBeginningOfWord()
|
||||||
startPosition = @editSession.getCursorBufferPosition()
|
startPosition = @editSession.getCursorBufferPosition()
|
||||||
@editSession.transact =>
|
@editSession.transact =>
|
||||||
@editSession.insertText(snippet.body, autoIndent: false)
|
@editSession.insertText(snippet.body, autoIndent: false)
|
||||||
if snippet.tabStops.length
|
editSession.pushOperation
|
||||||
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
|
do: =>
|
||||||
if snippet.lineCount > 1
|
@subscribe @editSession, 'cursor-moved.snippet-expansion', (e) => @cursorMoved(e)
|
||||||
@indentSubsequentLines(startPosition.row, snippet)
|
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
|
||||||
|
@editSession.snippetExpansion = this
|
||||||
|
undo: => @destroy()
|
||||||
|
@indentSubsequentLines(startPosition.row, snippet) if snippet.lineCount > 1
|
||||||
|
|
||||||
@editSession.on 'cursor-moved.snippet-expansion', ({oldBufferPosition, newBufferPosition}) =>
|
cursorMoved: ({oldBufferPosition, newBufferPosition}) ->
|
||||||
return if @settingTabStop
|
return if @settingTabStop
|
||||||
|
|
||||||
oldTabStops = @tabStopsForBufferPosition(oldBufferPosition)
|
oldTabStops = @tabStopsForBufferPosition(oldBufferPosition)
|
||||||
newTabStops = @tabStopsForBufferPosition(newBufferPosition)
|
newTabStops = @tabStopsForBufferPosition(newBufferPosition)
|
||||||
|
|
||||||
@destroy() unless _.intersect(oldTabStops, newTabStops).length
|
@destroy() unless _.intersect(oldTabStops, newTabStops).length
|
||||||
|
|
||||||
placeTabStopAnchorRanges: (startPosition, tabStopRanges) ->
|
placeTabStopAnchorRanges: (startPosition, tabStopRanges) ->
|
||||||
|
return unless @snippet.tabStops.length > 0
|
||||||
|
|
||||||
@tabStopAnchorRanges = tabStopRanges.map ({start, end}) =>
|
@tabStopAnchorRanges = tabStopRanges.map ({start, end}) =>
|
||||||
@editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)])
|
anchorRange = @editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)])
|
||||||
|
@subscribe anchorRange, 'destroyed', =>
|
||||||
|
_.remove(@tabStopAnchorRanges, anchorRange)
|
||||||
|
anchorRange
|
||||||
@setTabStopIndex(0)
|
@setTabStopIndex(0)
|
||||||
|
|
||||||
|
|
||||||
indentSubsequentLines: (startRow, snippet) ->
|
indentSubsequentLines: (startRow, snippet) ->
|
||||||
initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0]
|
initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0]
|
||||||
for row in [startRow + 1...startRow + snippet.lineCount]
|
for row in [startRow + 1...startRow + snippet.lineCount]
|
||||||
@ -68,11 +79,13 @@ class SnippetExpansion
|
|||||||
_.intersection(@tabStopAnchorRanges, @editSession.anchorRangesForBufferPosition(bufferPosition))
|
_.intersection(@tabStopAnchorRanges, @editSession.anchorRangesForBufferPosition(bufferPosition))
|
||||||
|
|
||||||
destroy: ->
|
destroy: ->
|
||||||
|
@unsubscribe()
|
||||||
anchorRange.destroy() for anchorRange in @tabStopAnchorRanges
|
anchorRange.destroy() for anchorRange in @tabStopAnchorRanges
|
||||||
@editSession.off '.snippet-expansion'
|
|
||||||
@editSession.snippetExpansion = null
|
@editSession.snippetExpansion = null
|
||||||
|
|
||||||
restore: (@editSession) ->
|
restore: (@editSession) ->
|
||||||
@editSession.snippetExpansion = this
|
@editSession.snippetExpansion = this
|
||||||
@tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) =>
|
@tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) =>
|
||||||
@editSession.addAnchorRange(anchorRange.getBufferRange())
|
@editSession.addAnchorRange(anchorRange.getBufferRange())
|
||||||
|
|
||||||
|
_.extend(SnippetExpansion.prototype, Subscriber)
|
||||||
|
@ -3,34 +3,41 @@ Range = require 'range'
|
|||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class Snippet
|
class Snippet
|
||||||
|
name: null
|
||||||
|
prefix: null
|
||||||
body: null
|
body: null
|
||||||
lineCount: null
|
lineCount: null
|
||||||
tabStops: null
|
tabStops: null
|
||||||
|
|
||||||
constructor: ({@bodyPosition, @prefix, @description, body}) ->
|
constructor: ({@name, @prefix, bodyTree}) ->
|
||||||
@body = @extractTabStops(body)
|
@body = @extractTabStops(bodyTree)
|
||||||
|
|
||||||
extractTabStops: (bodyLines) ->
|
extractTabStops: (bodyTree) ->
|
||||||
tabStopsByIndex = {}
|
tabStopsByIndex = {}
|
||||||
bodyText = []
|
bodyText = []
|
||||||
|
|
||||||
[row, column] = [0, 0]
|
[row, column] = [0, 0]
|
||||||
for bodyLine, i in bodyLines
|
|
||||||
lineText = []
|
|
||||||
for segment in bodyLine
|
|
||||||
if segment.index
|
|
||||||
{ index, placeholderText } = segment
|
|
||||||
tabStopsByIndex[index] = new Range([row, column], [row, column + placeholderText.length])
|
|
||||||
lineText.push(placeholderText)
|
|
||||||
else
|
|
||||||
lineText.push(segment)
|
|
||||||
column += segment.length
|
|
||||||
bodyText.push(lineText.join(''))
|
|
||||||
row++; column = 0
|
|
||||||
@lineCount = row
|
|
||||||
|
|
||||||
|
# recursive helper function; mutates vars above
|
||||||
|
extractTabStops = (bodyTree) ->
|
||||||
|
for segment in bodyTree
|
||||||
|
if segment.index?
|
||||||
|
{ index, content } = segment
|
||||||
|
index = Infinity if index == 0
|
||||||
|
start = [row, column]
|
||||||
|
extractTabStops(content)
|
||||||
|
tabStopsByIndex[index] = new Range(start, [row, column])
|
||||||
|
else if _.isString(segment)
|
||||||
|
bodyText.push(segment)
|
||||||
|
segmentLines = segment.split('\n')
|
||||||
|
column += segmentLines.shift().length
|
||||||
|
while nextLine = segmentLines.shift()
|
||||||
|
row += 1
|
||||||
|
column = nextLine.length
|
||||||
|
|
||||||
|
extractTabStops(bodyTree)
|
||||||
|
@lineCount = row + 1
|
||||||
@tabStops = []
|
@tabStops = []
|
||||||
for index in _.keys(tabStopsByIndex).sort()
|
for index in _.keys(tabStopsByIndex).sort()
|
||||||
@tabStops.push tabStopsByIndex[index]
|
@tabStops.push tabStopsByIndex[index]
|
||||||
|
|
||||||
bodyText.join('\n')
|
bodyText.join('')
|
||||||
|
@ -2,38 +2,46 @@ fs = require 'fs'
|
|||||||
PEG = require 'pegjs'
|
PEG = require 'pegjs'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
SnippetExpansion = require 'snippets/src/snippet-expansion'
|
SnippetExpansion = require 'snippets/src/snippet-expansion'
|
||||||
|
Snippet = require './snippet'
|
||||||
|
require './package-extensions'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
name: 'Snippets'
|
|
||||||
snippetsByExtension: {}
|
snippetsByExtension: {}
|
||||||
snippetsParser: PEG.buildParser(fs.read(require.resolve 'snippets/snippets.pegjs'), trackLineAndColumn: true)
|
parser: PEG.buildParser(fs.read(require.resolve 'snippets/snippets.pegjs'), trackLineAndColumn: true)
|
||||||
|
userSnippetsDir: fs.join(config.configDirPath, 'snippets')
|
||||||
|
|
||||||
activate: (@rootView) ->
|
activate: (@rootView) ->
|
||||||
@loadSnippets()
|
window.snippets = this
|
||||||
|
@loadAll()
|
||||||
@rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor)
|
@rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor)
|
||||||
|
|
||||||
loadSnippets: ->
|
loadAll: ->
|
||||||
snippetsDir = fs.join(config.configDirPath, 'snippets')
|
for pack in atom.getPackages()
|
||||||
if fs.exists(snippetsDir)
|
pack.loadSnippets()
|
||||||
@loadSnippetsFile(path) for path in fs.list(snippetsDir) when fs.extension(path) == '.snippets'
|
|
||||||
|
|
||||||
loadSnippetsFile: (path) ->
|
for snippetsPath in fs.list(@userSnippetsDir)
|
||||||
@evalSnippets(fs.base(path, '.snippets'), fs.read(path))
|
@load(snippetsPath)
|
||||||
|
|
||||||
evalSnippets: (extension, text) ->
|
load: (snippetsPath) ->
|
||||||
@snippetsByExtension[extension] = @snippetsParser.parse(text)
|
@add(fs.readObject(snippetsPath))
|
||||||
|
|
||||||
|
add: (snippetsBySelector) ->
|
||||||
|
for selector, snippetsByName of snippetsBySelector
|
||||||
|
snippetsByPrefix = {}
|
||||||
|
for name, attributes of snippetsByName
|
||||||
|
{ prefix, body } = attributes
|
||||||
|
bodyTree = @parser.parse(body)
|
||||||
|
snippet = new Snippet({name, prefix, bodyTree})
|
||||||
|
snippetsByPrefix[snippet.prefix] = snippet
|
||||||
|
syntax.addProperties(selector, snippets: snippetsByPrefix)
|
||||||
|
|
||||||
enableSnippetsInEditor: (editor) ->
|
enableSnippetsInEditor: (editor) ->
|
||||||
editor.command 'snippets:expand', (e) =>
|
editor.command 'snippets:expand', (e) =>
|
||||||
editSession = editor.activeEditSession
|
editSession = editor.activeEditSession
|
||||||
prefix = editSession.getCursor().getCurrentWordPrefix()
|
prefix = editSession.getCursor().getCurrentWordPrefix()
|
||||||
if snippet = @snippetsByExtension[editSession.getFileExtension()]?[prefix]
|
if snippet = syntax.getProperty(editSession.getCursorScopes(), "snippets.#{prefix}")
|
||||||
editSession.transact ->
|
editSession.transact ->
|
||||||
snippetExpansion = new SnippetExpansion(snippet, editSession)
|
new SnippetExpansion(snippet, editSession)
|
||||||
editSession.snippetExpansion = snippetExpansion
|
|
||||||
editSession.pushOperation
|
|
||||||
undo: -> snippetExpansion.destroy()
|
|
||||||
redo: (editSession) -> snippetExpansion.restore(editSession)
|
|
||||||
else
|
else
|
||||||
e.abortKeyBinding()
|
e.abortKeyBinding()
|
||||||
|
|
||||||
|
@ -184,3 +184,11 @@ module.exports =
|
|||||||
CoffeeScript.eval(contents, bare: true)
|
CoffeeScript.eval(contents, bare: true)
|
||||||
else
|
else
|
||||||
JSON.parse(contents)
|
JSON.parse(contents)
|
||||||
|
|
||||||
|
readPlist: (path) ->
|
||||||
|
plist = require 'plist'
|
||||||
|
object = null
|
||||||
|
plist.parseString @read(path), (e, data) ->
|
||||||
|
throw new Error(e) if e
|
||||||
|
object = data[0]
|
||||||
|
object
|
||||||
|
Loading…
Reference in New Issue
Block a user