Merge branch 'snippets'

Conflicts:
	src/app/root-view.coffee
This commit is contained in:
Nathan Sobo
2012-06-26 22:58:32 -06:00
24 changed files with 3673 additions and 3788 deletions

View File

@@ -72,8 +72,10 @@ describe "Keymap", ->
expect(insertCharHandler).toHaveBeenCalled()
describe "when the event's target node descends from multiple nodes that match selectors with a binding", ->
it "only triggers bindings on selectors associated with the closest ancestor node", ->
beforeEach ->
keymap.bindKeys '.child-node', 'x': 'foo'
it "only triggers bindings on selectors associated with the closest ancestor node", ->
fooHandler = jasmine.createSpy 'fooHandler'
fragment.on 'foo', fooHandler
@@ -83,6 +85,22 @@ describe "Keymap", ->
expect(deleteCharHandler).not.toHaveBeenCalled()
expect(insertCharHandler).not.toHaveBeenCalled()
describe "when 'abortKeyBinding' is called on the triggered event", ->
it "aborts the current event and tries again with the next-most-specific key binding", ->
fooHandler1 = jasmine.createSpy('fooHandler1').andCallFake (e) ->
expect(deleteCharHandler).not.toHaveBeenCalled()
e.abortKeyBinding()
fooHandler2 = jasmine.createSpy('fooHandler2')
fragment.find('.child-node').on 'foo', fooHandler1
fragment.on 'foo', fooHandler2
target = fragment.find('.grandchild-node')[0]
keymap.handleKeyEvent(keydownEvent('x', target: target))
expect(fooHandler1).toHaveBeenCalled()
expect(fooHandler2).not.toHaveBeenCalled()
expect(deleteCharHandler).toHaveBeenCalled()
describe "when the event bubbles to a node that matches multiple selectors", ->
describe "when the matching selectors differ in specificity", ->
it "triggers the binding for the most specific selector", ->

View File

@@ -7,37 +7,43 @@ describe "Project", ->
project = new Project(require.resolve('fixtures/dir'))
describe ".open(path)", ->
[absolutePath, newBufferHandler] = []
[absolutePath, newBufferHandler, newEditSessionHandler] = []
beforeEach ->
absolutePath = require.resolve('fixtures/dir/a')
newBufferHandler = jasmine.createSpy('newBufferHandler')
project.on 'new-buffer', newBufferHandler
newEditSessionHandler = jasmine.createSpy('newEditSessionHandler')
project.on 'new-edit-session', newEditSessionHandler
describe "when given an absolute path that hasn't been opened previously", ->
it "returns a new edit session for the given path and emits a 'new-buffer' event", ->
it "returns a new edit session for the given path and emits 'new-buffer' and 'new-edit-session' events", ->
editSession = project.open(absolutePath)
expect(editSession.buffer.path).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when given a relative path that hasn't been opened previously", ->
it "returns a new edit session for the given path (relative to the project root) and emits a 'new-buffer' event", ->
it "returns a new edit session for the given path (relative to the project root) and emits 'new-buffer' and 'new-edit-session' events", ->
editSession = project.open('a')
expect(editSession.buffer.path).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when passed the path to a buffer that has already been opened", ->
it "returns a new edit session containing previously opened buffer", ->
it "returns a new edit session containing previously opened buffer and emits a 'new-edit-session' event", ->
editSession = project.open(absolutePath)
newBufferHandler.reset()
expect(project.open(absolutePath).buffer).toBe editSession.buffer
expect(project.open('a').buffer).toBe editSession.buffer
expect(newBufferHandler).not.toHaveBeenCalled()
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when not passed a path", ->
it "returns a new edit session and emits a new-buffer event", ->
it "returns a new edit session and emits 'new-buffer' and 'new-edit-session' events", ->
editSession = project.open()
expect(editSession.buffer.getPath()).toBeUndefined()
expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer)
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe ".resolve(path)", ->
it "returns an absolute path based on the project's root", ->

View File

