Use SpacePen for all views

SpacePen is better because its objects inherit directly from the jQuery
prototype, meaning you can create them with `new`.
This commit is contained in:
Nathan Sobo
2012-02-06 12:12:45 -07:00
parent f1a5368eb3
commit 64a97b9427
16 changed files with 657 additions and 729 deletions

View File

@@ -9,7 +9,7 @@ describe "Cursor", ->
beforeEach ->
buffer = new Buffer(require.resolve('fixtures/sample.js'))
editor = Editor.build()
editor = new Editor
editor.enableKeymap()
editor.setBuffer(buffer)
cursor = editor.cursor

View File

@@ -11,7 +11,7 @@ describe "Editor", ->
beforeEach ->
buffer = new Buffer(require.resolve('fixtures/sample.js'))
editor = Editor.build()
editor = new Editor
editor.enableKeymap()
editor.setBuffer(buffer)

View File

@@ -6,7 +6,7 @@ describe 'FileFinder', ->
beforeEach ->
urls = ['app.coffee', 'buffer.coffee', 'atom/app.coffee', 'atom/buffer.coffee']
finder = FileFinder.build {urls}
finder = new FileFinder({urls})
describe "initialize", ->
it "populates the ol with all urls and selects the first element", ->
@@ -63,7 +63,7 @@ describe 'FileFinder', ->
selectedCallback = jasmine.createSpy 'selected'
beforeEach ->
finder = FileFinder.build {urls, selected: selectedCallback}
finder = new FileFinder({urls, selected: selectedCallback})
it "when a file is selected Editor.open is called", ->
spyOn(finder, 'remove')

View File

@@ -9,7 +9,7 @@ describe "RootView", ->
beforeEach ->
url = require.resolve 'fixtures/dir/a'
rootView = RootView.build {url}
rootView = new RootView({url})
rootView.enableKeymap()
project = rootView.project
@@ -22,14 +22,14 @@ describe "RootView", ->
describe "when called with a url that references a directory", ->
it "creates a project for the directory and opens an empty buffer", ->
url = require.resolve 'fixtures/dir/'
rootView = RootView.build {url}
rootView = new RootView({url})
expect(rootView.project.url).toBe url
expect(rootView.editor.buffer.url).toBeUndefined()
describe "when not called with a url", ->
it "opens an empty buffer", ->
rootView = RootView.build()
rootView = new RootView
expect(rootView.editor.buffer.url).toBeUndefined()
describe ".addPane(view)", ->
@@ -70,7 +70,7 @@ describe "RootView", ->
describe "when there is no project", ->
beforeEach ->
rootView = RootView.build()
rootView = new RootView
it "does not open the FileFinder", ->
expect(rootView.editor.buffer.url).toBeUndefined()

View File

@@ -7,7 +7,7 @@ describe "Selection", ->
beforeEach ->
buffer = new Buffer(require.resolve('fixtures/sample.js'))
editor = Editor.build()
editor = new Editor
editor.enableKeymap()
editor.setBuffer(buffer)
selection = editor.selection

View File

@@ -5,7 +5,7 @@ describe "VimMode", ->
editor = null
beforeEach ->
editor = Editor.build()
editor = new Editor
editor.enableKeymap()
vimMode = new VimMode(editor)

View File

@@ -1,126 +0,0 @@
$ = require 'jquery'
Template = require 'template'
describe "Template", ->
describe "toView", ->
view = null
beforeEach ->
subviewTemplate = class extends Template
content: (params) ->
@div =>
@h2 { outlet: "header" }, params.title
@div "I am a subview"
template = class extends Template
content: (attrs) ->
@div keydown: 'viewClicked', class: 'rootDiv', =>
@h1 { outlet: 'header' }, attrs.title
@list()
@subview 'subview', subviewTemplate.build(title: "Subview")
list: ->
@ol =>
@li outlet: 'li1', click: 'li1Clicked', class: 'foo', "one"
@li outlet: 'li2', keypress:'li2Keypressed', class: 'bar', "two"
viewProperties:
initialize: (attrs) ->
@initializeCalledWith = attrs
foo: "bar",
li1Clicked: ->,
li2Keypressed: ->
viewClicked: ->
view = template.build(title: "Zebra")
describe ".build(attributes)", ->
it "generates markup based on the content method", ->
expect(view).toMatchSelector "div"
expect(view.find("h1:contains(Zebra)")).toExist()
expect(view.find("ol > li.foo:contains(one)")).toExist()
expect(view.find("ol > li.bar:contains(two)")).toExist()
it "extends the view with viewProperties, calling the 'constructor' property if present", ->
expect(view.constructor).toBeDefined()
expect(view.foo).toBe("bar")
expect(view.initializeCalledWith).toEqual title: "Zebra"
it "wires references for elements with 'outlet' attributes", ->
expect(view.li1).toMatchSelector "li.foo:contains(one)"
expect(view.li2).toMatchSelector "li.bar:contains(two)"
it "constructs and wires outlets for subviews", ->
expect(view.subview).toExist()
expect(view.subview.find('h2:contains(Subview)')).toExist()
expect(view.subview.parentView).toBe view
it "does not overwrite outlets on the superview with outlets from the subviews", ->
expect(view.header).toMatchSelector "h1"
expect(view.subview.header).toMatchSelector "h2"
it "binds events for elements with event name attributes", ->
spyOn(view, 'viewClicked').andCallFake (event, elt) ->
expect(event.type).toBe 'keydown'
expect(elt).toMatchSelector "div.rootDiv"
spyOn(view, 'li1Clicked').andCallFake (event, elt) ->
expect(event.type).toBe 'click'
expect(elt).toMatchSelector 'li.foo:contains(one)'
spyOn(view, 'li2Keypressed').andCallFake (event, elt) ->
expect(event.type).toBe 'keypress'
expect(elt).toMatchSelector "li.bar:contains(two)"
view.keydown()
expect(view.viewClicked).toHaveBeenCalled()
view.li1.click()
expect(view.li1Clicked).toHaveBeenCalled()
expect(view.li2Keypressed).not.toHaveBeenCalled()
view.li1Clicked.reset()
view.li2.keypress()
expect(view.li2Keypressed).toHaveBeenCalled()
expect(view.li1Clicked).not.toHaveBeenCalled()
it "makes the original jquery wrapper accessible via the view method from any child element", ->
expect(view.view()).toBe view
expect(view.header.view()).toBe view
expect(view.subview.view()).toBe view.subview
expect(view.subview.header.view()).toBe view.subview
describe "when a view is inserted within another element with jquery", ->
[attachHandler, subviewAttachHandler] = []
beforeEach ->
attachHandler = jasmine.createSpy 'attachHandler'
subviewAttachHandler = jasmine.createSpy 'subviewAttachHandler'
view.on 'attach', attachHandler
view.subview.on 'attach', subviewAttachHandler
describe "when attached to an element that is on the DOM", ->
it "triggers an 'attach' event on the view and its subviews", ->
content = $('#jasmine-content')
content.append view
expect(attachHandler).toHaveBeenCalled()
expect(subviewAttachHandler).toHaveBeenCalled()
view.detach()
content.empty()
attachHandler.reset()
subviewAttachHandler.reset()
otherElt = $('<div>')
content.append(otherElt)
view.insertBefore(otherElt)
expect(attachHandler).toHaveBeenCalled()
expect(subviewAttachHandler).toHaveBeenCalled()
describe "when attached to an element that is not on the DOM", ->
it "does not trigger an attach event", ->
fragment = $('<div>')
fragment.append view
expect(attachHandler).not.toHaveBeenCalled()

