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'
|
||||
require 'window'
|
||||
requireStylesheet "jasmine.css"
|
||||
require.paths.unshift(require.resolve('fixtures/packages'))
|
||||
fixturePackagesPath = require.resolve('fixtures/packages')
|
||||
require.paths.unshift(fixturePackagesPath)
|
||||
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
|
||||
|
||||
# Load TextMate bundles, which specs rely on (but not other packages)
|
||||
atom.loadPackages(atom.getAvailableTextMateBundles())
|
||||
atom.loadTextMatePackages()
|
||||
|
||||
beforeEach ->
|
||||
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`
|
||||
window.config = new Config()
|
||||
config.packageDirPaths.unshift(fixturePackagesPath)
|
||||
spyOn(config, 'load')
|
||||
spyOn(config, 'save')
|
||||
config.set "editor.fontSize", 16
|
||||
|
@ -1,4 +1,7 @@
|
||||
Range = require 'range'
|
||||
EventEmitter = require 'event-emitter'
|
||||
Subscriber = require 'subscriber'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class AnchorRange
|
||||
@ -6,11 +9,14 @@ class AnchorRange
|
||||
end: null
|
||||
buffer: null
|
||||
editSession: null # optional
|
||||
destroyed: false
|
||||
|
||||
constructor: (bufferRange, @buffer, @editSession) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
@startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreChangesStartingOnAnchor: true)
|
||||
@endAnchor = @buffer.addAnchorAtPosition(bufferRange.end)
|
||||
@subscribe @startAnchor, 'destroyed', => @destroy()
|
||||
@subscribe @endAnchor, 'destroyed', => @destroy()
|
||||
|
||||
getBufferRange: ->
|
||||
new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition())
|
||||
@ -22,7 +28,14 @@ class AnchorRange
|
||||
@getBufferRange().containsPoint(bufferPosition)
|
||||
|
||||
destroy: ->
|
||||
return if @destroyed
|
||||
@unsubscribe()
|
||||
@startAnchor.destroy()
|
||||
@endAnchor.destroy()
|
||||
@buffer.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
|
||||
ignoreChangesStartingOnAnchor: false
|
||||
strong: false
|
||||
destroyed: false
|
||||
|
||||
constructor: (@buffer, options = {}) ->
|
||||
{ @editSession, @ignoreChangesStartingOnAnchor, @strong } = options
|
||||
@ -81,8 +82,10 @@ class Anchor
|
||||
@setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false, autoscroll: options.autoscroll)
|
||||
|
||||
destroy: ->
|
||||
return if @destroyed
|
||||
@buffer.removeAnchor(this)
|
||||
@editSession?.removeAnchor(this)
|
||||
@destroyed = true
|
||||
@trigger 'destroyed'
|
||||
|
||||
_.extend(Anchor.prototype, EventEmitter)
|
||||
|
@ -9,15 +9,18 @@ class AtomPackage extends Package
|
||||
constructor: (@name) ->
|
||||
super
|
||||
@keymapsDirPath = fs.join(@path, 'keymaps')
|
||||
if @requireModule
|
||||
@module = require(@path)
|
||||
@module.name = @name
|
||||
|
||||
load: ->
|
||||
@loadMetadata()
|
||||
@loadKeymaps()
|
||||
@loadStylesheets()
|
||||
rootView.activatePackage(@name, @module) if @module
|
||||
try
|
||||
if @requireModule
|
||||
@module = require(@path)
|
||||
@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: ->
|
||||
if metadataPath = fs.resolveExtension(fs.join(@path, "package"), ['cson', 'json'])
|
||||
|
@ -12,7 +12,23 @@ _.extend atom,
|
||||
|
||||
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 = []
|
||||
for packageDirPath in config.packageDirPaths
|
||||
packageNames = fs.list(packageDirPath)
|
||||
@ -20,17 +36,7 @@ _.extend atom,
|
||||
.map((packagePath) -> fs.base(packagePath))
|
||||
allPackageNames.push(packageNames...)
|
||||
_.unique(allPackageNames)
|
||||
|
||||
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)
|
||||
.filter (name) -> not _.contains(disabledPackages, name)
|
||||
|
||||
loadThemes: ->
|
||||
themeNames = config.get("core.themes") ? ['IR_Black']
|
||||
|
@ -2,18 +2,13 @@ fs = require 'fs'
|
||||
|
||||
module.exports =
|
||||
class Package
|
||||
@load: (name) ->
|
||||
@build: (name) ->
|
||||
AtomPackage = require 'atom-package'
|
||||
TextMatePackage = require 'text-mate-package'
|
||||
|
||||
try
|
||||
if TextMatePackage.testName(name)
|
||||
new TextMatePackage(name).load()
|
||||
else
|
||||
new AtomPackage(name).load()
|
||||
catch e
|
||||
console.warn "Failed to load package named '#{name}'", e.stack
|
||||
|
||||
if TextMatePackage.testName(name)
|
||||
new TextMatePackage(name)
|
||||
else
|
||||
new AtomPackage(name)
|
||||
|
||||
name: null
|
||||
path: null
|
||||
@ -29,10 +24,3 @@ class Package
|
||||
else
|
||||
@requireModule = true
|
||||
@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")
|
||||
@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: ->
|
||||
return @grammars if @grammars
|
||||
@grammars = []
|
||||
|
@ -1,39 +1,15 @@
|
||||
{
|
||||
var Snippet = require('snippets/src/snippet');
|
||||
var Point = require('point');
|
||||
}
|
||||
bodyContent = content:(tabStop / bodyContentText)* { return content; }
|
||||
bodyContentText = text:bodyContentChar+ { return text.join(''); }
|
||||
bodyContentChar = !tabStop char:. { return char; }
|
||||
|
||||
snippets = snippets:snippet+ ws? {
|
||||
var snippetsByPrefix = {};
|
||||
snippets.forEach(function(snippet) {
|
||||
snippetsByPrefix[snippet.prefix] = snippet
|
||||
});
|
||||
return snippetsByPrefix;
|
||||
}
|
||||
placeholderContent = content:(tabStop / placeholderContentText)* { return content; }
|
||||
placeholderContentText = text:placeholderContentChar+ { return text.join(''); }
|
||||
placeholderContentChar = !tabStop char:[^}] { return char; }
|
||||
|
||||
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
|
||||
simpleTabStop = '$' index:[0-9]+ {
|
||||
return { index: parseInt(index), placeholderText: '' };
|
||||
return { index: parseInt(index), content: [] };
|
||||
}
|
||||
tabStopWithPlaceholder = '${' index:[0-9]+ ':' placeholderText:[^}]* '}' {
|
||||
return { index: parseInt(index), placeholderText: placeholderText.join('') };
|
||||
tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' {
|
||||
return { index: parseInt(index), content: content };
|
||||
}
|
||||
|
||||
end = 'endsnippet'
|
||||
ws = ([ \n] / comment)+
|
||||
comment = '#' [^\n]*
|
||||
|
@ -1,14 +1,19 @@
|
||||
Snippets = require 'snippets'
|
||||
Snippet = require 'snippets/src/snippet'
|
||||
RootView = require 'root-view'
|
||||
Buffer = require 'buffer'
|
||||
Editor = require 'editor'
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
AtomPackage = require 'atom-package'
|
||||
TextMatePackage = require 'text-mate-package'
|
||||
|
||||
describe "Snippets extension", ->
|
||||
[buffer, editor] = []
|
||||
beforeEach ->
|
||||
rootView = new RootView(require.resolve('fixtures/sample.js'))
|
||||
spyOn(AtomPackage.prototype, 'loadSnippets')
|
||||
spyOn(TextMatePackage.prototype, 'loadSnippets')
|
||||
atom.loadPackage("snippets")
|
||||
editor = rootView.getActiveEditor()
|
||||
buffer = editor.getBuffer()
|
||||
@ -17,36 +22,50 @@ describe "Snippets extension", ->
|
||||
|
||||
afterEach ->
|
||||
rootView.remove()
|
||||
delete window.snippets
|
||||
|
||||
describe "when 'tab' is triggered on the editor", ->
|
||||
beforeEach ->
|
||||
Snippets.evalSnippets 'js', """
|
||||
snippet t1 "Snippet without tab stops"
|
||||
this is a test
|
||||
endsnippet
|
||||
snippets.add
|
||||
".source.js":
|
||||
"without tab stops":
|
||||
prefix: "t1"
|
||||
body: "this is a test"
|
||||
|
||||
snippet t2 "With tab stops"
|
||||
go here next:($2) and finally go here:($3)
|
||||
go here first:($1)
|
||||
"tab stops":
|
||||
prefix: "t2"
|
||||
body: """
|
||||
go here next:($2) and finally go here:($0)
|
||||
go here first:($1)
|
||||
|
||||
endsnippet
|
||||
"""
|
||||
|
||||
snippet t3 "With indented second line"
|
||||
line 1
|
||||
line 2$1
|
||||
"indented second line":
|
||||
prefix: "t3"
|
||||
body: """
|
||||
line 1
|
||||
line 2$1
|
||||
|
||||
endsnippet
|
||||
"""
|
||||
|
||||
snippet t4 "With tab stop placeholders"
|
||||
go here ${1:first} and then here ${2:second}
|
||||
"tab stop placeholders":
|
||||
prefix: "t4"
|
||||
body: """
|
||||
go here ${1:first
|
||||
think a while}, and then here ${2:second}
|
||||
|
||||
endsnippet
|
||||
"""
|
||||
|
||||
snippet t5 "Caused problems with undo"
|
||||
first line$1
|
||||
${2:placeholder ending second line}
|
||||
endsnippet
|
||||
"""
|
||||
"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}
|
||||
"""
|
||||
|
||||
describe "when the letters preceding the cursor trigger a snippet", ->
|
||||
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", ->
|
||||
editor.insertText 't4'
|
||||
editor.trigger 'snippets:expand'
|
||||
expect(buffer.lineForRow(0)).toBe 'go here first and then here second'
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[0, 8], [0, 13]]
|
||||
expect(buffer.lineForRow(0)).toBe 'go here first'
|
||||
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", ->
|
||||
it "terminates the snippet", ->
|
||||
@ -152,92 +185,68 @@ describe "Snippets extension", ->
|
||||
|
||||
describe "when a previous snippet expansion has just been undone", ->
|
||||
it "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", ->
|
||||
editor.insertText 't5\n'
|
||||
editor.insertText 't6\n'
|
||||
editor.setCursorBufferPosition [0, 2]
|
||||
editor.trigger keydownEvent('tab', target: editor[0])
|
||||
expect(buffer.lineForRow(0)).toBe "first line"
|
||||
editor.undo()
|
||||
expect(buffer.lineForRow(0)).toBe "t5"
|
||||
expect(buffer.lineForRow(0)).toBe "t6"
|
||||
editor.trigger keydownEvent('tab', target: editor[0])
|
||||
expect(buffer.lineForRow(0)).toBe "first line"
|
||||
|
||||
describe "when a snippet expansion is undone and redone", ->
|
||||
it "recreates the snippet's tab stops", ->
|
||||
editor.insertText ' t5\n'
|
||||
editor.insertText ' t6\n'
|
||||
editor.setCursorBufferPosition [0, 6]
|
||||
editor.trigger keydownEvent('tab', target: editor[0])
|
||||
expect(buffer.lineForRow(0)).toBe " first line"
|
||||
editor.undo()
|
||||
editor.redo()
|
||||
|
||||
expect(editor.getCursorBufferPosition()).toEqual [0, 14]
|
||||
editor.trigger keydownEvent('tab', target: editor[0])
|
||||
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", ->
|
||||
anotherEditor = editor.splitRight()
|
||||
describe "snippet loading", ->
|
||||
it "loads snippets from all atom packages with a snippets directory", ->
|
||||
jasmine.unspy(AtomPackage.prototype, 'loadSnippets')
|
||||
snippets.loadAll()
|
||||
|
||||
editor.insertText ' t5\n'
|
||||
editor.setCursorBufferPosition [0, 6]
|
||||
editor.trigger keydownEvent('tab', target: editor[0])
|
||||
expect(buffer.lineForRow(0)).toBe " first line"
|
||||
editor.undo()
|
||||
expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet
|
||||
|
||||
anotherEditor.redo()
|
||||
expect(anotherEditor.getCursorBufferPosition()).toEqual [0, 14]
|
||||
anotherEditor.trigger keydownEvent('tab', target: anotherEditor[0])
|
||||
expect(anotherEditor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]]
|
||||
it "loads snippets from all TextMate packages with snippets", ->
|
||||
jasmine.unspy(TextMatePackage.prototype, 'loadSnippets')
|
||||
snippets.loadAll()
|
||||
|
||||
describe ".loadSnippetsFile(path)", ->
|
||||
it "loads the snippets in the given file", ->
|
||||
spyOn(fs, 'read').andReturn """
|
||||
snippet t1 "Test snippet 1"
|
||||
this is a test 1
|
||||
endsnippet
|
||||
snippet = syntax.getProperty(['.source.js'], 'snippets.fun')
|
||||
expect(snippet.constructor).toBe Snippet
|
||||
expect(snippet.prefix).toBe 'fun'
|
||||
expect(snippet.name).toBe 'Function'
|
||||
expect(snippet.body).toBe """
|
||||
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", ->
|
||||
it "can parse multiple snippets", ->
|
||||
snippets = Snippets.snippetsParser.parse """
|
||||
snippet t1 "Test snippet 1"
|
||||
this is a test 1
|
||||
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
|
||||
it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", ->
|
||||
bodyTree = Snippets.parser.parse """
|
||||
the quick brown $1fox ${2:jumped ${3:over}
|
||||
}the ${4:lazy} dog
|
||||
"""
|
||||
|
||||
snippet = snippets['t1']
|
||||
expect(snippet.body).toBe """
|
||||
go here next:() and finally go here:()
|
||||
go here first:()
|
||||
"""
|
||||
|
||||
expect(snippet.tabStops).toEqual [[[1, 15], [1, 15]], [[0, 14], [0, 14]], [[0, 37], [0, 37]]]
|
||||
expect(bodyTree).toEqual [
|
||||
"the quick brown ",
|
||||
{ index: 1, content: [] },
|
||||
"fox ",
|
||||
{
|
||||
index: 2,
|
||||
content: [
|
||||
"jumped ",
|
||||
{ index: 3, content: ["over"]},
|
||||
"\n"
|
||||
],
|
||||
}
|
||||
"the "
|
||||
{ index: 4, content: ["lazy"] },
|
||||
" dog"
|
||||
]
|
||||
|
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'
|
||||
|
||||
module.exports =
|
||||
class SnippetExpansion
|
||||
snippet: null
|
||||
tabStopAnchorRanges: null
|
||||
settingTabStop: false
|
||||
|
||||
constructor: (snippet, @editSession) ->
|
||||
constructor: (@snippet, @editSession) ->
|
||||
@editSession.selectToBeginningOfWord()
|
||||
startPosition = @editSession.getCursorBufferPosition()
|
||||
@editSession.transact =>
|
||||
@editSession.insertText(snippet.body, autoIndent: false)
|
||||
if snippet.tabStops.length
|
||||
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
|
||||
if snippet.lineCount > 1
|
||||
@indentSubsequentLines(startPosition.row, snippet)
|
||||
editSession.pushOperation
|
||||
do: =>
|
||||
@subscribe @editSession, 'cursor-moved.snippet-expansion', (e) => @cursorMoved(e)
|
||||
@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}) =>
|
||||
return if @settingTabStop
|
||||
cursorMoved: ({oldBufferPosition, newBufferPosition}) ->
|
||||
return if @settingTabStop
|
||||
|
||||
oldTabStops = @tabStopsForBufferPosition(oldBufferPosition)
|
||||
newTabStops = @tabStopsForBufferPosition(newBufferPosition)
|
||||
oldTabStops = @tabStopsForBufferPosition(oldBufferPosition)
|
||||
newTabStops = @tabStopsForBufferPosition(newBufferPosition)
|
||||
|
||||
@destroy() unless _.intersect(oldTabStops, newTabStops).length
|
||||
@destroy() unless _.intersect(oldTabStops, newTabStops).length
|
||||
|
||||
placeTabStopAnchorRanges: (startPosition, tabStopRanges) ->
|
||||
return unless @snippet.tabStops.length > 0
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
indentSubsequentLines: (startRow, snippet) ->
|
||||
initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0]
|
||||
for row in [startRow + 1...startRow + snippet.lineCount]
|
||||
@ -68,11 +79,13 @@ class SnippetExpansion
|
||||
_.intersection(@tabStopAnchorRanges, @editSession.anchorRangesForBufferPosition(bufferPosition))
|
||||
|
||||
destroy: ->
|
||||
@unsubscribe()
|
||||
anchorRange.destroy() for anchorRange in @tabStopAnchorRanges
|
||||
@editSession.off '.snippet-expansion'
|
||||
@editSession.snippetExpansion = null
|
||||
|
||||
restore: (@editSession) ->
|
||||
@editSession.snippetExpansion = this
|
||||
@tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) =>
|
||||
@editSession.addAnchorRange(anchorRange.getBufferRange())
|
||||
|
||||
_.extend(SnippetExpansion.prototype, Subscriber)
|
||||
|
@ -3,34 +3,41 @@ Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class Snippet
|
||||
name: null
|
||||
prefix: null
|
||||
body: null
|
||||
lineCount: null
|
||||
tabStops: null
|
||||
|
||||
constructor: ({@bodyPosition, @prefix, @description, body}) ->
|
||||
@body = @extractTabStops(body)
|
||||
constructor: ({@name, @prefix, bodyTree}) ->
|
||||
@body = @extractTabStops(bodyTree)
|
||||
|
||||
extractTabStops: (bodyLines) ->
|
||||
extractTabStops: (bodyTree) ->
|
||||
tabStopsByIndex = {}
|
||||
bodyText = []
|
||||
|
||||
[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 = []
|
||||
for index in _.keys(tabStopsByIndex).sort()
|
||||
@tabStops.push tabStopsByIndex[index]
|
||||
|
||||
bodyText.join('\n')
|
||||
bodyText.join('')
|
||||
|
@ -2,38 +2,46 @@ fs = require 'fs'
|
||||
PEG = require 'pegjs'
|
||||
_ = require 'underscore'
|
||||
SnippetExpansion = require 'snippets/src/snippet-expansion'
|
||||
Snippet = require './snippet'
|
||||
require './package-extensions'
|
||||
|
||||
module.exports =
|
||||
name: 'Snippets'
|
||||
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) ->
|
||||
@loadSnippets()
|
||||
window.snippets = this
|
||||
@loadAll()
|
||||
@rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor)
|
||||
|
||||
loadSnippets: ->
|
||||
snippetsDir = fs.join(config.configDirPath, 'snippets')
|
||||
if fs.exists(snippetsDir)
|
||||
@loadSnippetsFile(path) for path in fs.list(snippetsDir) when fs.extension(path) == '.snippets'
|
||||
loadAll: ->
|
||||
for pack in atom.getPackages()
|
||||
pack.loadSnippets()
|
||||
|
||||
loadSnippetsFile: (path) ->
|
||||
@evalSnippets(fs.base(path, '.snippets'), fs.read(path))
|
||||
for snippetsPath in fs.list(@userSnippetsDir)
|
||||
@load(snippetsPath)
|
||||
|
||||
evalSnippets: (extension, text) ->
|
||||
@snippetsByExtension[extension] = @snippetsParser.parse(text)
|
||||
load: (snippetsPath) ->
|
||||
@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) ->
|
||||
editor.command 'snippets:expand', (e) =>
|
||||
editSession = editor.activeEditSession
|
||||
prefix = editSession.getCursor().getCurrentWordPrefix()
|
||||
if snippet = @snippetsByExtension[editSession.getFileExtension()]?[prefix]
|
||||
if snippet = syntax.getProperty(editSession.getCursorScopes(), "snippets.#{prefix}")
|
||||
editSession.transact ->
|
||||
snippetExpansion = new SnippetExpansion(snippet, editSession)
|
||||
editSession.snippetExpansion = snippetExpansion
|
||||
editSession.pushOperation
|
||||
undo: -> snippetExpansion.destroy()
|
||||
redo: (editSession) -> snippetExpansion.restore(editSession)
|
||||
new SnippetExpansion(snippet, editSession)
|
||||
else
|
||||
e.abortKeyBinding()
|
||||
|
||||
|
@ -184,3 +184,11 @@ module.exports =
|
||||
CoffeeScript.eval(contents, bare: true)
|
||||
else
|
||||
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