@@ -0,0 +1,188 @@
Snippets = require 'snippets'
RootView = require 'root-view'
Buffer = require 'buffer'
Editor = require 'editor'
_ = require 'underscore'
fs = require 'fs'
describe "Snippets extension", ->
[buffer, editor] = []
beforeEach ->
rootView = new RootView(require.resolve('fixtures/sample.js'))
rootView.activateExtension(Snippets)
editor = rootView.activeEditor()
buffer = editor.buffer
rootView.simulateDomAttachment()
rootView.enableKeymap()
describe "when 'tab' is triggered on the editor", ->
beforeEach ->
Snippets.evalSnippets 'js', """
snippet t1 "Snippet without tab stops"
this is a test
endsnippet
snippet t2 "With tab stops"
go here next:($2) and finally go here:($3)
go here first:($1)
endsnippet
snippet t3 "With indented second line"
line 1
line 2$1
endsnippet
snippet t4 "With tab stop placeholders"
go here ${1:first} and then here ${2:second}
endsnippet
"""
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]
editor.trigger keydownEvent('tab', target: editor[0])
expect(buffer.lineForRow(0)).toBe "this is a testvar quicksort = function () {"
expect(editor.getCursorScreenPosition()).toEqual [0, 14]
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", ->
anchorCountBefore = editor.activeEditSession.getAnchors().length
editor.setCursorScreenPosition([2, 0])
editor.insertText('t2')
editor.trigger keydownEvent('tab', target: editor[0])
expect(buffer.lineForRow(2)).toBe "go here next:() and finally go here:()"
expect(buffer.lineForRow(3)).toBe "go here first:()"
expect(buffer.lineForRow(4)).toBe " if (items.length <= 1) return items;"
expect(editor.getSelectedBufferRange()).toEqual [[3, 15], [3, 15]]
editor.trigger keydownEvent('tab', target: editor[0])
expect(editor.getSelectedBufferRange()).toEqual [[2, 14], [2, 14]]
editor.insertText 'abc'
editor.trigger keydownEvent('tab', target: editor[0])
expect(editor.getSelectedBufferRange()).toEqual [[2, 40], [2, 40]]
# tab backwards
editor.trigger keydownEvent('tab', shiftKey: true, target: editor[0])
expect(editor.getSelectedBufferRange()).toEqual [[2, 14], [2, 17]] # should highlight text typed at tab stop
editor.trigger keydownEvent('tab', shiftKey: true, target: editor[0])
expect(editor.getSelectedBufferRange()).toEqual [[3, 15], [3, 15]]
# shift-tab on first tab-stop does nothing
editor.trigger keydownEvent('tab', shiftKey: true, target: editor[0])
expect(editor.getCursorScreenPosition()).toEqual [3, 15]
# tab through all tab stops, then tab on last stop to terminate snippet
editor.trigger keydownEvent('tab', target: editor[0])
editor.trigger keydownEvent('tab', target: editor[0])
editor.trigger keydownEvent('tab', target: editor[0])
expect(buffer.lineForRow(2)).toBe "go here next:(abc) and finally go here:( )"
expect(editor.activeEditSession.getAnchors().length).toBe anchorCountBefore
describe "when the tab stops have placeholder text", ->
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]]
describe "when the cursor is moved beyond the bounds of a tab stop", ->
it "terminates the snippet on the next tab", ->
editor.setCursorScreenPosition([2, 0])
editor.insertText('t2')
editor.trigger keydownEvent('tab', target: editor[0])
editor.moveCursorRight()
editor.trigger keydownEvent('tab', target: editor[0])
expect(buffer.lineForRow(3)).toBe "go here first:() "
expect(editor.getCursorBufferPosition()).toEqual [3, 18]
# test we can terminate with shift-tab
editor.setCursorScreenPosition([4, 0])
editor.insertText('t2')
editor.trigger keydownEvent('tab', target: editor[0])
editor.trigger keydownEvent('tab', target: editor[0])
editor.moveCursorRight()
editor.trigger keydownEvent('tab', shiftKey: true, target: editor[0])
expect(editor.getCursorBufferPosition()).toEqual [4, 15]
describe "when a the start of the snippet is indented", ->
it "indents the subsequent lines of the snippet to be even with the start of the first line", ->
editor.setCursorScreenPosition([2, Infinity])
editor.insertText ' t3'
editor.trigger 'snippets:expand'
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items; line 1"
expect(buffer.lineForRow(3)).toBe " line 2"
expect(editor.getCursorBufferPosition()).toEqual [3, 12]
describe "when the letters preceding the cursor don't match a snippet", ->
it "inserts a tab as normal", ->
editor.insertText("xte")
expect(editor.getCursorScreenPosition()).toEqual [0, 3]
editor.trigger 'tab'
expect(buffer.lineForRow(0)).toBe "xte var quicksort = function () {"
expect(editor.getCursorScreenPosition()).toEqual [0, 5]
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
"""
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
"""
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]]]