View File

@@ -1,5 +1,4 @@
Builder = require 'template/builder'
Template = require 'template'
describe "Builder", ->
builder = null
@@ -74,28 +73,3 @@ describe "Builder", ->
builder.raw '&nbsp;'
expect(builder.toHtml()).toBe '&nbsp;'
describe ".subview(name, template, attrs)", ->
template = null
beforeEach ->
template = class extends Template
content: (params) ->
@div =>
@h2 params.title
@div "I am a subview"
viewProperties:
foo: "bar"
it "inserts a view built from the given template with the given params", ->
builder.tag 'div', ->
builder.tag 'h1', "Superview"
builder.subview 'sub', template.build(title: "Subview")
fragment = builder.toFragment()
expect(fragment.find("h1:contains(Superview)")).toExist()
expect(fragment.find("h2:contains(Subview)")).toExist()
subview = fragment.sub
expect(subview).toMatchSelector ':has(h2):contains(I am a subview)'
expect(subview.foo).toBe 'bar'

View File

@@ -1,120 +1,119 @@
Template = require 'template'
{View} = require 'space-pen'
Point = require 'point'
_ = require 'underscore'
module.exports =
class Cursor extends Template
content: ->
class Cursor extends View
@content: ->
@pre class: 'cursor idle', style: 'position: absolute;', => @raw '&nbsp;'
viewProperties:
editor: null
editor: null
initialize: (@editor) ->
@one 'attach', => @updateAppearance()
initialize: (@editor) ->
@one 'attach', => @updateAppearance()
bufferChanged: (e) ->
@setPosition(e.postRange.end)
bufferChanged: (e) ->
@setPosition(e.postRange.end)
setPosition: (point) ->
point = Point.fromObject(point)
@point = @editor.clipPosition(point)
@goalColumn = null
@updateAppearance()
@trigger 'cursor:position-changed'
setPosition: (point) ->
point = Point.fromObject(point)
@point = @editor.clipPosition(point)
@goalColumn = null
@updateAppearance()
@trigger 'cursor:position-changed'
@removeClass 'idle'
window.clearTimeout(@idleTimeout) if @idleTimeout
@idleTimeout = window.setTimeout (=> @addClass 'idle'), 200
@removeClass 'idle'
window.clearTimeout(@idleTimeout) if @idleTimeout
@idleTimeout = window.setTimeout (=> @addClass 'idle'), 200
getPosition: -> _.clone(@point)
getPosition: -> _.clone(@point)
setColumn: (column) ->
{ row } = @getPosition()
@setPosition {row, column}
setColumn: (column) ->
{ row } = @getPosition()
@setPosition {row, column}
getColumn: ->
@getPosition().column
getColumn: ->
@getPosition().column
getRow: ->
@getPosition().row
getRow: ->
@getPosition().row
moveUp: ->
{ row, column } = @getPosition()
column = @goalColumn if @goalColumn?
if row > 0
@setPosition({row: row - 1, column: column})
else
@moveToLineStart()
moveUp: ->
{ row, column } = @getPosition()
column = @goalColumn if @goalColumn?
if row > 0
@setPosition({row: row - 1, column: column})
else
@moveToLineStart()
@goalColumn = column
@goalColumn = column
moveDown: ->
{ row, column } = @getPosition()
column = @goalColumn if @goalColumn?
if row < @editor.buffer.numLines() - 1
@setPosition({row: row + 1, column: column})
else
@moveToLineEnd()
moveDown: ->
{ row, column } = @getPosition()
column = @goalColumn if @goalColumn?
if row < @editor.buffer.numLines() - 1
@setPosition({row: row + 1, column: column})
else
@moveToLineEnd()
@goalColumn = column
@goalColumn = column
moveToLineEnd: ->
{ row } = @getPosition()
@setPosition({ row, column: @editor.buffer.getLine(row).length })
moveToLineEnd: ->
{ row } = @getPosition()
@setPosition({ row, column: @editor.buffer.getLine(row).length })
moveToLineStart: ->
{ row } = @getPosition()
@setPosition({ row, column: 0 })
moveToLineStart: ->
{ row } = @getPosition()
@setPosition({ row, column: 0 })
moveRight: ->
{ row, column } = @getPosition()
if column < @editor.buffer.getLine(row).length
column++
else if row < @editor.buffer.numLines() - 1
row++
column = 0
@setPosition({row, column})
moveRight: ->
{ row, column } = @getPosition()
if column < @editor.buffer.getLine(row).length
column++
else if row < @editor.buffer.numLines() - 1
row++
column = 0
@setPosition({row, column})
moveLeft: ->
{ row, column } = @getPosition()
if column > 0
column--
else if row > 0
row--
column = @editor.buffer.getLine(row).length
moveLeft: ->
{ row, column } = @getPosition()
if column > 0
column--
else if row > 0
row--
column = @editor.buffer.getLine(row).length
@setPosition({row, column})
@setPosition({row, column})
updateAppearance: ->
position = @editor.pixelPositionFromPoint(@point)
@css(position)
@autoScrollVertically(position)
@autoScrollHorizontally(position)
updateAppearance: ->
position = @editor.pixelPositionFromPoint(@point)
@css(position)
@autoScrollVertically(position)
@autoScrollHorizontally(position)
autoScrollVertically: (position) ->
linesInView = @editor.height() / @height()
maxScrollMargin = Math.floor((linesInView - 1) / 2)
scrollMargin = Math.min(@editor.vScrollMargin, maxScrollMargin)
margin = scrollMargin * @height()
desiredTop = position.top - margin
desiredBottom = position.top + @height() + margin
autoScrollVertically: (position) ->
linesInView = @editor.height() / @height()
maxScrollMargin = Math.floor((linesInView - 1) / 2)
scrollMargin = Math.min(@editor.vScrollMargin, maxScrollMargin)
margin = scrollMargin * @height()
desiredTop = position.top - margin
desiredBottom = position.top + @height() + margin
if desiredBottom > @editor.scrollBottom()
@editor.scrollBottom(desiredBottom)
else if desiredTop < @editor.scrollTop()
@editor.scrollTop(desiredTop)
if desiredBottom > @editor.scrollBottom()
@editor.scrollBottom(desiredBottom)
else if desiredTop < @editor.scrollTop()
@editor.scrollTop(desiredTop)
autoScrollHorizontally: (position) ->
charsInView = @editor.width() / @width()
maxScrollMargin = Math.floor((charsInView - 1) / 2)
scrollMargin = Math.min(@editor.hScrollMargin, maxScrollMargin)
margin = scrollMargin * @width()
desiredRight = position.left + @width() + margin
desiredLeft = position.left - margin
autoScrollHorizontally: (position) ->
charsInView = @editor.width() / @width()
maxScrollMargin = Math.floor((charsInView - 1) / 2)
scrollMargin = Math.min(@editor.hScrollMargin, maxScrollMargin)
margin = scrollMargin * @width()
desiredRight = position.left + @width() + margin
desiredLeft = position.left - margin
if desiredRight > @editor.scrollRight()
@editor.scrollRight(desiredRight)
else if desiredLeft < @editor.scrollLeft()
@editor.scrollLeft(desiredLeft)
if desiredRight > @editor.scrollRight()
@editor.scrollRight(desiredRight)
else if desiredLeft < @editor.scrollLeft()
@editor.scrollLeft(desiredLeft)

