mirror of
https://github.com/atom/atom.git
synced 2026-01-23 05:48:10 -05:00
Merge branch 'snippets'
Conflicts: src/app/root-view.coffee
This commit is contained in:
@@ -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", ->
|
||||
|
||||
@@ -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", ->
|
||||
|
||||
188
spec/extensions/snippets-spec.coffee
Normal file
188
spec/extensions/snippets-spec.coffee
Normal 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]]]
|
||||
22
spec/stdlib/underscore-extensions-spec.coffee
Normal file
22
spec/stdlib/underscore-extensions-spec.coffee
Normal 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 []
|
||||
24
src/app/anchor-range.coffee
Normal file
24
src/app/anchor-range.coffee
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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: ->
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -150,6 +150,9 @@ class Cursor
|
||||
getCurrentLineBufferRange: ->
|
||||
@editSession.bufferRangeForBufferRow(@getBufferRow())
|
||||
|
||||
getCurrentWordPrefix: ->
|
||||
@editSession.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])
|
||||
|
||||
isAtBeginningOfLine: ->
|
||||
@getBufferPosition().column == 0
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
6
src/app/keymaps/snippets.coffee
Normal file
6
src/app/keymaps/snippets.coffee
Normal 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'
|
||||
@@ -90,6 +90,7 @@ class Project
|
||||
softWrap: @getSoftWrap()
|
||||
|
||||
@editSessions.push editSession
|
||||
@trigger 'new-edit-session', editSession
|
||||
editSession
|
||||
|
||||
buildBuffer: (filePath) ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
1
src/extensions/snippets/index.coffee
Normal file
1
src/extensions/snippets/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require 'extensions/snippets/snippets.coffee'
|
||||
36
src/extensions/snippets/snippet.coffee
Normal file
36
src/extensions/snippets/snippet.coffee
Normal 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')
|
||||
101
src/extensions/snippets/snippets.coffee
Normal file
101
src/extensions/snippets/snippets.coffee
Normal 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
|
||||
39
src/extensions/snippets/snippets.pegjs
Normal file
39
src/extensions/snippets/snippets.pegjs
Normal 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]*
|
||||
@@ -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
6915
vendor/pegjs.js
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user