View File

@@ -0,0 +1,22 @@
_ = require 'underscore'
describe "underscore extensions", ->
describe "_.adviseBefore", ->
[object, calls] = []
beforeEach ->
calls = []
object = {
method: (args...) ->
calls.push(["original", this, args])
}
it "calls the given function before the advised method", ->
_.adviseBefore object, 'method', (args...) -> calls.push(["advice", this, args])
object.method(1, 2, 3)
expect(calls).toEqual [['advice', object, [1, 2, 3]], ['original', object, [1, 2, 3]]]
it "cancels the original method's invocation if the advice returns true", ->
_.adviseBefore object, 'method', -> false
object.method(1, 2, 3)
expect(calls).toEqual []

View File

@@ -0,0 +1,24 @@
Range = require 'range'
module.exports =
class AnchorRange
start: null
end: null
constructor: (@editSession, bufferRange) ->
bufferRange = Range.fromObject(bufferRange)
@startAnchor = @editSession.addAnchorAtBufferPosition(bufferRange.start, ignoreEqual: true)
@endAnchor = @editSession.addAnchorAtBufferPosition(bufferRange.end)
getBufferRange: ->
new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition())
getScreenRange: ->
new Range(@startAnchor.getScreenPosition(), @endAnchor.getScreenPosition())
containsBufferPosition: (bufferPosition) ->
@getBufferRange().containsPoint(bufferPosition)
destroy: ->
@startAnchor.destroy()
@endAnchor.destroy()

View File

@@ -8,12 +8,17 @@ class Anchor
bufferPosition: null
screenPosition: null
constructor: (@editSession) ->
constructor: (@editSession, options = {}) ->
{ @ignoreEqual } = options
handleBufferChange: (e) ->
{ oldRange, newRange } = e
position = @getBufferPosition()
return if position.isLessThan(oldRange.end)
if @ignoreEqual
return if position.isLessThanOrEqual(oldRange.end)
else
return if position.isLessThan(oldRange.end)
newRow = newRange.end.row
newColumn = newRange.end.column
@@ -56,4 +61,7 @@ class Anchor
screenPosition = @editSession.screenPositionForBufferPosition(@bufferPosition, options)
@setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false)
destroy: ->
@editSession.removeAnchor(this)
_.extend(Anchor.prototype, EventEmitter)

View File

@@ -9,13 +9,15 @@ module.exports =
class Atom
keymap: null
windows: null
userConfigurationPath: null
configFilePath: null
configDirPath: null
rootViewStates: null
constructor: (@loadPath, nativeMethods) ->
@windows = []
@setUpKeymap()
@userConfigurationPath = fs.absolute "~/.atom/atom.coffee"
@configDirPath = fs.absolute("~/.atom")
@configFilePath = fs.join(@configDirPath, "atom.coffee")
@rootViewStates = {}
setUpKeymap: ->

View File

@@ -12,7 +12,7 @@ class BindingSet
commandForEvent: null
parser: null
constructor: (@selector, mapOrFunction) ->
constructor: (@selector, mapOrFunction, @index) ->
@parser = PEG.buildParser(fs.read(require.resolve 'keystroke-pattern.pegjs'))
@specificity = Specificity(@selector)
@commandsByKeystrokes = {}

View File

@@ -26,6 +26,12 @@ class Buffer
getPath: ->
@path
getExtension: ->
if @getPath()
@getPath().split('/').pop().split('.').pop()
else
null
setPath: (path) ->
@path = path
@trigger "path-change", this

View File