View File

@@ -1,4 +1,4 @@
Template = require 'template'
{View} = require 'space-pen'
Buffer = require 'buffer'
Point = require 'point'
Cursor = require 'cursor'
@@ -11,224 +11,223 @@ $$ = require 'template/builder'
_ = require 'underscore'
module.exports =
class Editor extends Template
content: ->
class Editor extends View
@content: ->
@div class: 'editor', tabindex: -1, =>
@div outlet: 'lines'
@input class: 'hidden-input', outlet: 'hiddenInput'
viewProperties:
vScrollMargin: 2
hScrollMargin: 10
cursor: null
buffer: null
selection: null
lineHeight: null
charWidth: null
vScrollMargin: 2
hScrollMargin: 10
cursor: null
buffer: null
selection: null
lineHeight: null
charWidth: null
initialize: () ->
requireStylesheet 'editor.css'
requireStylesheet 'theme/twilight.css'
@bindKeys()
@buildCursorAndSelection()
@handleEvents()
@setBuffer(new Buffer)
initialize: () ->
requireStylesheet 'editor.css'
requireStylesheet 'theme/twilight.css'
@bindKeys()
@buildCursorAndSelection()
@handleEvents()
@setBuffer(new Buffer)
bindKeys: ->
atom.bindKeys '*',
right: 'move-right'
left: 'move-left'
down: 'move-down'
up: 'move-up'
'shift-right': 'select-right'
'shift-left': 'select-left'
'shift-up': 'select-up'
'shift-down': 'select-down'
enter: 'newline'
backspace: 'delete-left'
delete: 'delete-right'
'meta-x': 'cut'
'meta-c': 'copy'
'meta-v': 'paste'
bindKeys: ->
atom.bindKeys '*',
right: 'move-right'
left: 'move-left'
down: 'move-down'
up: 'move-up'
'shift-right': 'select-right'
'shift-left': 'select-left'
'shift-up': 'select-up'
'shift-down': 'select-down'
enter: 'newline'
backspace: 'delete-left'
delete: 'delete-right'
'meta-x': 'cut'
'meta-c': 'copy'
'meta-v': 'paste'
@on 'move-right', => @moveCursorRight()
@on 'move-left', => @moveCursorLeft()
@on 'move-down', => @moveCursorDown()
@on 'move-up', => @moveCursorUp()
@on 'select-right', => @selectRight()
@on 'select-left', => @selectLeft()
@on 'select-up', => @selectUp()
@on 'select-down', => @selectDown()
@on 'newline', => @insertNewline()
@on 'delete-left', => @deleteLeft()
@on 'delete-right', => @deleteRight()
@on 'cut', => @cutSelection()
@on 'copy', => @copySelection()
@on 'paste', => @paste()
@on 'move-right', => @moveCursorRight()
@on 'move-left', => @moveCursorLeft()
@on 'move-down', => @moveCursorDown()
@on 'move-up', => @moveCursorUp()
@on 'select-right', => @selectRight()
@on 'select-left', => @selectLeft()
@on 'select-up', => @selectUp()
@on 'select-down', => @selectDown()
@on 'newline', => @insertNewline()
@on 'delete-left', => @deleteLeft()
@on 'delete-right', => @deleteRight()
@on 'cut', => @cutSelection()
@on 'copy', => @copySelection()
@on 'paste', => @paste()
buildCursorAndSelection: ->
@cursor = Cursor.build(this)
@append(@cursor)
buildCursorAndSelection: ->
@cursor = new Cursor(this)
@append(@cursor)
@selection = Selection.build(this)
@append(@selection)
@selection = new Selection(this)
@append(@selection)
handleEvents: ->
@on 'focus', =>
@hiddenInput.focus()
false
handleEvents: ->
@on 'focus', =>
@hiddenInput.focus()
false
@on 'mousedown', (e) =>
clickCount = e.originalEvent.detail
@on 'mousedown', (e) =>
clickCount = e.originalEvent.detail
if clickCount == 1
@setCursorPosition @pointFromMouseEvent(e)
@selectTextOnMouseMovement()
else if clickCount == 2
@selection.selectWord()
@selectTextOnMouseMovement()
if clickCount == 1
@setCursorPosition @pointFromMouseEvent(e)
@selectTextOnMouseMovement()
else if clickCount == 2
@selection.selectWord()
@selectTextOnMouseMovement()
@hiddenInput.on "textInput", (e) =>
@insertText(e.originalEvent.data)
@hiddenInput.on "textInput", (e) =>
@insertText(e.originalEvent.data)
@on 'cursor:position-changed', =>
@hiddenInput.css(@pixelPositionFromPoint(@cursor.getPosition()))
@on 'cursor:position-changed', =>
@hiddenInput.css(@pixelPositionFromPoint(@cursor.getPosition()))
@one 'attach', =>
@calculateDimensions()
@hiddenInput.width(@charWidth)
@focus()
@one 'attach', =>
@calculateDimensions()
@hiddenInput.width(@charWidth)
@focus()
selectTextOnMouseMovement: ->
moveHandler = (e) => @selectToPosition(@pointFromMouseEvent(e))
@on 'mousemove', moveHandler
$(document).one 'mouseup', => @off 'mousemove', moveHandler
selectTextOnMouseMovement: ->
moveHandler = (e) => @selectToPosition(@pointFromMouseEvent(e))
@on 'mousemove', moveHandler
$(document).one 'mouseup', => @off 'mousemove', moveHandler
buildLineElement: (row) ->
tokens = @highlighter.tokensForRow(row)
$$.pre class: 'line', ->
if tokens.length
for token in tokens
classes = token.type.split('.').map((c) -> "ace_#{c}").join(' ')
@span { class: token.type.replace('.', ' ') }, token.value
else
@raw '&nbsp;'
setBuffer: (@buffer) ->
@highlighter = new Highlighter(@buffer)
@lines.empty()
for row in [0..@buffer.lastRow()]
line = @buildLineElement(row)
@lines.append line
@setCursorPosition(row: 0, column: 0)
@buffer.on 'change', (e) =>
@cursor.bufferChanged(e)
@highlighter.on 'change', (e) =>
{ preRange, postRange } = e
if postRange.end.row > preRange.end.row
# update, then insert elements
for row in [preRange.start.row..postRange.end.row]
if row <= preRange.end.row
@updateLineElement(row)
else
@insertLineElement(row)
else
# traverse in reverse... remove, then update elements
for row in [preRange.end.row..preRange.start.row]
if row > postRange.end.row
@removeLineElement(row)
else
@updateLineElement(row)
updateLineElement: (row) ->
@getLineElement(row).replaceWith(@buildLineElement(row))
insertLineElement: (row) ->
@getLineElement(row).before(@buildLineElement(row))
removeLineElement: (row) ->
@getLineElement(row).remove()
getLineElement: (row) ->
@lines.find("pre.line:eq(#{row})")
clipPosition: ({row, column}) ->
row = Math.min(Math.max(0, row), @buffer.numLines() - 1)
column = Math.min(Math.max(0, column), @buffer.getLine(row).length)
new Point(row, column)
pixelPositionFromPoint: ({row, column}) ->
{ top: row * @lineHeight, left: column * @charWidth }
pointFromPixelPosition: ({top, left}) ->
{ row: Math.floor(top / @lineHeight), column: Math.floor(left / @charWidth) }
pointFromMouseEvent: (e) ->
{ pageX, pageY } = e
@pointFromPixelPosition
top: pageY - @lines.offset().top
left: pageX - @lines.offset().left
calculateDimensions: ->
fragment = $('<pre style="position: absolute; visibility: hidden;">x</pre>')
@lines.append(fragment)
@charWidth = fragment.width()
@lineHeight = fragment.outerHeight()
fragment.remove()
scrollBottom: (newValue) ->
if newValue?
@scrollTop(newValue - @height())
buildLineElement: (row) ->
tokens = @highlighter.tokensForRow(row)
$$.pre class: 'line', ->
if tokens.length
for token in tokens
classes = token.type.split('.').map((c) -> "ace_#{c}").join(' ')
@span { class: token.type.replace('.', ' ') }, token.value
else
@scrollTop() + @height()
@raw '&nbsp;'
scrollRight: (newValue) ->
if newValue?
@scrollLeft(newValue - @width())
setBuffer: (@buffer) ->
@highlighter = new Highlighter(@buffer)
@lines.empty()
for row in [0..@buffer.lastRow()]
line = @buildLineElement(row)
@lines.append line
@setCursorPosition(row: 0, column: 0)
@buffer.on 'change', (e) =>
@cursor.bufferChanged(e)
@highlighter.on 'change', (e) =>
{ preRange, postRange } = e
if postRange.end.row > preRange.end.row
# update, then insert elements
for row in [preRange.start.row..postRange.end.row]
if row <= preRange.end.row
@updateLineElement(row)
else
@insertLineElement(row)
else
@scrollLeft() + @width()
# traverse in reverse... remove, then update elements
for row in [preRange.end.row..preRange.start.row]
if row > postRange.end.row
@removeLineElement(row)
else
@updateLineElement(row)
getCursor: -> @cursor
getSelection: -> @selection
updateLineElement: (row) ->
@getLineElement(row).replaceWith(@buildLineElement(row))
getCurrentLine: -> @buffer.getLine(@getCursorRow())
getSelectedText: -> @selection.getText()
moveCursorUp: -> @cursor.moveUp()
moveCursorDown: -> @cursor.moveDown()
moveCursorRight: -> @cursor.moveRight()
moveCursorLeft: -> @cursor.moveLeft()
setCursorPosition: (point) -> @cursor.setPosition(point)
getCursorPosition: -> @cursor.getPosition()
setCursorRow: (row) -> @cursor.setRow(row)
getCursorRow: -> @cursor.getRow()
setCursorColumn: (column) -> @cursor.setColumn(column)
getCursorColumn: -> @cursor.getColumn()
insertLineElement: (row) ->
@getLineElement(row).before(@buildLineElement(row))
selectRight: -> @selection.selectRight()
selectLeft: -> @selection.selectLeft()
selectUp: -> @selection.selectUp()
selectDown: -> @selection.selectDown()
selectToPosition: (position) ->
@selection.selectToPosition(position)
removeLineElement: (row) ->
@getLineElement(row).remove()
insertText: (text) -> @selection.insertText(text)
insertNewline: -> @selection.insertNewline()
getLineElement: (row) ->
@lines.find("pre.line:eq(#{row})")
cutSelection: -> @selection.cut()
copySelection: -> @selection.copy()
paste: -> @selection.insertText(atom.native.readFromPasteboard())
clipPosition: ({row, column}) ->
row = Math.min(Math.max(0, row), @buffer.numLines() - 1)
column = Math.min(Math.max(0, column), @buffer.getLine(row).length)
new Point(row, column)
deleteLeft: ->
@selectLeft() if @selection.isEmpty()
@selection.delete()
pixelPositionFromPoint: ({row, column}) ->
{ top: row * @lineHeight, left: column * @charWidth }
deleteRight: ->
@selectRight() if @selection.isEmpty()
@selection.delete()
pointFromPixelPosition: ({top, left}) ->
{ row: Math.floor(top / @lineHeight), column: Math.floor(left / @charWidth) }
pointFromMouseEvent: (e) ->
{ pageX, pageY } = e
@pointFromPixelPosition
top: pageY - @lines.offset().top
left: pageX - @lines.offset().left
calculateDimensions: ->
fragment = $('<pre style="position: absolute; visibility: hidden;">x</pre>')
@lines.append(fragment)
@charWidth = fragment.width()
@lineHeight = fragment.outerHeight()
fragment.remove()
scrollBottom: (newValue) ->
if newValue?
@scrollTop(newValue - @height())
else
@scrollTop() + @height()
scrollRight: (newValue) ->
if newValue?
@scrollLeft(newValue - @width())
else
@scrollLeft() + @width()
getCursor: -> @cursor
getSelection: -> @selection
getCurrentLine: -> @buffer.getLine(@getCursorRow())
getSelectedText: -> @selection.getText()
moveCursorUp: -> @cursor.moveUp()
moveCursorDown: -> @cursor.moveDown()
moveCursorRight: -> @cursor.moveRight()
moveCursorLeft: -> @cursor.moveLeft()
setCursorPosition: (point) -> @cursor.setPosition(point)
getCursorPosition: -> @cursor.getPosition()
setCursorRow: (row) -> @cursor.setRow(row)
getCursorRow: -> @cursor.getRow()
setCursorColumn: (column) -> @cursor.setColumn(column)
getCursorColumn: -> @cursor.getColumn()
selectRight: -> @selection.selectRight()
selectLeft: -> @selection.selectLeft()
selectUp: -> @selection.selectUp()
selectDown: -> @selection.selectDown()
selectToPosition: (position) ->
@selection.selectToPosition(position)
insertText: (text) -> @selection.insertText(text)
insertNewline: -> @selection.insertNewline()
cutSelection: -> @selection.cut()
copySelection: -> @selection.copy()
paste: -> @selection.insertText(atom.native.readFromPasteboard())
deleteLeft: ->
@selectLeft() if @selection.isEmpty()
@selection.delete()
deleteRight: ->
@selectRight() if @selection.isEmpty()
@selection.delete()

View File

@@ -1,67 +1,66 @@
$ = require 'jquery'
Template = require 'template'
{View} = require 'space-pen'
stringScore = require 'stringscore'
module.exports =
class FileFinder extends Template
content: ->
class FileFinder extends View
@content: ->
@div class: 'file-finder', =>
@link rel: 'stylesheet', href: "#{require.resolve('file-finder.css')}?#{(new Date).getTime()}"
@ol outlet: 'urlList'
@input outlet: 'input', input: 'populateUrlList'
viewProperties:
urls: null
maxResults: null
urls: null
maxResults: null
initialize: ({@urls, @selected}) ->
@maxResults = 10
initialize: ({@urls, @selected}) ->
@maxResults = 10
@populateUrlList()
atom.bindKeys ".file-finder",
'up': 'move-up'
'down': 'move-down'
'enter': 'select'
@populateUrlList()
atom.bindKeys ".file-finder",
'up': 'move-up'
'down': 'move-down'
'enter': 'select'
@on 'move-up', => @moveUp()
@on 'move-down', => @moveDown()
@on 'select', => @select()
@on 'move-up', => @moveUp()
@on 'move-down', => @moveDown()
@on 'select', => @select()
populateUrlList: ->
@urlList.empty()
for url in @findMatches(@input.val())
@urlList.append $("<li>#{url}</li>")
populateUrlList: ->
@urlList.empty()
for url in @findMatches(@input.val())
@urlList.append $("<li>#{url}</li>")
@urlList.children('li:first').addClass 'selected'
@urlList.children('li:first').addClass 'selected'
findSelectedLi: ->
@urlList.children('li.selected')
findSelectedLi: ->
@urlList.children('li.selected')
select: ->
filePath = @findSelectedLi().text()
@selected(filePath) if filePath and @selected
@remove()
select: ->
filePath = @findSelectedLi().text()
@selected(filePath) if filePath and @selected
@remove()
moveUp: ->
@findSelectedLi()
.filter(':not(:first-child)')
.removeClass('selected')
.prev()
.addClass('selected')
moveUp: ->
@findSelectedLi()
.filter(':not(:first-child)')
.removeClass('selected')
.prev()
.addClass('selected')
moveDown: ->
@findSelectedLi()
.filter(':not(:last-child)')
.removeClass('selected')
.next()
.addClass('selected')
moveDown: ->
@findSelectedLi()
.filter(':not(:last-child)')
.removeClass('selected')
.next()
.addClass('selected')
findMatches: (query) ->
if not query
urls = @urls
else
scoredUrls = ({url, score: stringScore(url, query)} for url in @urls)
scoredUrls.sort (a, b) -> a.score > b.score
urls = (urlAndScore.url for urlAndScore in scoredUrls when urlAndScore.score > 0)
findMatches: (query) ->
if not query
urls = @urls
else
scoredUrls = ({url, score: stringScore(url, query)} for url in @urls)
scoredUrls.sort (a, b) -> a.score > b.score
urls = (urlAndScore.url for urlAndScore in scoredUrls when urlAndScore.score > 0)
urls.slice 0, @maxResults
urls.slice 0, @maxResults