@@ -150,6 +150,9 @@ class Cursor
getCurrentLineBufferRange: ->
@editSession.bufferRangeForBufferRow(@getBufferRow())
getCurrentWordPrefix: ->
@editSession.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])
isAtBeginningOfLine: ->
@getBufferPosition().column == 0

View File

@@ -5,6 +5,7 @@ DisplayBuffer = require 'display-buffer'
Cursor = require 'cursor'
Selection = require 'selection'
EventEmitter = require 'event-emitter'
AnchorRange = require 'anchor-range'
_ = require 'underscore'
module.exports =
@@ -22,6 +23,7 @@ class EditSession
scrollLeft: 0
displayBuffer: null
anchors: null
anchorRanges: null
cursors: null
selections: null
autoIndent: true
@@ -34,6 +36,7 @@ class EditSession
@displayBuffer = new DisplayBuffer(@buffer, { @tabText })
@tokenizedBuffer = @displayBuffer.tokenizedBuffer
@anchors = []
@anchorRanges = []
@cursors = []
@selections = []
@addCursorAtScreenPosition([0, 0])
@@ -234,11 +237,21 @@ class EditSession
getAnchors: ->
new Array(@anchors...)
addAnchor: ->
anchor = new Anchor(this)
addAnchor: (options) ->
anchor = new Anchor(this, options)
@anchors.push(anchor)
anchor
addAnchorAtBufferPosition: (bufferPosition, options) ->
anchor = @addAnchor(options)
anchor.setBufferPosition(bufferPosition)
anchor
addAnchorRange: (range) ->
anchorRange = new AnchorRange(this, range)
@anchorRanges.push(anchorRange)
anchorRange
removeAnchor: (anchor) ->
_.remove(@anchors, anchor)
@@ -342,6 +355,9 @@ class EditSession
getSelectedText: ->
@getLastSelection().getText()
getTextInBufferRange: (range) ->
@buffer.getTextInRange(range)
moveCursorUp: ->
@moveCursors (cursor) -> cursor.moveUp()

View File

@@ -186,6 +186,7 @@ class Editor extends View
getSelectionsOrderedByBufferPosition: -> @activeEditSession.getSelectionsOrderedByBufferPosition()
getLastSelectionInBuffer: -> @activeEditSession.getLastSelectionInBuffer()
getSelectedText: -> @activeEditSession.getSelectedText()
getSelectedBufferRange: -> @activeEditSession.getSelectedBufferRange()
setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options)
setSelectedBufferRanges: (bufferRanges, options) -> @activeEditSession.setSelectedBufferRanges(bufferRanges, options)
addSelectionForBufferRange: (bufferRange, options) -> @activeEditSession.addSelectionForBufferRange(bufferRange, options)

View File

@@ -20,21 +20,21 @@ class Keymap
'meta-o': 'open'
$(document).on 'new-window', => $native.newWindow()
$(document).on 'open-user-configuration', => atom.open(atom.userConfigurationPath)
$(document).on 'open-user-configuration', => atom.open(atom.configFilePath)
$(document).on 'open', =>
path = $native.openDialog()
atom.open(path) if path
bindKeys: (selector, bindings) ->
@bindingSets.unshift(new BindingSet(selector, bindings))
index = @bindingSets.length
@bindingSets.unshift(new BindingSet(selector, bindings, index))
bindingsForElement: (element) ->
keystrokeMap = {}
currentNode = $(element)
while currentNode.length
bindingSets = @bindingSets.filter (set) -> currentNode.is(set.selector)
bindingSets.sort (a, b) -> b.specificity - a.specificity
bindingSets = @bindingSetsForNode(currentNode)
_.defaults(keystrokeMap, set.commandsByKeystrokes) for set in bindingSets
currentNode = currentNode.parent()
@@ -46,12 +46,11 @@ class Keymap
@queuedKeystrokes = null
currentNode = $(event.target)
while currentNode.length
candidateBindingSets = @bindingSets.filter (set) -> currentNode.is(set.selector)
candidateBindingSets.sort (a, b) -> b.specificity - a.specificity
candidateBindingSets = @bindingSetsForNode(currentNode)
for bindingSet in candidateBindingSets
command = bindingSet.commandForEvent(event)
if command
@triggerCommandEvent(event, command)
continue if @triggerCommandEvent(event, command)
return false
else if command == false
return false
@@ -63,10 +62,23 @@ class Keymap
!isMultiKeystroke
bindingSetsForNode: (node) ->
bindingSets = @bindingSets.filter (set) -> node.is(set.selector)
bindingSets.sort (a, b) ->
if b.specificity == a.specificity
b.index - a.index
else
b.specificity - a.specificity
triggerCommandEvent: (keyEvent, commandName) ->
commandEvent = $.Event(commandName)
commandEvent.keyEvent = keyEvent
aborted = false
commandEvent.abortKeyBinding = ->
@stopImmediatePropagation()
aborted = true
$(keyEvent.target).trigger(commandEvent)
aborted
multiKeystrokeStringForEvent: (event) ->
currentKeystroke = @keystrokeStringForEvent(event)