View File

@@ -2,7 +2,7 @@ $ = require 'jquery'
fs = require 'fs'
_ = require 'underscore'
Template = require 'template'
{View} = require 'space-pen'
Buffer = require 'buffer'
Editor = require 'editor'
FileFinder = require 'file-finder'
@@ -11,58 +11,57 @@ GlobalKeymap = require 'global-keymap'
VimMode = require 'vim-mode'
module.exports =
class RootView extends Template
content: ->
class RootView extends View
@content: ->
@div id: 'app-horizontal', =>
@div id: 'app-vertical', outlet: 'vertical', =>
@div id: 'main', outlet: 'main', =>
@subview 'editor', Editor.build()
@subview 'editor', new Editor
viewProperties:
globalKeymap: null
globalKeymap: null
initialize: ({url}) ->
@editor.keyEventHandler = atom.globalKeymap
@createProject(url)
initialize: ({url}) ->
@editor.keyEventHandler = atom.globalKeymap
@createProject(url)
atom.bindKeys '*'
'meta-s': 'save'
'meta-w': 'close'
'meta-t': 'toggle-file-finder'
'alt-meta-i': 'show-console'
atom.bindKeys '*'
'meta-s': 'save'
'meta-w': 'close'
'meta-t': 'toggle-file-finder'
'alt-meta-i': 'show-console'
@on 'toggle-file-finder', => @toggleFileFinder()
@on 'show-console', -> window.showConsole()
@on 'toggle-file-finder', => @toggleFileFinder()
@on 'show-console', -> window.showConsole()
@on 'focusout', (e) =>
# if anything but the editor and its input loses focus, restore focus to the editor
unless $(e.target).closest('.editor').length
@editor.focus()
@on 'focusout', (e) =>
# if anything but the editor and its input loses focus, restore focus to the editor
unless $(e.target).closest('.editor').length
@editor.focus()
createProject: (url) ->
if url
@project = new Project(fs.directory(url))
@editor.setBuffer(@project.open(url)) if fs.isFile(url)
createProject: (url) ->
if url
@project = new Project(fs.directory(url))
@editor.setBuffer(@project.open(url)) if fs.isFile(url)
bindKeys: (selector, bindings) ->
@globalKeymap.bindKeys(selector, bindings)
bindKeys: (selector, bindings) ->
@globalKeymap.bindKeys(selector, bindings)
addPane: (view) ->
pane = $('<div class="pane">')
pane.append(view)
@main.after(pane)
addPane: (view) ->
pane = $('<div class="pane">')
pane.append(view)
@main.after(pane)
toggleFileFinder: ->
return unless @project
toggleFileFinder: ->
return unless @project
if @fileFinder and @fileFinder.parent()[0]
@fileFinder.remove()
@fileFinder = null
else
@project.getFilePaths().done (paths) =>
relativePaths = (path.replace(@project.url, "") for path in paths)
@fileFinder = FileFinder.build
urls: relativePaths
selected: (relativePath) => @editor.setBuffer(@project.open(relativePath))
@addPane @fileFinder
@fileFinder.input.focus()
if @fileFinder and @fileFinder.parent()[0]
@fileFinder.remove()
@fileFinder = null
else
@project.getFilePaths().done (paths) =>
relativePaths = (path.replace(@project.url, "") for path in paths)
@fileFinder = new FileFinder
urls: relativePaths
selected: (relativePath) => @editor.setBuffer(@project.open(relativePath))
@addPane @fileFinder
@fileFinder.input.focus()