View File

@@ -0,0 +1,6 @@
window.keymap.bindKeys '.editor'
'tab': 'snippets:expand'
window.keymap.bindKeys '.editor'
'tab': 'snippets:next-tab-stop'
'shift-tab': 'snippets:previous-tab-stop'

View File

@@ -90,6 +90,7 @@ class Project
softWrap: @getSoftWrap()
@editSessions.push editSession
@trigger 'new-edit-session', editSession
editSession
buildBuffer: (filePath) ->

View File

@@ -40,6 +40,9 @@ class Range
inspect: ->
"[#{@start.inspect()} - #{@end.inspect()}]"
add: (point) ->
new Range(@start.add(point), @end.add(point))
intersectsWith: (otherRange) ->
if @start.isLessThanOrEqual(otherRange.start)
@end.isGreaterThanOrEqual(otherRange.start)

View File

@@ -174,7 +174,7 @@ class RootView extends View
loadUserConfiguration: ->
try
require atom.userConfigurationPath if fs.exists(atom.userConfigurationPath)
require atom.configFilePath if fs.exists(atom.configFilePath)
catch error
console.error "Failed to load `#{atom.userConfigurationPath}`", error.message, error
console.error "Failed to load `#{atom.configFilePath}`", error.message, error

View File

@@ -23,8 +23,7 @@ class TokenizedBuffer
@aceAdaptor = new AceAdaptor(this)
requireAceMode: ->
extension = if @buffer.getPath() then @buffer.getPath().split('/').pop().split('.').pop() else null
modeName = switch extension
modeName = switch @buffer.getExtension()
when 'js' then 'javascript'
when 'coffee' then 'coffee'
when 'rb', 'ru' then 'ruby'

View File

@@ -0,0 +1 @@
module.exports = require 'extensions/snippets/snippets.coffee'

View File

@@ -0,0 +1,36 @@
_ = require 'underscore'
Range = require 'range'
module.exports =
class Snippet
body: null
lineCount: null
tabStops: null
constructor: ({@bodyPosition, @prefix, @description, body}) ->
@body = @extractTabStops(body)
extractTabStops: (bodyLines) ->
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 + 1
@tabStops = []
for index in _.keys(tabStopsByIndex).sort()
@tabStops.push tabStopsByIndex[index]
bodyText.join('\n')

View File