View File

@@ -1,150 +1,149 @@
Cursor = require 'cursor'
Range = require 'range'
Template = require 'template'
{View} = require 'space-pen'
$$ = require 'template/builder'
module.exports =
class Selection extends Template
content: ->
class Selection extends View
@content: ->
@div()
viewProperties:
anchor: null
modifyingSelection: null
regions: null
anchor: null
modifyingSelection: null
regions: null
initialize: (@editor) ->
@regions = []
@cursor = @editor.cursor
@cursor.on 'cursor:position-changed', =>
if @modifyingSelection
@updateAppearance()
else
@clearSelection()
clearSelection: ->
@anchor = null
@updateAppearance()
updateAppearance: ->
@clearRegions()
range = @getRange()
return if range.isEmpty()
rowSpan = range.end.row - range.start.row
if rowSpan == 0
@appendRegion(1, range.start, range.end)
initialize: (@editor) ->
@regions = []
@cursor = @editor.cursor
@cursor.on 'cursor:position-changed', =>
if @modifyingSelection
@updateAppearance()
else
@appendRegion(1, range.start, null)
if rowSpan > 1
@appendRegion(rowSpan - 1, { row: range.start.row + 1, column: 0}, null)
@appendRegion(1, { row: range.end.row, column: 0 }, range.end)
@clearSelection()
appendRegion: (rows, start, end) ->
{ lineHeight, charWidth } = @editor
css = {}
css.top = start.row * lineHeight
css.left = start.column * charWidth
css.height = lineHeight * rows
if end
css.width = end.column * charWidth - css.left
else
css.right = 0
clearSelection: ->
@anchor = null
@updateAppearance()
region = $$.div(class: 'selection').css(css)
@append(region)
@regions.push(region)
updateAppearance: ->
@clearRegions()
clearRegions: ->
region.remove() for region in @regions
@regions = []
range = @getRange()
return if range.isEmpty()
getRange: ->
if @anchor
new Range(@anchor.getPosition(), @cursor.getPosition())
else
new Range(@cursor.getPosition(), @cursor.getPosition())
rowSpan = range.end.row - range.start.row
setRange: (range) ->
@cursor.setPosition(range.start)
@modifySelection =>
@cursor.setPosition(range.end)
if rowSpan == 0
@appendRegion(1, range.start, range.end)
else
@appendRegion(1, range.start, null)
if rowSpan > 1
@appendRegion(rowSpan - 1, { row: range.start.row + 1, column: 0}, null)
@appendRegion(1, { row: range.end.row, column: 0 }, range.end)
getText: ->
@editor.buffer.getTextInRange @getRange()
appendRegion: (rows, start, end) ->
{ lineHeight, charWidth } = @editor
css = {}
css.top = start.row * lineHeight
css.left = start.column * charWidth
css.height = lineHeight * rows
if end
css.width = end.column * charWidth - css.left
else
css.right = 0
insertText: (text) ->
@editor.buffer.change(@getRange(), text)
region = $$.div(class: 'selection').css(css)
@append(region)
@regions.push(region)
insertNewline: ->
@insertText('\n')
clearRegions: ->
region.remove() for region in @regions
@regions = []
delete: ->
range = @getRange()
@editor.buffer.change(range, '') unless range.isEmpty()
getRange: ->
if @anchor
new Range(@anchor.getPosition(), @cursor.getPosition())
else
new Range(@cursor.getPosition(), @cursor.getPosition())
isEmpty: ->
@getRange().isEmpty()
setRange: (range) ->
@cursor.setPosition(range.start)
@modifySelection =>
@cursor.setPosition(range.end)
modifySelection: (fn) ->
@placeAnchor()
@modifyingSelection = true
fn()
@modifyingSelection = false
getText: ->
@editor.buffer.getTextInRange @getRange()
placeAnchor: ->
return if @anchor
cursorPosition = @cursor.getPosition()
@anchor = { getPosition: -> cursorPosition }
insertText: (text) ->
@editor.buffer.change(@getRange(), text)
selectWord: ->
row = @cursor.getRow()
column = @cursor.getColumn()
insertNewline: ->
@insertText('\n')
line = @editor.buffer.getLine(row)
leftSide = line[0...column].split('').reverse().join('') # reverse left side
rightSide = line[column..]
delete: ->
range = @getRange()
@editor.buffer.change(range, '') unless range.isEmpty()
regex = /^\w*/
startOffset = -regex.exec(leftSide)?[0]?.length or 0
endOffset = regex.exec(rightSide)?[0]?.length or 0
isEmpty: ->
@getRange().isEmpty()
range = new Range([row, column + startOffset], [row, column + endOffset])
@setRange range
modifySelection: (fn) ->
@placeAnchor()
@modifyingSelection = true
fn()
@modifyingSelection = false
selectRight: ->
@modifySelection =>
@cursor.moveRight()
placeAnchor: ->
return if @anchor
cursorPosition = @cursor.getPosition()
@anchor = { getPosition: -> cursorPosition }
selectLeft: ->
@modifySelection =>
@cursor.moveLeft()
selectWord: ->
row = @cursor.getRow()
column = @cursor.getColumn()
selectUp: ->
@modifySelection =>
@cursor.moveUp()
line = @editor.buffer.getLine(row)
leftSide = line[0...column].split('').reverse().join('') # reverse left side
rightSide = line[column..]
selectDown: ->
@modifySelection =>
@cursor.moveDown()
regex = /^\w*/
startOffset = -regex.exec(leftSide)?[0]?.length or 0
endOffset = regex.exec(rightSide)?[0]?.length or 0
selectToPosition: (position) ->
@modifySelection =>
@cursor.setPosition(position)
range = new Range([row, column + startOffset], [row, column + endOffset])
@setRange range
moveCursorToLineEnd: ->
@cursor.moveToLineEnd()
selectRight: ->
@modifySelection =>
@cursor.moveRight()
moveCursorToLineStart: ->
@cursor.moveToLineStart()
selectLeft: ->
@modifySelection =>
@cursor.moveLeft()
cut: ->
@copy()
@delete()
selectUp: ->
@modifySelection =>
@cursor.moveUp()
copy: ->
return if @isEmpty()
text = @editor.buffer.getTextInRange @getRange()
atom.native.writeToPasteboard text
selectDown: ->
@modifySelection =>
@cursor.moveDown()
selectToPosition: (position) ->
@modifySelection =>
@cursor.setPosition(position)
moveCursorToLineEnd: ->
@cursor.moveToLineEnd()
moveCursorToLineStart: ->
@cursor.moveToLineStart()
cut: ->
@copy()
@delete()
copy: ->
return if @isEmpty()
text = @editor.buffer.getTextInRange @getRange()
atom.native.writeToPasteboard text

View File

@@ -14,7 +14,7 @@ windowAdditions =
startup: ->
@menuItemActions = {}
@rootView = RootView.build(url: $atomController.url?.toString())
@rootView = new RootView(url: $atomController.url?.toString())
$('body').append @rootView
@registerEventHandlers()
@bindMenuItems()

View File

@@ -1,76 +0,0 @@
$ = require 'jquery'
_ = require 'underscore'
Builder = require 'template/builder'
module.exports =
class Template
@events: 'blur change click dblclick error focus input keydown
keypress keyup load mousedown mousemove mouseout mouseover
mouseup resize scroll select submit unload'.split /\s+/
@buildTagMethod: (name) ->
this.prototype[name] = (args...) -> @builder.tag(name, args...)
@buildTagMethod(name) for name in Builder.elements.normal
@buildTagMethod(name) for name in Builder.elements.void
@build: (attributes) ->
(new this).build(attributes)
@toHtml: (attributes) ->
(new this).toHtml(attributes)
build: (attributes={}) ->
@builder = new Builder
@content(attributes)
view = @builder.toFragment()
@bindEvents(view)
if @viewProperties
$.extend(view, @viewProperties)
view.attr('triggerAttachEvents', true)
view.initialize?(attributes)
view
toHtml: (attributes) ->
@builder = new Builder
@content(attributes)
@builder.toHtml()
subview: (args...) ->
@builder.subview.apply(@builder, args)
raw: (text) ->
@builder.raw(text)
bindEvents: (view) ->
for eventName in this.constructor.events
selector = "[#{eventName}]"
elements = view.find(selector).add(view.filter(selector))
elements.each ->
elt = $(this)
methodName = elt.attr(eventName)
elt.on eventName, (event) -> view[methodName](event, elt)
$.fn.view = ->
this.data('view')
# Trigger attach event when views are added to the DOM
triggerAttachEvent = (elt) ->
if elt.attr?('triggerAttachEvents') and elt.parents('html').length
elt.find('[triggerAttachEvents]').add(elt).trigger('attach')
_.each ['append', 'prepend', 'after', 'before'], (methodName) ->
originalMethod = $.fn[methodName]
$.fn[methodName] = (args...) ->
result = originalMethod.apply(this, args)
triggerAttachEvent(args[0])
result
_.each ['prependTo', 'appendTo', 'insertAfter', 'insertBefore'], (methodName) ->
originalMethod = $.fn[methodName]
$.fn[methodName] = (args...) ->
result = originalMethod.apply(this, args)
triggerAttachEvent(this)
result