@@ -0,0 +1,101 @@
fs = require 'fs'
PEG = require 'pegjs'
_ = require 'underscore'
module.exports =
name: 'Snippets'
snippetsByExtension: {}
snippetsParser: PEG.buildParser(fs.read(require.resolve 'extensions/snippets/snippets.pegjs'), trackLineAndColumn: true)
activate: (@rootView) ->
@loadSnippets()
@rootView.on 'editor-open', (e, editor) => @enableSnippetsInEditor(editor)
loadSnippets: ->
snippetsDir = fs.join(atom.configDirPath, 'snippets')
return unless fs.exists(snippetsDir)
@loadSnippetsFile(path) for path in fs.list(snippetsDir) when fs.extension(path) == '.snippets'
loadSnippetsFile: (path) ->
@evalSnippets(fs.base(path, '.snippets'), fs.read(path))
evalSnippets: (extension, text) ->
@snippetsByExtension[extension] = @snippetsParser.parse(text)
enableSnippetsInEditor: (editor) ->
editor.on 'snippets:expand', (e) =>
editSession = editor.activeEditSession
editSession.snippetsSession ?= new SnippetsSession(editSession, @snippetsByExtension)
e.abortKeyBinding() unless editSession.snippetsSession.expandSnippet()
editor.on 'snippets:next-tab-stop', (e) ->
editSession = editor.activeEditSession
e.abortKeyBinding() unless editSession.snippetsSession?.goToNextTabStop()
editor.on 'snippets:previous-tab-stop', (e) ->
editSession = editor.activeEditSession
e.abortKeyBinding() unless editSession.snippetsSession?.goToPreviousTabStop()
class SnippetsSession
tabStopAnchorRanges: null
constructor: (@editSession, @snippetsByExtension) ->
@editSession.on 'move-cursor', => @terminateIfCursorIsOutsideTabStops()
expandSnippet: ->
return unless snippets = @snippetsByExtension[@editSession.buffer.getExtension()]
prefix = @editSession.getLastCursor().getCurrentWordPrefix()
if snippet = snippets[prefix]
@editSession.selectToBeginningOfWord()
startPosition = @editSession.getCursorBufferPosition()
@editSession.insertText(snippet.body)
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
@indentSnippet(startPosition.row, snippet)
true
else
false
placeTabStopAnchorRanges: (startPosition, tabStopRanges) ->
return unless tabStopRanges.length
@tabStopAnchorRanges = tabStopRanges.map (tabStopRange) =>
{ start, end } = tabStopRange
@editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)])
@setTabStopIndex(0)
indentSnippet: (startRow, snippet) ->
if snippet.lineCount > 1
initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0]
for row in [startRow + 1...startRow + snippet.lineCount]
@editSession.buffer.insert([row, 0], initialIndent)
goToNextTabStop: ->
return false unless @ensureValidTabStops()
nextIndex = @tabStopIndex + 1
if nextIndex < @tabStopAnchorRanges.length
@setTabStopIndex(nextIndex)
true
else
@terminateActiveSnippet()
false
goToPreviousTabStop: ->
return false unless @ensureValidTabStops()
@setTabStopIndex(@tabStopIndex - 1) if @tabStopIndex > 0
true
ensureValidTabStops: ->
@tabStopAnchorRanges? and @terminateIfCursorIsOutsideTabStops()
setTabStopIndex: (@tabStopIndex) ->
@editSession.setSelectedBufferRange(@tabStopAnchorRanges[@tabStopIndex].getBufferRange())
terminateIfCursorIsOutsideTabStops: ->
return unless @tabStopAnchorRanges
position = @editSession.getCursorBufferPosition()
for anchorRange in @tabStopAnchorRanges
return true if anchorRange.containsBufferPosition(position)
@terminateActiveSnippet()
false
terminateActiveSnippet: ->
anchorRange.destroy() for anchorRange in @tabStopAnchorRanges
@tabStopAnchorRanges = null

View File

@@ -0,0 +1,39 @@
{
var Snippet = require('extensions/snippets/snippet');
var Point = require('point');
}
snippets = snippets:snippet+ ws? {
var snippetsByPrefix = {};
snippets.forEach(function(snippet) {
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
simpleTabStop = '$' index:[0-9]+ {
return { index: parseInt(index), placeholderText: '' };
}
tabStopWithPlaceholder = '${' index:[0-9]+ ':' placeholderText:[^}]* '}' {
return { index: parseInt(index), placeholderText: placeholderText.join('') };
}
end = 'endsnippet'
ws = ([ \n] / comment)+
comment = '#' [^\n]*

View File

@@ -10,6 +10,12 @@ _.mixin
sum += elt for elt in array
sum
adviseBefore: (object, methodName, advice) ->
original = object[methodName]
object[methodName] = (args...) ->
unless advice.apply(this, args) == false
original.apply(this, args)
escapeRegExp: (string) ->
# Referring to the table here:
# https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/regexp

6915
vendor/pegjs.js vendored

File diff suppressed because it is too large Load Diff