161
vendor/space-pen.coffee vendored Normal file
View File

@@ -0,0 +1,161 @@
# Modified from 26fca5374e546fd8cc2f12d1140f915185611bdc
# Add require 'jquery'
$ = jQuery = require('jquery')
elements =
'a abbr address article aside audio b bdi bdo blockquote body button
canvas caption cite code colgroup datalist dd del details dfn div dl dt em
fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup
html i iframe ins kbd label legend li map mark menu meter nav noscript object
ol optgroup option output p pre progress q rp rt ruby s samp script section
select small span strong style sub summary sup table tbody td textarea tfoot
th thead time title tr u ul video area base br col command embed hr img input
keygen link meta param source track wbrk'.split /\s+/
voidElements =
'area base br col command embed hr img input keygen link meta param
source track wbr'.split /\s+/
events =
'blur change click dblclick error focus input keydown
keypress keyup load mousedown mousemove mouseout mouseover
mouseup resize scroll select submit unload'.split /\s+/
idCounter = 0
class View extends jQuery
elements.forEach (tagName) ->
View[tagName] = (args...) -> @builder.tag(tagName, args...)
@subview: (name, view) -> @builder.subview(name, view)
@text: (string) -> @builder.text(string)
@raw: (string) -> @builder.raw(string)
constructor: (params={}) ->
postProcessingSteps = @buildHtml(params)
@constructor = jQuery # sadly, jQuery assumes this.constructor == jQuery in pushStack
@wireOutlets(this)
@bindEventHandlers(this)
@find('*').andSelf().data('view', this)
@attr('triggerAttachEvents', true)
step(this) for step in postProcessingSteps
@initialize?(params)
buildHtml: (params) ->
@constructor.builder = new Builder
@constructor.content(params)
[html, postProcessingSteps] = @constructor.builder.buildHtml()
@constructor.builder = null
jQuery.fn.init.call(this, html)
postProcessingSteps
wireOutlets: (view) ->
@find('[outlet]').each ->
element = $(this)
view[element.attr('outlet')] = element
bindEventHandlers: (view) ->
for eventName in events
selector = "[#{eventName}]"
elements = view.find(selector).add(view.filter(selector))
elements.each ->
element = $(this)
methodName = element.attr(eventName)
element.on eventName, (event) -> view[methodName](event, element)
class Builder
constructor: ->
@document = []
@postProcessingSteps = []
buildHtml: ->
[@document.join(''), @postProcessingSteps]
tag: (name, args...) ->
options = @extractOptions(args)
@openTag(name, options.attributes)
if name in voidElements
if (options.text? or options.content?)
throw new Error("Self-closing tag #{name} cannot have text or content")
else
options.content?()
@text(options.text) if options.text
@closeTag(name)
openTag: (name, attributes) ->
attributePairs =
for attributeName, value of attributes
"#{attributeName}=\"#{value}\""
attributesString =
if attributePairs.length
" " + attributePairs.join(" ")
else
""
@document.push "<#{name}#{attributesString}>"
closeTag: (name) ->
@document.push "</#{name}>"
text: (string) ->
escapedString = string
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
@document.push escapedString
raw: (string) ->
@document.push string
subview: (outletName, subview) ->
subviewId = "subview-#{++idCounter}"
@tag 'div', id: subviewId
@postProcessingSteps.push (view) ->
view[outletName] = subview
subview.parentView = view
view.find("div##{subviewId}").replaceWith(subview)
extractOptions: (args) ->
options = {}
for arg in args
type = typeof(arg)
if type is "function"
options.content = arg
else if type is "string" or type is "number"
options.text = arg.toString()
else
options.attributes = arg
options
jQuery.fn.view = -> this.data('view')
# Trigger attach event when views are added to the DOM
triggerAttachEvent = (element) ->
if element.attr?('triggerAttachEvents') and element.parents('html').length
element.find('[triggerAttachEvents]').add(element).trigger('attach')
for methodName in ['append', 'prepend', 'after', 'before']
do (methodName) ->
originalMethod = $.fn[methodName]
jQuery.fn[methodName] = (args...) ->
result = originalMethod.apply(this, args)
triggerAttachEvent(args[0])
result
for methodName in ['prependTo', 'appendTo', 'insertAfter', 'insertBefore']
do (methodName) ->
originalMethod = $.fn[methodName]
jQuery.fn[methodName] = (args...) ->
result = originalMethod.apply(this, args)
triggerAttachEvent(this)
result
(exports ? this).View = View