Rename App.coffee to Atom.coffee. This also required moving src/atom,spec/atom to src/app,spec/app

This commit is contained in:
Corey Johnson
2012-04-03 10:33:08 -07:00
parent e0274c293f
commit 1efb712fd3
73 changed files with 15 additions and 14 deletions

View File

@@ -0,0 +1,21 @@
Range = require 'range'
module.exports =
class AceOutdentAdaptor
constructor: (@buffer, @editor) ->
getLine: (row) ->
@buffer.lineForRow(row)
# We don't care where the bracket is; we always outdent one level
findMatchingBracket: ({row, column}) ->
{row: 0, column: 0}
# Does not actually replace text, just line at range.start outdents one level
replace: (range, text) ->
{row, column} = @editor.getCursorBufferPosition()
start = range.start
end = {row: range.start.row, column: range.start.column + atom.tabText.length}
@buffer.change(new Range(start, end), "")
@editor.setCursorBufferPosition({row, column: column - atom.tabText.length})

47
src/app/anchor.coffee Normal file
View File

@@ -0,0 +1,47 @@
Point = require 'point'
module.exports =
class Anchor
editor: null
bufferPosition: null
screenPosition: null
constructor: (editor) ->
@editor = editor
@bufferPosition = new Point(0, 0)
@screenPosition = new Point(0, 0)
handleBufferChange: (e) ->
{ oldRange, newRange } = e
position = @getBufferPosition()
return if position.isLessThan(oldRange.end)
newRow = newRange.end.row
newColumn = newRange.end.column
if position.row == oldRange.end.row
newColumn += position.column - oldRange.end.column
else
newColumn = position.column
newRow += position.row - oldRange.end.row
@setBufferPosition [newRow, newColumn]
getBufferPosition: ->
@bufferPosition
setBufferPosition: (position) ->
screenPosition = @editor.screenPositionForBufferPosition(position)
@setScreenPosition(screenPosition, clip: false)
getScreenPosition: ->
@screenPosition
setScreenPosition: (position, options={}) ->
position = Point.fromObject(position)
clip = options.clip ? true
@screenPosition = if clip then @editor.clipScreenPosition(position) else position
@bufferPosition = @editor.bufferPositionForScreenPosition(position)
Object.freeze @screenPosition
Object.freeze @bufferPosition

41
src/app/atom.coffee Normal file
View File

@@ -0,0 +1,41 @@
Keymap = require 'keymap'
fs = require 'fs'
$ = require 'jquery'
_ = require 'underscore'
require 'underscore-extensions'
module.exports =
class Atom
keymap: null
windows: null
tabText: null
userConfigurationPath: null
constructor: (@loadPath, nativeMethods)->
@windows = []
@setUpKeymap()
@tabText = " "
@userConfigurationPath = fs.absolute "~/.atom/atom.coffee"
setUpKeymap: ->
@keymap = new Keymap()
@handleKeyEvent = (e) => @keymap.handleKeyEvent(e)
$(document).on 'keydown', @handleKeyEvent
@keymap.bindDefaultKeys()
destroy: ->
$(document).off 'keydown', @handleKeyEvent
@keymap.unbindDefaultKeys()
open: (path) ->
$native.open path
quit: ->
$native.terminate null
windowOpened: (window) ->
@windows.push(window) unless _.contains(@windows, window)
windowClosed: (window) ->
_.remove(@windows, window)

View File

@@ -0,0 +1,25 @@
$ = require 'jquery'
_ = require 'underscore'
Specificity = require 'specificity'
module.exports =
class BindingSet
selector: null
commandForEvent: null
constructor: (@selector, mapOrFunction) ->
@specificity = Specificity(@selector)
@commandForEvent = @buildEventHandler(mapOrFunction)
buildEventHandler: (mapOrFunction) ->
if _.isFunction(mapOrFunction)
mapOrFunction
else
(event) =>
for pattern, command of mapOrFunction
return command if @eventMatchesPattern(event, pattern)
null
eventMatchesPattern: (event, pattern) ->
pattern = pattern.replace(/^<|>$/g, '')
event.keystroke == pattern

225
src/app/buffer.coffee Normal file
View File

@@ -0,0 +1,225 @@
_ = require 'underscore'
fs = require 'fs'
Point = require 'point'
Range = require 'range'
EventEmitter = require 'event-emitter'
UndoManager = require 'undo-manager'
module.exports =
class Buffer
@idCounter = 1
lines: null
path: null
constructor: (path) ->
@id = @constructor.idCounter++
@setPath(path)
@lines = ['']
if @getPath() and fs.exists(@getPath())
@setText(fs.read(@getPath()))
else
@setText('')
@undoManager = new UndoManager(this)
getPath: ->
@path
setPath: (path) ->
@path = path
@trigger "path-change", this
getText: ->
@lines.join('\n')
setText: (text) ->
@change(@getRange(), text)
getRange: ->
new Range([0, 0], [@getLastRow(), @lastLine().length])
getTextInRange: (range) ->
range = Range.fromObject(range)
if range.start.row == range.end.row
return @lines[range.start.row][range.start.column...range.end.column]
multipleLines = []
multipleLines.push @lines[range.start.row][range.start.column..] # first line
for row in [range.start.row + 1...range.end.row]
multipleLines.push @lines[row] # middle lines
multipleLines.push @lines[range.end.row][0...range.end.column] # last line
return multipleLines.join '\n'
getLines: ->
@lines
lineForRow: (row) ->
@lines[row]
lineLengthForRow: (row) ->
@lines[row].length
rangeForRow: (row) ->
new Range([row, 0], [row, @lineLengthForRow(row)])
numLines: ->
@getLines().length
getLastRow: ->
@getLines().length - 1
lastLine: ->
@lineForRow(@getLastRow())
getEofPosition: ->
lastRow = @getLastRow()
new Point(lastRow, @lineLengthForRow(lastRow))
characterIndexForPosition: (position) ->
position = Point.fromObject(position)
index = 0
index += @lineLengthForRow(row) + 1 for row in [0...position.row]
index + position.column
positionForCharacterIndex: (index) ->
row = 0
while index >= (lineLength = @lineLengthForRow(row) + 1)
index -= lineLength
row++
new Point(row, index)
deleteRow: (row) ->
range = null
if row == @getLastRow()
range = new Range([row - 1, @lineLengthForRow(row - 1)], [row, @lineLengthForRow(row)])
else
range = new Range([row, 0], [row + 1, 0])
@change(range, '')
insert: (point, text) ->
@change(new Range(point, point), text)
delete: (range) ->
@change(range, '')
change: (oldRange, newText) ->
oldRange = Range.fromObject(oldRange)
newRange = new Range(oldRange.start.copy(), oldRange.start.copy())
prefix = @lines[oldRange.start.row][0...oldRange.start.column]
suffix = @lines[oldRange.end.row][oldRange.end.column..]
oldText = @getTextInRange(oldRange)
newTextLines = newText.split('\n')
if newTextLines.length == 1
newRange.end.column += newText.length
newTextLines = [prefix + newText + suffix]
else
lastLineIndex = newTextLines.length - 1
newTextLines[0] = prefix + newTextLines[0]
newRange.end.row += lastLineIndex
newRange.end.column = newTextLines[lastLineIndex].length
newTextLines[lastLineIndex] += suffix
@lines[oldRange.start.row..oldRange.end.row] = newTextLines
@trigger 'change', { oldRange, newRange, oldText, newText }
undo: ->
@undoManager.undo()
redo: ->
@undoManager.redo()
save: ->
if not @getPath() then throw new Error("Tried to save buffer with no file path")
fs.write @getPath(), @getText()
saveAs: (path) ->
@setPath(path)
@save()
getMode: ->
return @mode if @mode
extension = if @getPath() then @getPath().split('/').pop().split('.').pop() else null
modeName = switch extension
when 'js' then 'javascript'
when 'coffee' then 'coffee'
when 'rb', 'ru' then 'ruby'
when 'c', 'h', 'cpp' then 'c_cpp'
when 'html', 'htm' then 'html'
when 'css' then 'css'
else 'text'
@mode = new (require("ace/mode/#{modeName}").Mode)
traverseRegexMatchesInRange: (regex, range, iterator) ->
range = Range.fromObject(range)
global = regex.global
regex = new RegExp(regex.source, 'gm')
traverseRecursively = (text, startIndex, endIndex, lengthDelta) =>
regex.lastIndex = startIndex
return unless match = regex.exec(text)
matchLength = match[0].length
matchStartIndex = match.index
matchEndIndex = matchStartIndex + matchLength
if matchEndIndex > endIndex
regex.lastIndex = 0
if matchStartIndex < endIndex and match = regex.exec(text[matchStartIndex...endIndex])
matchLength = match[0].length
matchEndIndex = matchStartIndex + matchLength
else
return
startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta)
endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta)
range = new Range(startPosition, endPosition)
recurse = true
replacementText = null
stop = -> recurse = false
replace = (text) -> replacementText = text
iterator(match, range, { stop, replace })
if replacementText
@change(range, replacementText)
lengthDelta += replacementText.length - matchLength
if matchLength is 0
matchStartIndex++
matchEndIndex++
if global and recurse
traverseRecursively(text, matchEndIndex, endIndex, lengthDelta)
startIndex = @characterIndexForPosition(range.start)
endIndex = @characterIndexForPosition(range.end)
traverseRecursively(@getText(), startIndex, endIndex, 0)
backwardsTraverseRegexMatchesInRange: (regex, range, iterator) ->
global = regex.global
regex = new RegExp(regex.source, 'gm')
matches = []
@traverseRegexMatchesInRange regex, range, (match, matchRange) ->
matches.push([match, matchRange])
matches.reverse()
recurse = true
stop = -> recurse = false
replacementText = null
replace = (text) -> replacementText = text
for [match, matchRange] in matches
replacementText = null
iterator(match, matchRange, { stop, replace })
@change(matchRange, replacementText) if replacementText
return unless global and recurse
_.extend(Buffer.prototype, EventEmitter)

View File

@@ -0,0 +1,16 @@
fs = require 'fs'
PEG = require 'pegjs'
module.exports =
class CommandInterpreter
constructor: ->
@parser = PEG.buildParser(fs.read(require.resolve 'commands.pegjs'))
eval: (editor, string) ->
command = @parser.parse(string)
@lastRelativeAddress = command if command.isRelativeAddress()
command.execute(editor)
repeatRelativeAddress: (editor) ->
@lastRelativeAddress?.execute(editor)

View File

@@ -0,0 +1,12 @@
Address = require 'command-interpreter/address'
Range = require 'range'
module.exports =
class AddressRange extends Address
constructor: (@startAddress, @endAddress) ->
getRange: (editor) ->
new Range(@startAddress.getRange(editor).start, @endAddress.getRange(editor).end)
isRelative: ->
@startAddress.isRelative() or @endAddress.isRelative()

View File

@@ -0,0 +1,10 @@
Command = require 'command-interpreter/command'
module.exports =
class Address extends Command
execute: (editor) ->
range = @getRange(editor)
editor.clearSelections()
editor.setSelectionBufferRange(range)
isAddress: -> true

View File

@@ -0,0 +1,5 @@
_ = require 'underscore'
module.exports =
class Command
isAddress: -> false

View File

@@ -0,0 +1,12 @@
_ = require 'underscore'
module.exports =
class CompositeCommand
constructor: (@subcommands) ->
execute: (editor) ->
command.execute(editor) for command in @subcommands
isRelativeAddress: ->
_.all(@subcommands, (command) -> command.isAddress() and command.isRelative())

View File

@@ -0,0 +1,9 @@
Address = require 'command-interpreter/address'
Range = require 'range'
module.exports =
class CurrentSelectionAddress extends Address
getRange: (editor) ->
editor.getSelection().getBufferRange()
isRelative: -> true

View File

@@ -0,0 +1,11 @@
Address = require 'command-interpreter/address'
Range = require 'range'
module.exports =
class EofAddress extends Address
getRange: (editor) ->
lastRow = editor.getLastBufferRow()
column = editor.lineLengthForBufferRow(lastRow)
new Range([lastRow, column], [lastRow, column])
isRelative: -> false

View File

@@ -0,0 +1,12 @@
Address = require 'command-interpreter/address'
Range = require 'range'
module.exports =
class LineAddress extends Address
constructor: (lineNumber) ->
@row = lineNumber - 1
getRange: ->
new Range([@row, 0], [@row + 1, 0])
isRelative: -> false

View File

@@ -0,0 +1,28 @@
Address = require 'command-interpreter/address'
Range = require 'range'
module.exports =
class RegexAddress extends Address
regex: null
constructor: (pattern) ->
@regex = new RegExp(pattern)
getRange: (editor) ->
selectedRange = editor.getLastSelectionInBuffer().getBufferRange()
rangeToSearch = new Range(selectedRange.end, editor.getEofPosition())
rangeToReturn = null
editor.buffer.traverseRegexMatchesInRange @regex, rangeToSearch, (match, range) ->
rangeToReturn = range
if rangeToReturn
rangeToReturn
else
rangeToSearch = new Range([0, 0], rangeToSearch.start)
editor.buffer.traverseRegexMatchesInRange @regex, rangeToSearch, (match, range) ->
rangeToReturn = range
rangeToReturn or selectedRange
isRelative: -> true

View File

@@ -0,0 +1,19 @@
Command = require 'command-interpreter/command'
Range = require 'range'
module.exports =
class SelectAllMatches extends Command
regex: null
constructor: (pattern) ->
@regex = new RegExp(pattern, 'g')
execute: (editor) ->
rangesToSelect = []
for selection in editor.getSelections()
editor.buffer.traverseRegexMatchesInRange @regex, selection.getBufferRange(), (match, range) ->
rangesToSelect.push(range)
editor.clearSelections()
editor.setSelectionBufferRange(rangesToSelect[0])
editor.addSelectionForBufferRange(range) for range in rangesToSelect[1..]

View File

@@ -0,0 +1,16 @@
Command = require 'command-interpreter/command'
module.exports =
class Substitution extends Command
regex: null
replacementText: null
constructor: (pattern, replacementText, options) ->
@replacementText = replacementText
@regex = new RegExp(pattern, options.join(''))
execute: (editor) ->
range = editor.getSelection().getBufferRange()
editor.buffer.traverseRegexMatchesInRange @regex, range, (match, matchRange, { replace }) =>
replace(@replacementText)

View File

@@ -0,0 +1,72 @@
{View} = require 'space-pen'
CommandInterpreter = require 'command-interpreter'
Editor = require 'editor'
{SyntaxError} = require('pegjs').parser
module.exports =
class CommandPanel extends View
@content: ->
@div class: 'command-panel', =>
@div ':', class: 'prompt', outlet: 'prompt'
@subview 'editor', new Editor
commandInterpreter: null
history: null
historyIndex: 0
initialize: ({@rootView})->
requireStylesheet 'command-panel.css'
@commandInterpreter = new CommandInterpreter()
@history = []
@rootView.on 'command-panel:toggle', => @toggle()
@rootView.on 'command-panel:execute', => @execute()
@rootView.on 'command-panel:repeat-relative-address', => @repeatRelativeAddress()
@editor.addClass 'single-line'
@editor.off 'move-up move-down'
@editor.on 'move-up', => @navigateBackwardInHistory()
@editor.on 'move-down', => @navigateForwardInHistory()
toggle: ->
if @parent().length then @hide() else @show()
show: ->
@rootView.append(this)
@prompt.css 'font', @editor.css('font')
@editor.focus()
@editor.buffer.setText('')
hide: ->
@detach()
@rootView.activeEditor().focus()
execute: (command = @editor.getText()) ->
try
@commandInterpreter.eval(@rootView.activeEditor(), command)
catch error
if error instanceof SyntaxError
@addClass 'error'
removeErrorClass = => @removeClass 'error'
window.setTimeout(removeErrorClass, 200)
return
else
throw error
@history.push(command)
@historyIndex = @history.length
@hide()
navigateBackwardInHistory: ->
return if @historyIndex == 0
@historyIndex--
@editor.setText(@history[@historyIndex])
navigateForwardInHistory: ->
return if @historyIndex == @history.length
@historyIndex++
@editor.setText(@history[@historyIndex] or '')
repeatRelativeAddress: ->
@commandInterpreter.repeatRelativeAddress(@rootView.activeEditor())

49
src/app/commands.pegjs Normal file
View File

@@ -0,0 +1,49 @@
{
var CompositeCommand = require('command-interpreter/composite-command')
var Substitution = require('command-interpreter/substitution');
var LineAddress = require('command-interpreter/line-address');
var AddressRange = require('command-interpreter/address-range');
var EofAddress = require('command-interpreter/eof-address');
var CurrentSelectionAddress = require('command-interpreter/current-selection-address')
var RegexAddress = require('command-interpreter/regex-address')
var SelectAllMatches = require('command-interpreter/select-all-matches')
}
start = expressions:(expression+) {
return new CompositeCommand(expressions)
}
expression = _ expression:(address / command) _ { return expression; }
address = addressRange / primitiveAddress
addressRange
= start:primitiveAddress? _ ',' _ end:address? {
if (!start) start = new LineAddress(0)
if (!end) end = new EofAddress()
return new AddressRange(start, end)
}
primitiveAddress
= lineNumber:integer { return new LineAddress(lineNumber) }
/ '$' { return new EofAddress() }
/ '.' { return new CurrentSelectionAddress() }
/ '/' pattern:pattern '/'? { return new RegexAddress(pattern)}
command = substitution / selectAllMatches
substitution
= "s" _ "/" find:pattern "/" replace:pattern "/" _ options:[g]* {
return new Substitution(find, replace, options);
}
selectAllMatches
= 'x' _ '/' pattern:pattern '/'? { return new SelectAllMatches(pattern) }
pattern
= pattern:[^/]* { return pattern.join('') }
integer
= digits:[0-9]+ { return parseInt(digits.join('')); }
_ = " "*

View File

@@ -0,0 +1,93 @@
Cursor = require 'cursor'
_ = require 'underscore'
module.exports =
class CompositeCursor
constructor: (@editor) ->
@cursors = []
@addCursor()
handleBufferChange: (e) ->
@moveCursors (cursor) -> cursor.handleBufferChange(e)
getCursor: (index) ->
index ?= @cursors.length - 1
@cursors[index]
getCursors: ->
@cursors
addCursor: ->
cursor = new Cursor(@editor)
@cursors.push(cursor)
@editor.lines.append(cursor)
cursor
addCursorAtScreenPosition: (screenPosition) ->
cursor = @addCursor()
cursor.setScreenPosition(screenPosition)
addCursorAtBufferPosition: (bufferPosition) ->
cursor = @addCursor()
cursor.setBufferPosition(bufferPosition)
removeCursor: (cursor) ->
_.remove(@cursors, cursor)
moveCursors: (fn) ->
fn(cursor) for cursor in @cursors
@mergeCursors()
setScreenPosition: (screenPosition) ->
@moveCursors (cursor) -> cursor.setScreenPosition(screenPosition)
setBufferPosition: (bufferPosition) ->
@moveCursors (cursor) -> cursor.setBufferPosition(bufferPosition)
updateBufferPosition: ->
@moveCursors (cursor) -> cursor.setBufferPosition(cursor.getBufferPosition())
moveLeft: ->
@moveCursors (cursor) -> cursor.moveLeft()
moveRight: ->
@moveCursors (cursor) -> cursor.moveRight()
moveUp: ->
@moveCursors (cursor) -> cursor.moveUp()
moveDown: ->
@moveCursors (cursor) -> cursor.moveDown()
moveToNextWord: ->
@moveCursors (cursor) -> cursor.moveToNextWord()
moveToBeginningOfWord: ->
@moveCursors (cursor) -> cursor.moveToBeginningOfWord()
moveToEndOfWord: ->
@moveCursors (cursor) -> cursor.moveToEndOfWord()
moveToTop: ->
@moveCursors (cursor) -> cursor.moveToTop()
moveToBottom: ->
@moveCursors (cursor) -> cursor.moveToBottom()
moveToBeginningOfLine: ->
@moveCursors (cursor) -> cursor.moveToBeginningOfLine()
moveToEndOfLine: ->
@moveCursors (cursor) -> cursor.moveToEndOfLine()
moveToFirstCharacterOfLine: ->
@moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine()
mergeCursors: ->
positions = []
for cursor in new Array(@cursors...)
position = cursor.getBufferPosition().toString()
if position in positions
cursor.remove()
else
positions.push(position)

View File

@@ -0,0 +1,138 @@
Selection = require 'selection'
_ = require 'underscore'
module.exports =
class CompositeSeleciton
constructor: (@editor) ->
@selections = []
handleBufferChange: (e) ->
selection.handleBufferChange(e) for selection in @getSelections()
getSelection: (index) ->
index ?= @selections.length - 1
@selections[index]
getSelections: ->
new Array(@selections...)
getLastSelection: ->
_.last(@selections)
getLastSelectionInBuffer: ->
_.last(@getSelections().sort (a, b) ->
aRange = a.getBufferRange()
bRange = b.getBufferRange()
aRange.end.compare(bRange.end))
clearSelections: ->
for selection in @getSelections()[1..]
selection.cursor.remove()
@getLastSelection().clearSelection()
addSelectionForCursor: (cursor) ->
selection = new Selection({@editor, cursor})
@selections.push(selection)
@editor.lines.append(selection)
selection
addSelectionForBufferRange: (bufferRange, options) ->
cursor = @editor.compositeCursor.addCursor()
@selectionForCursor(cursor).setBufferRange(bufferRange, options)
removeSelectionForCursor: (cursor) ->
selection = @selectionForCursor(cursor)
selection.cursor = null
selection.remove()
_.remove(@selections, selection)
selectionForCursor: (cursor) ->
_.find @selections, (selection) -> selection.cursor == cursor
setBufferRange: (bufferRange, options) ->
@getLastSelection().setBufferRange(bufferRange, options)
getBufferRange: (bufferRange) ->
@getLastSelection().getBufferRange()
getText: ->
@getLastSelection().getText()
expandSelectionsForward: (fn) ->
fn(selection) for selection in @getSelections()
@mergeIntersectingSelections()
expandSelectionsBackward: (fn) ->
fn(selection) for selection in @getSelections()
@mergeIntersectingSelections(reverse: true)
insertText: (text) ->
selection.insertText(text) for selection in @getSelections()
backspace: ->
selection.backspace() for selection in @getSelections()
backspaceToBeginningOfWord: ->
selection.backspaceToBeginningOfWord() for selection in @getSelections()
delete: ->
selection.delete() for selection in @getSelections()
deleteToEndOfWord: ->
selection.deleteToEndOfWord() for selection in @getSelections()
selectToScreenPosition: (position) ->
@getLastSelection().selectToScreenPosition(position)
selectRight: ->
@expandSelectionsForward (selection) => selection.selectRight()
selectLeft: ->
@expandSelectionsBackward (selection) => selection.selectLeft()
selectUp: ->
@expandSelectionsBackward (selection) => selection.selectUp()
selectDown: ->
@expandSelectionsForward (selection) => selection.selectDown()
selectToTop: ->
@expandSelectionsBackward (selection) => selection.selectToTop()
selectToBottom: ->
@expandSelectionsForward (selection) => selection.selectToBottom()
selectToBeginningOfLine: ->
@expandSelectionsBackward (selection) => selection.selectToBeginningOfLine()
selectToEndOfLine: ->
@expandSelectionsForward (selection) => selection.selectToEndOfLine()
selectToBeginningOfWord: ->
@expandSelectionsBackward (selection) => selection.selectToBeginningOfWord()
selectToEndOfWord: ->
@expandSelectionsForward (selection) => selection.selectToEndOfWord()
cut: ->
maintainPasteboard = false
for selection in @getSelections()
selection.cut(maintainPasteboard)
maintainPasteboard = true
copy: ->
maintainPasteboard = false
for selection in @getSelections()
selection.copy(maintainPasteboard)
maintainPasteboard = true
mergeIntersectingSelections: (options) ->
for selection in @getSelections()
otherSelections = @getSelections()
_.remove(otherSelections, selection)
for otherSelection in otherSelections
if selection.intersectsWith(otherSelection)
selection.merge(otherSelection, options)
@mergeIntersectingSelections(options)
return

168
src/app/cursor.coffee Normal file
View File

@@ -0,0 +1,168 @@
{View} = require 'space-pen'
Anchor = require 'anchor'
Point = require 'point'
_ = require 'underscore'
module.exports =
class Cursor extends View
@content: ->
@pre class: 'cursor idle', => @raw '&nbsp;'
anchor: null
editor: null
wordRegex: /(\w+)|([^\w\s]+)/g
initialize: (@editor) ->
@anchor = new Anchor(@editor)
@selection = @editor.compositeSelection.addSelectionForCursor(this)
@one 'attach', => @updateAppearance()
handleBufferChange: (e) ->
@anchor.handleBufferChange(e)
@refreshScreenPosition()
remove: ->
@editor.compositeCursor.removeCursor(this)
@editor.compositeSelection.removeSelectionForCursor(this)
super
getBufferPosition: ->
@anchor.getBufferPosition()
setBufferPosition: (bufferPosition) ->
@anchor.setBufferPosition(bufferPosition)
@refreshScreenPosition()
@clearSelection()
getScreenPosition: ->
@anchor.getScreenPosition()
setScreenPosition: (position, options={}) ->
@anchor.setScreenPosition(position, options)
@refreshScreenPosition(position, options)
@clearSelection()
refreshScreenPosition: ->
@goalColumn = null
@updateAppearance()
@trigger 'cursor:position-changed'
@removeClass 'idle'
window.clearTimeout(@idleTimeout) if @idleTimeout
@idleTimeout = window.setTimeout (=> @addClass 'idle'), 200
clearSelection: ->
@selection.clearSelection() unless @selection.retainSelection
getCurrentBufferLine: ->
@editor.lineForBufferRow(@getBufferPosition().row)
isOnEOL: ->
@getScreenPosition().column == @getCurrentBufferLine().length
moveUp: ->
{ row, column } = @getScreenPosition()
column = @goalColumn if @goalColumn?
@setScreenPosition({row: row - 1, column: column})
@goalColumn = column
moveDown: ->
{ row, column } = @getScreenPosition()
column = @goalColumn if @goalColumn?
@setScreenPosition({row: row + 1, column: column})
@goalColumn = column
moveToNextWord: ->
bufferPosition = @getBufferPosition()
range = [bufferPosition, @editor.getEofPosition()]
nextPosition = null
@editor.traverseRegexMatchesInRange @wordRegex, range, (match, matchRange, { stop }) =>
if matchRange.start.isGreaterThan(bufferPosition)
nextPosition = matchRange.start
stop()
@setBufferPosition(nextPosition or @editor.getEofPosition())
moveToBeginningOfWord: ->
bufferPosition = @getBufferPosition()
range = [[0,0], bufferPosition]
@editor.backwardsTraverseRegexMatchesInRange @wordRegex, range, (match, matchRange, { stop }) =>
@setBufferPosition matchRange.start
stop()
moveToEndOfWord: ->
bufferPosition = @getBufferPosition()
range = [bufferPosition, @editor.getEofPosition()]
@editor.traverseRegexMatchesInRange @wordRegex, range, (match, matchRange, { stop }) =>
@setBufferPosition matchRange.end
stop()
moveToEndOfLine: ->
{ row } = @getBufferPosition()
@setBufferPosition({ row, column: @editor.buffer.lineForRow(row).length })
moveToBeginningOfLine: ->
{ row } = @getScreenPosition()
@setScreenPosition({ row, column: 0 })
moveToFirstCharacterOfLine: ->
position = @getBufferPosition()
range = @editor.rangeForBufferRow(position.row)
newPosition = null
@editor.traverseRegexMatchesInRange /^\s*/, range, (match, matchRange) =>
newPosition = matchRange.end
newPosition = [position.row, 0] if newPosition.isEqual(position)
@setBufferPosition(newPosition)
moveRight: ->
{ row, column } = @getScreenPosition()
@setScreenPosition(@editor.clipScreenPosition([row, column + 1], skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true))
moveLeft: ->
{ row, column } = @getScreenPosition()
[row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity]
@setScreenPosition({row, column})
moveToTop: ->
@setBufferPosition [0,0]
moveToBottom: ->
@setBufferPosition @editor.getEofPosition()
updateAppearance: ->
position = @editor.pixelPositionForScreenPosition(@getScreenPosition())
@css(position)
_.defer =>
@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
if desiredBottom > @editor.scrollBottom()
@editor.scrollBottom(desiredBottom)
else if desiredTop < @editor.scrollTop()
@editor.scrollTop(desiredTop)
autoScrollHorizontally: (position) ->
return if @editor.softWrap
charsInView = @editor.horizontalScroller.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.horizontalScroller.scrollRight()
@editor.horizontalScroller.scrollRight(desiredRight)
else if desiredLeft < @editor.horizontalScroller.scrollLeft()
@editor.horizontalScroller.scrollLeft(desiredLeft)

View File

@@ -0,0 +1,8 @@
Point = require 'point'
module.exports =
class EditSession
cursorScreenPosition: new Point(0, 0)
scrollTop: 0
scrollLeft: 0

462
src/app/editor.coffee Normal file
View File

@@ -0,0 +1,462 @@
{View, $$} = require 'space-pen'
Buffer = require 'buffer'
CompositeCursor = require 'composite-cursor'
CompositeSelection = require 'composite-selection'
Gutter = require 'gutter'
Renderer = require 'renderer'
Point = require 'point'
Range = require 'range'
EditSession = require 'edit-session'
$ = require 'jquery'
_ = require 'underscore'
module.exports =
class Editor extends View
@idCounter: 1
@content: ->
@div class: 'editor', tabindex: -1, =>
@div class: 'scrollable-content', =>
@subview 'gutter', new Gutter
@div class: 'horizontal-scroller', outlet: 'horizontalScroller', =>
@div class: 'lines', outlet: 'lines', =>
@input class: 'hidden-input', outlet: 'hiddenInput'
vScrollMargin: 2
hScrollMargin: 10
softWrap: false
lineHeight: null
charWidth: null
charHeight: null
cursor: null
selection: null
buffer: null
highlighter: null
renderer: null
autoIndent: null
lineCache: null
isFocused: false
initialize: ({buffer}) ->
requireStylesheet 'editor.css'
requireStylesheet 'theme/twilight.css'
@id = Editor.idCounter++
@editSessionsByBufferId = {}
@bindKeys()
@buildCursorAndSelection()
@handleEvents()
@setBuffer(buffer ? new Buffer)
@autoIndent = true
bindKeys: ->
@on 'save', => @save()
@on 'move-right', => @moveCursorRight()
@on 'move-left', => @moveCursorLeft()
@on 'move-down', => @moveCursorDown()
@on 'move-up', => @moveCursorUp()
@on 'move-to-next-word', => @moveCursorToNextWord()
@on 'move-to-previous-word', => @moveCursorToPreviousWord()
@on 'select-right', => @selectRight()
@on 'select-left', => @selectLeft()
@on 'select-up', => @selectUp()
@on 'select-down', => @selectDown()
@on 'newline', => @insertText("\n")
@on 'backspace', => @backspace()
@on 'backspace-to-beginning-of-word', => @backspaceToBeginningOfWord()
@on 'delete', => @delete()
@on 'delete-to-end-of-word', => @deleteToEndOfWord()
@on 'cut', => @cutSelection()
@on 'copy', => @copySelection()
@on 'paste', => @paste()
@on 'undo', => @undo()
@on 'redo', => @redo()
@on 'toggle-soft-wrap', => @toggleSoftWrap()
@on 'fold-selection', => @foldSelection()
@on 'split-left', => @splitLeft()
@on 'split-right', => @splitRight()
@on 'split-up', => @splitUp()
@on 'split-down', => @splitDown()
@on 'close', => @remove(); false
@on 'move-to-top', => @moveCursorToTop()
@on 'move-to-bottom', => @moveCursorToBottom()
@on 'move-to-beginning-of-line', => @moveCursorToBeginningOfLine()
@on 'move-to-end-of-line', => @moveCursorToEndOfLine()
@on 'move-to-first-character-of-line', => @moveCursorToFirstCharacterOfLine()
@on 'move-to-beginning-of-word', => @moveCursorToBeginningOfWord()
@on 'move-to-end-of-word', => @moveCursorToEndOfWord()
@on 'select-to-top', => @selectToTop()
@on 'select-to-bottom', => @selectToBottom()
@on 'select-to-end-of-line', => @selectToEndOfLine()
@on 'select-to-beginning-of-line', => @selectToBeginningOfLine()
@on 'select-to-end-of-word', => @selectToEndOfWord()
@on 'select-to-beginning-of-word', => @selectToBeginningOfWord()
buildCursorAndSelection: ->
@compositeSelection = new CompositeSelection(this)
@compositeCursor = new CompositeCursor(this)
addCursorAtScreenPosition: (screenPosition) ->
@compositeCursor.addCursorAtScreenPosition(screenPosition)
addCursorAtBufferPosition: (bufferPosition) ->
@compositeCursor.addCursorAtBufferPosition(bufferPosition)
addSelectionForCursor: (cursor) ->
@compositeSelection.addSelectionForCursor(cursor)
handleEvents: ->
@on 'focus', =>
@hiddenInput.focus()
false
@hiddenInput.on 'focus', =>
@rootView()?.editorFocused(this)
@isFocused = true
@addClass 'focused'
@hiddenInput.on 'focusout', =>
@isFocused = false
@removeClass 'focused'
@on 'mousedown', '.fold-placeholder', (e) =>
@destroyFold($(e.currentTarget).attr('foldId'))
false
@on 'mousedown', (e) =>
clickCount = e.originalEvent.detail
if clickCount == 1
screenPosition = @screenPositionFromMouseEvent(e)
if e.metaKey
@addCursorAtScreenPosition(screenPosition)
else
@setCursorScreenPosition(screenPosition)
else if clickCount == 2
@compositeSelection.getLastSelection().selectWord()
else if clickCount >= 3
@compositeSelection.getLastSelection().selectLine()
@selectOnMousemoveUntilMouseup()
@hiddenInput.on "textInput", (e) =>
@insertText(e.originalEvent.data)
@horizontalScroller.on 'scroll', =>
if @horizontalScroller.scrollLeft() == 0
@gutter.removeClass('drop-shadow')
else
@gutter.addClass('drop-shadow')
@one 'attach', =>
@calculateDimensions()
@hiddenInput.width(@charWidth)
@setMaxLineLength() if @softWrap
@focus()
rootView: ->
@parents('#root-view').view()
selectOnMousemoveUntilMouseup: ->
moveHandler = (e) => @selectToScreenPosition(@screenPositionFromMouseEvent(e))
@on 'mousemove', moveHandler
$(document).one 'mouseup', =>
@off 'mousemove', moveHandler
reverse = @compositeSelection.getLastSelection().isReversed()
@compositeSelection.mergeIntersectingSelections({reverse})
renderLines: ->
@lineCache = []
@lines.find('.line').remove()
@insertLineElements(0, @buildLineElements(0, @getLastScreenRow()))
getScreenLines: ->
@renderer.getLines()
linesForRows: (start, end) ->
@renderer.linesForRows(start, end)
screenLineCount: ->
@renderer.lineCount()
getLastScreenRow: ->
@screenLineCount() - 1
setBuffer: (buffer) ->
if @buffer
@saveEditSession()
@unsubscribeFromBuffer()
@buffer = buffer
@trigger 'buffer-path-change'
@buffer.on "path-change.editor#{@id}", => @trigger 'buffer-path-change'
@renderer = new Renderer(@buffer)
@renderLines()
@gutter.renderLineNumbers()
@loadEditSessionForBuffer(@buffer)
@buffer.on "change.editor#{@id}", (e) => @handleBufferChange(e)
@renderer.on 'change', (e) => @handleRendererChange(e)
loadEditSessionForBuffer: (buffer) ->
@editSession = (@editSessionsByBufferId[buffer.id] ?= new EditSession)
@setCursorScreenPosition(@editSession.cursorScreenPosition)
@scrollTop(@editSession.scrollTop)
@horizontalScroller.scrollLeft(@editSession.scrollLeft)
saveEditSession: ->
@editSession.cursorScreenPosition = @getCursorScreenPosition()
@editSession.scrollTop = @scrollTop()
@editSession.scrollLeft = @horizontalScroller.scrollLeft()
handleBufferChange: (e) ->
@compositeCursor.handleBufferChange(e)
@compositeSelection.handleBufferChange(e)
handleRendererChange: (e) ->
{ oldRange, newRange } = e
unless newRange.isSingleLine() and newRange.coversSameRows(oldRange)
@gutter.renderLineNumbers(@getScreenLines())
@compositeCursor.updateBufferPosition() unless e.bufferChanged
lineElements = @buildLineElements(newRange.start.row, newRange.end.row)
@replaceLineElements(oldRange.start.row, oldRange.end.row, lineElements)
buildLineElements: (startRow, endRow) ->
charWidth = @charWidth
charHeight = @charHeight
lines = @renderer.linesForRows(startRow, endRow)
$$ ->
for line in lines
@div class: 'line', =>
appendNbsp = true
for token in line.tokens
if token.type is 'fold-placeholder'
@span ' ', class: 'fold-placeholder', style: "width: #{3 * charWidth}px; height: #{charHeight}px;", 'foldId': token.fold.id, =>
@div class: "ellipsis", => @raw "&hellip;"
else
appendNbsp = false
@span { class: token.type.replace('.', ' ') }, token.value
@raw '&nbsp;' if appendNbsp
insertLineElements: (row, lineElements) ->
@spliceLineElements(row, 0, lineElements)
replaceLineElements: (startRow, endRow, lineElements) ->
@spliceLineElements(startRow, endRow - startRow + 1, lineElements)
spliceLineElements: (startRow, rowCount, lineElements) ->
endRow = startRow + rowCount
elementToInsertBefore = @lineCache[startRow]
elementsToReplace = @lineCache[startRow...endRow]
@lineCache[startRow...endRow] = lineElements?.toArray() or []
lines = @lines[0]
if lineElements
fragment = document.createDocumentFragment()
lineElements.each -> fragment.appendChild(this)
if elementToInsertBefore
lines.insertBefore(fragment, elementToInsertBefore)
else
lines.appendChild(fragment)
elementsToReplace.forEach (element) =>
lines.removeChild(element)
getLineElement: (row) ->
@lineCache[row]
toggleSoftWrap: ->
@setSoftWrap(not @softWrap)
setMaxLineLength: (maxLineLength) ->
maxLineLength ?=
if @softWrap
Math.floor(@horizontalScroller.width() / @charWidth)
else
Infinity
@renderer.setMaxLineLength(maxLineLength) if maxLineLength
createFold: (range) ->
@renderer.createFold(range)
setSoftWrap: (@softWrap, maxLineLength=undefined) ->
@setMaxLineLength(maxLineLength)
if @softWrap
@addClass 'soft-wrap'
@_setMaxLineLength = => @setMaxLineLength()
$(window).on 'resize', @_setMaxLineLength
else
@removeClass 'soft-wrap'
$(window).off 'resize', @_setMaxLineLength
save: ->
if not @buffer.getPath()
path = $native.saveDialog()
return if not path
@buffer.saveAs(path)
else
@buffer.save()
clipScreenPosition: (screenPosition, options={}) ->
@renderer.clipScreenPosition(screenPosition, options)
pixelPositionForScreenPosition: ({row, column}) ->
{ top: row * @lineHeight, left: column * @charWidth }
screenPositionFromPixelPosition: ({top, left}) ->
screenPosition = new Point(Math.floor(top / @lineHeight), Math.floor(left / @charWidth))
screenPositionForBufferPosition: (position) ->
@renderer.screenPositionForBufferPosition(position)
bufferPositionForScreenPosition: (position) ->
@renderer.bufferPositionForScreenPosition(position)
screenRangeForBufferRange: (range) ->
@renderer.screenRangeForBufferRange(range)
bufferRangeForScreenRange: (range) ->
@renderer.bufferRangeForScreenRange(range)
bufferRowsForScreenRows: ->
@renderer.bufferRowsForScreenRows()
screenPositionFromMouseEvent: (e) ->
{ pageX, pageY } = e
@screenPositionFromPixelPosition
top: pageY - @horizontalScroller.offset().top
left: pageX - @horizontalScroller.offset().left + @horizontalScroller.scrollLeft()
calculateDimensions: ->
fragment = $('<div class="line" style="position: absolute; visibility: hidden;"><span>x</span></div>')
@lines.append(fragment)
@charWidth = fragment.width()
@charHeight = fragment.find('span').height()
@lineHeight = fragment.outerHeight()
fragment.remove()
getCursors: -> @compositeCursor.getCursors()
moveCursorUp: -> @compositeCursor.moveUp()
moveCursorDown: -> @compositeCursor.moveDown()
moveCursorRight: -> @compositeCursor.moveRight()
moveCursorLeft: -> @compositeCursor.moveLeft()
moveCursorToNextWord: -> @compositeCursor.moveToNextWord()
moveCursorToBeginningOfWord: -> @compositeCursor.moveToBeginningOfWord()
moveCursorToEndOfWord: -> @compositeCursor.moveToEndOfWord()
moveCursorToTop: -> @compositeCursor.moveToTop()
moveCursorToBottom: -> @compositeCursor.moveToBottom()
moveCursorToBeginningOfLine: -> @compositeCursor.moveToBeginningOfLine()
moveCursorToFirstCharacterOfLine: -> @compositeCursor.moveToFirstCharacterOfLine()
moveCursorToEndOfLine: -> @compositeCursor.moveToEndOfLine()
setCursorScreenPosition: (position) -> @compositeCursor.setScreenPosition(position)
getCursorScreenPosition: -> @compositeCursor.getCursor().getScreenPosition()
setCursorBufferPosition: (position) -> @compositeCursor.setBufferPosition(position)
getCursorBufferPosition: -> @compositeCursor.getCursor().getBufferPosition()
getSelection: (index) -> @compositeSelection.getSelection(index)
getSelections: -> @compositeSelection.getSelections()
getLastSelectionInBuffer: -> @compositeSelection.getLastSelectionInBuffer()
getSelectedText: -> @compositeSelection.getSelection().getText()
setSelectionBufferRange: (bufferRange, options) -> @compositeSelection.setBufferRange(bufferRange, options)
addSelectionForBufferRange: (bufferRange, options) -> @compositeSelection.addSelectionForBufferRange(bufferRange, options)
selectRight: -> @compositeSelection.selectRight()
selectLeft: -> @compositeSelection.selectLeft()
selectUp: -> @compositeSelection.selectUp()
selectDown: -> @compositeSelection.selectDown()
selectToTop: -> @compositeSelection.selectToTop()
selectToBottom: -> @compositeSelection.selectToBottom()
selectToBeginningOfLine: -> @compositeSelection.selectToBeginningOfLine()
selectToEndOfLine: -> @compositeSelection.selectToEndOfLine()
selectToBeginningOfWord: -> @compositeSelection.selectToBeginningOfWord()
selectToEndOfWord: -> @compositeSelection.selectToEndOfWord()
selectToScreenPosition: (position) -> @compositeSelection.selectToScreenPosition(position)
clearSelections: -> @compositeSelection.clearSelections()
backspace: -> @compositeSelection.backspace()
backspaceToBeginningOfWord: -> @compositeSelection.backspaceToBeginningOfWord()
delete: -> @compositeSelection.delete()
deleteToEndOfWord: -> @compositeSelection.deleteToEndOfWord()
setText: (text) -> @buffer.setText(text)
getText: -> @buffer.getText()
getLastBufferRow: -> @buffer.getLastRow()
getTextInRange: (range) -> @buffer.getTextInRange(range)
getEofPosition: -> @buffer.getEofPosition()
lineForBufferRow: (row) -> @buffer.lineForRow(row)
lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row)
rangeForBufferRow: (row) -> @buffer.rangeForRow(row)
traverseRegexMatchesInRange: (args...) -> @buffer.traverseRegexMatchesInRange(args...)
backwardsTraverseRegexMatchesInRange: (args...) -> @buffer.backwardsTraverseRegexMatchesInRange(args...)
insertText: (text) ->
@compositeSelection.insertText(text)
cutSelection: -> @compositeSelection.cut()
copySelection: -> @compositeSelection.copy()
paste: -> @insertText($native.readFromPasteboard())
foldSelection: -> @getSelection().fold()
undo: ->
@buffer.undo()
redo: ->
@buffer.redo()
destroyFold: (foldId) ->
fold = @renderer.foldsById[foldId]
fold.destroy()
@setCursorBufferPosition(fold.start)
splitLeft: ->
@split('row', 'before')
splitRight: ->
@split('row', 'after')
splitUp: ->
@split('column', 'before')
splitDown: ->
@split('column', 'after')
split: (axis, insertMethod) ->
unless @parent().hasClass axis
container = $$ -> @div class: axis
container.insertBefore(this).append(this.detach())
editor = new Editor({@buffer})
editor.setCursorScreenPosition(@getCursorScreenPosition())
this[insertMethod](editor)
@rootView().adjustSplitPanes()
editor
remove: (selector, keepData) ->
return super if keepData
@unsubscribeFromBuffer()
rootView = @rootView()
parent = @parent()
super
parent.remove() if parent.is('.row:empty, .column:empty')
rootView?.editorRemoved(this)
unsubscribeFromBuffer: ->
@buffer.off ".editor#{@id}"
@renderer.destroy()
stateForScreenRow: (row) ->
@renderer.lineForRow(row).state
getCurrentMode: ->
@buffer.getMode()
logLines: ->
@renderer.logLines()

View File

@@ -0,0 +1,51 @@
_ = require 'underscore'
module.exports =
on: (eventName, handler) ->
[eventName, namespace] = eventName.split('.')
@eventHandlersByEventName ?= {}
@eventHandlersByEventName[eventName] ?= []
@eventHandlersByEventName[eventName].push(handler)
if namespace
@eventHandlersByNamespace ?= {}
@eventHandlersByNamespace[namespace] ?= {}
@eventHandlersByNamespace[namespace][eventName] ?= []
@eventHandlersByNamespace[namespace][eventName].push(handler)
trigger: (eventName, event) ->
[eventName, namespace] = eventName.split('.')
if namespace
@eventHandlersByNamespace?[namespace]?[eventName]?.forEach (handler) -> handler(event)
else
@eventHandlersByEventName?[eventName]?.forEach (handler) -> handler(event)
off: (eventName, handler) ->
[eventName, namespace] = eventName.split('.')
eventName = undefined if eventName is ''
if namespace
if eventName
handlers = @eventHandlersByNamespace?[namespace]?[eventName] ? []
for handler in new Array(handlers...)
_.remove(handlers, handler)
@off eventName, handler
else
for eventName, handlers of @eventHandlersByNamespace?[namespace] ? {}
for handler in new Array(handlers...)
_.remove(handlers, handler)
@off eventName, handler
else
if handler
_.remove(@eventHandlersByEventName[eventName], handler)
else
delete @eventHandlersByEventName?[eventName]
subscriptionCount: ->
count = 0
for name, handlers of @eventHandlersByEventName
count += handlers.length
count

View File

@@ -0,0 +1,78 @@
$ = require 'jquery'
{View} = require 'space-pen'
stringScore = require 'stringscore'
Editor = require 'editor'
module.exports =
class FileFinder extends View
@content: ->
@div class: 'file-finder', =>
@ol outlet: 'pathList'
@subview 'editor', new Editor
paths: null
maxResults: null
initialize: ({@paths, @selected}) ->
requireStylesheet 'file-finder.css'
@maxResults = 10
@previousFocusedElement = $(document.activeElement)
@populatePathList()
@on 'file-finder:close', => @remove()
@on 'move-up', => @moveUp()
@on 'move-down', => @moveDown()
@on 'file-finder:select-file', => @select()
@editor.addClass 'single-line'
@editor.buffer.on 'change', => @populatePathList()
@editor.off 'move-up move-down'
populatePathList: ->
@pathList.empty()
for path in @findMatches(@editor.buffer.getText())
@pathList.append $("<li>#{path}</li>")
@pathList.children('li:first').addClass 'selected'
findSelectedLi: ->
@pathList.children('li.selected')
select: ->
filePath = @findSelectedLi().text()
@selected(filePath) if filePath and @selected
@remove()
remove: ->
super()
@previousFocusedElement.focus()
moveUp: ->
@findSelectedLi()
.filter(':not(:first-child)')
.removeClass('selected')
.prev()
.addClass('selected')
moveDown: ->
@findSelectedLi()
.filter(':not(:last-child)')
.removeClass('selected')
.next()
.addClass('selected')
findMatches: (query) ->
if not query
paths = @paths
else
scoredPaths = ({path, score: stringScore(path, query)} for path in @paths)
scoredPaths.sort (a, b) ->
if a.score > b.score then -1
else if a.score < b.score then 1
else 0
window.x = scoredPaths
paths = (pathAndScore.path for pathAndScore in scoredPaths when pathAndScore.score > 0)
paths.slice 0, @maxResults

51
src/app/fold.coffee Normal file
View File

@@ -0,0 +1,51 @@
Range = require 'range'
module.exports =
class Fold
@idCounter: 1
start: null
end: null
constructor: (@lineFolder, {@start, @end}) ->
@id = @constructor.idCounter++
destroy: ->
@lineFolder.destroyFold(this)
getRange: ->
new Range(@start, @end)
handleBufferChange: (event) ->
oldStartRow = @start.row
{ oldRange } = event
if oldRange.start.isLessThanOrEqual(@start) and oldRange.end.isGreaterThanOrEqual(@end)
@lineFolder.unregisterFold(oldStartRow, this)
return
changeInsideFold = @start.isLessThanOrEqual(oldRange.start) and @end.isGreaterThan(oldRange.end)
@start = @updateAnchorPoint(@start, event)
@end = @updateAnchorPoint(@end, event, false)
if @start.row != oldStartRow
@lineFolder.unregisterFold(oldStartRow, this)
@lineFolder.registerFold(@start.row, this)
changeInsideFold
updateAnchorPoint: (point, event, inclusive=true) ->
{ newRange, oldRange } = event
if inclusive
return point if oldRange.end.isGreaterThan(point)
else
return point if oldRange.end.isGreaterThanOrEqual(point)
newRange.end.add(point.subtract(oldRange.end))
compare: (other) ->
startComparison = @start.compare(other.start)
if startComparison == 0
other.end.compare(@end)
else
startComparison

17
src/app/gutter.coffee Normal file
View File

@@ -0,0 +1,17 @@
{View, $$$} = require 'space-pen'
$ = require 'jquery'
_ = require 'underscore'
module.exports =
class Gutter extends View
@content: ->
@div class: 'gutter'
renderLineNumbers: ->
lastRow = -1
rows = @parentView.bufferRowsForScreenRows()
this[0].innerHTML = $$$ ->
for row in rows
@div {class: 'line-number'}, if row == lastRow then '' else row + 1
lastRow = row

View File

@@ -0,0 +1,72 @@
_ = require 'underscore'
ScreenLineFragment = require 'screen-line-fragment'
EventEmitter = require 'event-emitter'
module.exports =
class Highlighter
@idCounter: 1
buffer: null
screenLines: []
constructor: (@buffer) ->
@id = @constructor.idCounter++
@screenLines = @buildLinesForScreenRows('start', 0, @buffer.getLastRow())
@buffer.on "change.highlighter#{@id}", (e) => @handleBufferChange(e)
handleBufferChange: (e) ->
oldRange = e.oldRange.copy()
newRange = e.newRange.copy()
previousState = @screenLines[oldRange.end.row].state # used in spill detection below
startState = @screenLines[newRange.start.row - 1]?.state or 'start'
@screenLines[oldRange.start.row..oldRange.end.row] =
@buildLinesForScreenRows(startState, newRange.start.row, newRange.end.row)
# spill detection
# compare scanner state of last re-highlighted line with its previous state.
# if it differs, re-tokenize the next line with the new state and repeat for
# each line until the line's new state matches the previous state. this covers
# cases like inserting a /* needing to comment out lines below until we see a */
for row in [newRange.end.row...@buffer.getLastRow()]
break if @screenLines[row].state == previousState
nextRow = row + 1
previousState = @screenLines[nextRow].state
@screenLines[nextRow] = @buildLineForScreenRow(@screenLines[row].state, nextRow)
# if highlighting spilled beyond the bounds of the textual change, update
# the pre and post range to reflect area of highlight changes
if nextRow > newRange.end.row
oldRange.end.row += (nextRow - newRange.end.row)
newRange.end.row = nextRow
endColumn = @buffer.lineForRow(nextRow).length
newRange.end.column = endColumn
oldRange.end.column = endColumn
@trigger("change", {oldRange, newRange})
buildLinesForScreenRows: (startState, startRow, endRow) ->
state = startState
for row in [startRow..endRow]
screenLine = @buildLineForScreenRow(state, row)
state = screenLine.state
screenLine
buildLineForScreenRow: (state, row) ->
tokenizer = @buffer.getMode().getTokenizer()
line = @buffer.lineForRow(row)
{tokens, state} = tokenizer.getLineTokens(line, state)
new ScreenLineFragment(tokens, line, [1, 0], [1, 0], { state })
lineForScreenRow: (row) ->
@screenLines[row]
lineForRow: (row) ->
@lineForScreenRow(row)
linesForScreenRows: (startRow, endRow) ->
@screenLines[startRow..endRow]
destroy: ->
@buffer.off ".highlighter#{@id}"
_.extend(Highlighter.prototype, EventEmitter)

94
src/app/keymap.coffee Normal file
View File

@@ -0,0 +1,94 @@
fs = require 'fs'
BindingSet = require 'binding-set'
Specificity = require 'specificity'
$ = require 'jquery'
module.exports =
class Keymap
bindingSetsBySelector: null
constructor: ->
@bindingSets = []
bindDefaultKeys: ->
@bindKeys "*",
'meta-n': 'new-window'
'meta-,': 'open-user-configuration'
'meta-o': 'open'
$(document).on 'new-window', => $native.newWindow()
$(document).on 'open-user-configuration', => atom.open(atom.userConfigurationPath)
$(document).on 'open', =>
path = $native.openDialog()
atom.open(path) if path
unbindDefaultKeys: ->
$(document).unbind 'new-window', @_newWindow
$(document).unbind 'open', @_open
bindKeys: (selector, bindings) ->
@bindingSets.unshift(new BindingSet(selector, bindings))
bindKey: (selector, pattern, eventName) ->
bindings = {}
bindings[pattern] = eventName
@bindKeys(selector, bindings)
handleKeyEvent: (event) ->
event.keystroke = @keystrokeStringForEvent(event)
currentNode = $(event.target)
while currentNode.length
candidateBindingSets = @bindingSets.filter (set) -> currentNode.is(set.selector)
candidateBindingSets.sort (a, b) -> b.specificity - a.specificity
for bindingSet in candidateBindingSets
command = bindingSet.commandForEvent(event)
if command
@triggerCommandEvent(event, command)
return false
else if command == false
return false
currentNode = currentNode.parent()
true
reset: ->
@bindingSets = []
triggerCommandEvent: (keyEvent, commandName) ->
commandEvent = $.Event(commandName)
commandEvent.keyEvent = keyEvent
$(keyEvent.target).trigger(commandEvent)
keystrokeStringForEvent: (event) ->
if /^U\+/i.test event.originalEvent.keyIdentifier
hexCharCode = event.originalEvent.keyIdentifier.replace(/^U\+/i, '')
charCode = parseInt(hexCharCode, 16)
key = @keyFromCharCode(charCode)
else
key = event.originalEvent.keyIdentifier.toLowerCase()
modifiers = ''
if event.altKey and key isnt 'alt'
modifiers += 'alt-'
if event.ctrlKey and key isnt 'ctrl'
modifiers += 'ctrl-'
if event.metaKey and key isnt 'meta'
modifiers += 'meta-'
if event.shiftKey
isNamedKey = key.length > 1
modifiers += 'shift-' if isNamedKey
else
key = key.toLowerCase()
"#{modifiers}#{key}"
keyFromCharCode: (charCode) ->
switch charCode
when 8 then 'backspace'
when 9 then 'tab'
when 13 then 'enter'
when 27 then 'escape'
when 32 then 'space'
when 127 then 'delete'
else String.fromCharCode(charCode)

View File

@@ -0,0 +1,15 @@
window.keymap.bindKeys '.editor'
'meta-up': 'move-to-top'
'meta-down': 'move-to-bottom'
'meta-right': 'move-to-end-of-line'
'meta-left': 'move-to-beginning-of-line'
'alt-left': 'move-to-beginning-of-word'
'alt-right': 'move-to-end-of-word'
'meta-shift-up': 'select-to-top'
'meta-shift-down': 'select-to-bottom'
'meta-shift-left': 'select-to-beginning-of-line'
'meta-shift-right': 'select-to-end-of-line'
'alt-shift-left': 'select-to-beginning-of-word'
'alt-shift-right': 'select-to-end-of-word'
'alt-backspace': 'backspace-to-beginning-of-word'
'alt-delete': 'delete-to-end-of-word'

View File

@@ -0,0 +1,9 @@
window.keymap.bindKeys '*'
'meta-:': 'command-panel:toggle'
window.keymap.bindKeys '.command-panel .editor',
escape: 'command-panel:toggle'
enter: 'command-panel:execute'
window.keymap.bindKeys '.editor',
'meta-g': 'command-panel:repeat-relative-address'

View File

@@ -0,0 +1,30 @@
window.keymap.bindKeys '*'
'meta-s': 'save'
'meta-w': 'close'
'alt-meta-i': 'show-console'
'meta-f': 'find-in-file'
window.keymap.bindKeys '.editor',
'meta-s': 'save'
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: 'backspace'
'delete': 'delete'
'meta-x': 'cut'
'meta-c': 'copy'
'meta-v': 'paste'
'meta-z': 'undo'
'meta-Z': 'redo'
'alt-meta-w': 'toggle-soft-wrap'
'alt-meta-f': 'fold-selection'
'alt-meta-left': 'split-left'
'alt-meta-right': 'split-right'
'alt-meta-up': 'split-up'
'alt-meta-down': 'split-down'

View File

@@ -0,0 +1,13 @@
window.keymap.bindKeys '.editor',
'ctrl-f': 'move-right'
'ctrl-b': 'move-left'
'ctrl-p': 'move-up'
'ctrl-n': 'move-down'
'alt-f': 'move-to-end-of-word'
'alt-b': 'move-to-beginning-of-word'
'ctrl-a': 'move-to-first-character-of-line'
'ctrl-e': 'move-to-end-of-line'
'ctrl-h': 'backspace'
'ctrl-d': 'delete'
'alt-h': 'backspace-to-beginning-of-word'
'alt-d': 'delete-to-end-of-word'

View File

@@ -0,0 +1,6 @@
window.keymap.bindKeys '*'
'meta-t': 'toggle-file-finder'
window.keymap.bindKeys ".file-finder .editor",
'enter': 'file-finder:select-file',
'escape': 'file-finder:close'

176
src/app/line-map.coffee Normal file
View File

@@ -0,0 +1,176 @@
_ = require 'underscore'
Point = require 'point'
Range = require 'range'
module.exports =
class LineMap
constructor: ->
@lineFragments = []
insertAtBufferRow: (bufferRow, lineFragments) ->
@spliceAtBufferRow(bufferRow, 0, lineFragments)
spliceAtBufferRow: (startRow, rowCount, lineFragments) ->
@spliceByDelta('bufferDelta', startRow, rowCount, lineFragments)
spliceAtScreenRow: (startRow, rowCount, lineFragments) ->
@spliceByDelta('screenDelta', startRow, rowCount, lineFragments)
replaceBufferRows: (start, end, lineFragments) ->
@spliceAtBufferRow(start, end - start + 1, lineFragments)
replaceScreenRows: (start, end, lineFragments) ->
@spliceAtScreenRow(start, end - start + 1, lineFragments)
lineForScreenRow: (row) ->
@linesForScreenRows(row, row)[0]
linesForScreenRows: (startRow, endRow) ->
@linesByDelta('screenDelta', startRow, endRow)
lineForBufferRow: (row) ->
@linesForBufferRows(row, row)[0]
linesForBufferRows: (startRow, endRow) ->
@linesByDelta('bufferDelta', startRow, endRow)
bufferRowsForScreenRows: (startRow, endRow=@lastScreenRow())->
bufferRows = []
currentScreenRow = -1
@traverseByDelta 'screenDelta', [startRow, 0], [endRow, 0], ({ screenDelta, bufferDelta }) ->
bufferRows.push(bufferDelta.row) if screenDelta.row > currentScreenRow
currentScreenRow = screenDelta.row
bufferRows
bufferLineCount: ->
@lineCountByDelta('bufferDelta')
screenLineCount: ->
@lineCountByDelta('screenDelta')
lineCountByDelta: (deltaType) ->
@traverseByDelta(deltaType, new Point(Infinity, 0))[deltaType].row
lastScreenRow: ->
@screenLineCount() - 1
screenPositionForBufferPosition: (bufferPosition) ->
@translatePosition('bufferDelta', 'screenDelta', bufferPosition)
bufferPositionForScreenPosition: (screenPosition) ->
@translatePosition('screenDelta', 'bufferDelta', screenPosition)
screenRangeForBufferRange: (bufferRange) ->
bufferRange = Range.fromObject(bufferRange)
start = @screenPositionForBufferPosition(bufferRange.start)
end = @screenPositionForBufferPosition(bufferRange.end)
new Range(start, end)
bufferRangeForScreenRange: (screenRange) ->
start = @bufferPositionForScreenPosition(screenRange.start)
end = @bufferPositionForScreenPosition(screenRange.end)
new Range(start, end)
clipScreenPosition: (screenPosition, options) ->
@clipPosition('screenDelta', screenPosition, options)
clipPosition: (deltaType, position, options={}) ->
options.clipToBounds = true
@translatePosition(deltaType, deltaType, position, options)
spliceByDelta: (deltaType, startRow, rowCount, lineFragments) ->
stopRow = startRow + rowCount
startIndex = undefined
stopIndex = 0
delta = new Point
for lineFragment, i in @lineFragments
startIndex ?= i if delta.row == startRow
break if delta.row == stopRow
delta = delta.add(lineFragment[deltaType])
stopIndex++
startIndex ?= i
@lineFragments[startIndex...stopIndex] = lineFragments
linesByDelta: (deltaType, startRow, endRow) ->
lines = []
pendingFragment = null
@traverseByDelta deltaType, new Point(startRow, 0), new Point(endRow, Infinity), ({lineFragment}) ->
if pendingFragment
pendingFragment = pendingFragment.concat(lineFragment)
else
pendingFragment = lineFragment
if pendingFragment[deltaType].row > 0
lines.push pendingFragment
pendingFragment = null
lines
translatePosition: (sourceDeltaType, targetDeltaType, sourcePosition, options={}) ->
sourcePosition = Point.fromObject(sourcePosition)
wrapBeyondNewlines = options.wrapBeyondNewlines ? false
wrapAtSoftNewlines = options.wrapAtSoftNewlines ? false
skipAtomicTokens = options.skipAtomicTokens ? false
clipToBounds = options.clipToBounds ? false
@clipToBounds(sourceDeltaType, sourcePosition) if clipToBounds
traversalResult = @traverseByDelta(sourceDeltaType, sourcePosition)
lastLineFragment = traversalResult.lastLineFragment
sourceDelta = traversalResult[sourceDeltaType]
targetDelta = traversalResult[targetDeltaType]
return targetDelta unless lastLineFragment
maxSourceColumn = sourceDelta.column + lastLineFragment.text.length
maxTargetColumn = targetDelta.column + lastLineFragment.text.length
if lastLineFragment.isSoftWrapped() and sourcePosition.column >= maxSourceColumn
if wrapAtSoftNewlines
targetDelta.row++
targetDelta.column = 0
else
targetDelta.column = maxTargetColumn - 1
return @clipPosition(targetDeltaType, targetDelta)
else if sourcePosition.column > maxSourceColumn and wrapBeyondNewlines
targetDelta.row++
targetDelta.column = 0
else if lastLineFragment.isAtomic
if skipAtomicTokens and sourcePosition.column > sourceDelta.column
targetDelta.column += lastLineFragment.text.length
else
additionalColumns = sourcePosition.column - sourceDelta.column
targetDelta.column = Math.min(maxTargetColumn, targetDelta.column + additionalColumns)
targetDelta
clipToBounds: (deltaType, position) ->
if position.column < 0
position.column = 0
if position.row < 0
position.row = 0
position.column = 0
maxSourceRow = @lineCountByDelta(deltaType) - 1
if position.row > maxSourceRow
position.row = maxSourceRow
position.column = Infinity
traverseByDelta: (deltaType, startPosition, endPosition=startPosition, iterator=null) ->
traversalDelta = new Point
screenDelta = new Point
bufferDelta = new Point
for lineFragment in @lineFragments
iterator({ lineFragment, screenDelta, bufferDelta }) if traversalDelta.isGreaterThanOrEqual(startPosition) and iterator?
traversalDelta = traversalDelta.add(lineFragment[deltaType])
break if traversalDelta.isGreaterThan(endPosition)
screenDelta = screenDelta.add(lineFragment.screenDelta)
bufferDelta = bufferDelta.add(lineFragment.bufferDelta)
{ screenDelta, bufferDelta, lastLineFragment: lineFragment }
logLines: (start=0, end=@screenLineCount() - 1)->
for row in [start..end]
line = @lineForScreenRow(row).text
console.log row, line, line.length

78
src/app/point.coffee Normal file
View File

@@ -0,0 +1,78 @@
module.exports =
class Point
@fromObject: (object) ->
if object instanceof Point
object
else
if object instanceof Array
[row, column] = object
else
{ row, column } = object
new Point(row, column)
constructor: (@row=0, @column=0) ->
copy: ->
new Point(@row, @column)
add: (other) ->
row = @row + other.row
if other.row == 0
column = @column + other.column
else
column = other.column
new Point(row, column)
subtract: (other) ->
row = @row - other.row
if @row == other.row
column = @column - other.column
else
column = @column
new Point(row, column)
splitAt: (column) ->
if @row == 0
rightColumn = @column - column
else
rightColumn = @column
[new Point(0, column), new Point(@row, rightColumn)]
compare: (other) ->
if @row > other.row
1
else if @row < other.row
-1
else
if @column > other.column
1
else if @column < other.column
-1
else
0
isEqual: (other) ->
other = Point.fromObject(other)
@compare(other) == 0
isLessThan: (other) ->
@compare(other) < 0
isLessThanOrEqual: (other) ->
@compare(other) <= 0
isGreaterThan: (other) ->
@compare(other) > 0
isGreaterThanOrEqual: (other) ->
@compare(other) >= 0
inspect: ->
"(#{@row}, #{@column})"
toString: ->
"#{@row},#{@column}"

24
src/app/project.coffee Normal file
View File

@@ -0,0 +1,24 @@
fs = require 'fs'
Buffer = require 'buffer'
module.exports =
class Project
buffers: null
constructor: (@path) ->
@buffers = {}
getFilePaths: ->
projectPath = @path
fs.async.listTree(@path).pipe (paths) ->
path.replace(projectPath, "") for path in paths when fs.isFile(path)
open: (filePath) ->
filePath = @resolve filePath
@buffers[filePath] ?= new Buffer(filePath)
resolve: (filePath) ->
filePath = fs.join(@path, filePath) unless filePath[0] == '/'
fs.absolute filePath

64
src/app/range.coffee Normal file
View File

@@ -0,0 +1,64 @@
Point = require 'point'
_ = require 'underscore'
module.exports =
class Range
@fromObject: (object) ->
if _.isArray(object)
new Range(object...)
else
object
constructor: (pointA = new Point(0, 0), pointB = new Point(0, 0)) ->
pointA = Point.fromObject(pointA)
pointB = Point.fromObject(pointB)
if pointA.compare(pointB) <= 0
@start = pointA
@end = pointB
else
@start = pointB
@end = pointA
copy: (range) ->
new Range(@start.copy(), @end.copy())
isEqual: (other) ->
if other instanceof Array and other.length == 2
other = new Range(other...)
other.start.isEqual(@start) and other.end.isEqual(@end)
isSingleLine: ->
@start.row == @end.row
coversSameRows: (other) ->
@start.row == other.start.row && @end.row == other.end.row
inspect: ->
"[#{@start.inspect()} - #{@end.inspect()}]"
intersectsWith: (otherRange) ->
if @start.isLessThanOrEqual(otherRange.start)
@end.isGreaterThanOrEqual(otherRange.start)
else
otherRange.intersectsWith(this)
union: (otherRange) ->
start = if @start.isLessThan(otherRange.start) then @start else otherRange.start
end = if @end.isGreaterThan(otherRange.end) then @end else otherRange.end
new Range(start, end)
isEmpty: ->
@start.isEqual(@end)
toDelta: ->
rows = @end.row - @start.row
if rows == 0
columns = @end.column - @start.column
else
columns = @end.column
new Point(rows, columns)

222
src/app/renderer.coffee Normal file
View File

@@ -0,0 +1,222 @@
_ = require 'underscore'
Highlighter = require 'highlighter'
LineMap = require 'line-map'
Point = require 'point'
EventEmitter = require 'event-emitter'
Range = require 'range'
Fold = require 'fold'
ScreenLineFragment = require 'screen-line-fragment'
foldPlaceholderLength = 3
module.exports =
class Renderer
@idCounter: 1
lineMap: null
highlighter: null
activeFolds: null
foldsById: null
lastHighlighterChangeEvent: null
foldPlaceholderLength: 3
constructor: (@buffer) ->
@id = @constructor.idCounter++
@highlighter = new Highlighter(@buffer)
@maxLineLength = Infinity
@activeFolds = {}
@foldsById = {}
@buildLineMap()
@highlighter.on 'change', (e) => @lastHighlighterChangeEvent = e
@buffer.on "change.renderer#{@id}", (e) => @handleBufferChange(e)
buildLineMap: ->
@lineMap = new LineMap
@lineMap.insertAtBufferRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow())
setMaxLineLength: (@maxLineLength) ->
oldRange = @rangeForAllLines()
@buildLineMap()
newRange = @rangeForAllLines()
@trigger 'change', { oldRange, newRange }
lineForRow: (row) ->
@lineMap.lineForScreenRow(row)
linesForRows: (startRow, endRow) ->
@lineMap.linesForScreenRows(startRow, endRow)
getLines: ->
@lineMap.linesForScreenRows(0, @lineMap.lastScreenRow())
bufferRowsForScreenRows: ->
@lineMap.bufferRowsForScreenRows()
createFold: (bufferRange) ->
bufferRange = Range.fromObject(bufferRange)
return if bufferRange.isEmpty()
fold = new Fold(this, bufferRange)
@registerFold(bufferRange.start.row, fold)
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
lines = @buildLineForBufferRow(bufferRange.start.row)
@lineMap.replaceScreenRows(
oldScreenRange.start.row,
oldScreenRange.end.row,
lines)
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange
@trigger 'fold', bufferRange
fold
destroyFold: (fold) ->
bufferRange = fold.getRange()
@unregisterFold(bufferRange.start.row, fold)
startScreenRow = @screenRowForBufferRow(bufferRange.start.row)
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
lines = @buildLinesForBufferRows(bufferRange.start.row, bufferRange.end.row)
@lineMap.replaceScreenRows(
oldScreenRange.start.row,
oldScreenRange.end.row
lines)
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange
@trigger 'unfold', fold.getRange()
screenRowForBufferRow: (bufferRow) ->
@lineMap.screenPositionForBufferPosition([bufferRow, 0]).row
bufferRowForScreenRow: (screenRow) ->
@lineMap.bufferPositionForScreenPosition([screenRow, 0]).row
screenRangeForBufferRange: (bufferRange) ->
@lineMap.screenRangeForBufferRange(bufferRange)
bufferRangeForScreenRange: (screenRange) ->
@lineMap.bufferRangeForScreenRange(screenRange)
lineCount: ->
@lineMap.screenLineCount()
screenPositionForBufferPosition: (position) ->
@lineMap.screenPositionForBufferPosition(position)
bufferPositionForScreenPosition: (position) ->
@lineMap.bufferPositionForScreenPosition(position)
clipScreenPosition: (position, options={}) ->
@lineMap.clipScreenPosition(position, options)
handleBufferChange: (e) ->
for row, folds of @activeFolds
for fold in new Array(folds...)
changeInsideFold = true if fold.handleBufferChange(e)
unless changeInsideFold
@handleHighlighterChange(@lastHighlighterChangeEvent)
handleHighlighterChange: (e) ->
oldBufferRange = e.oldRange
newBufferRange = e.newRange
oldScreenRange = @screenLineRangeForBufferRange(oldBufferRange)
newScreenLines = @buildLinesForBufferRows(newBufferRange.start.row, newBufferRange.end.row)
@lineMap.replaceScreenRows oldScreenRange.start.row, oldScreenRange.end.row, newScreenLines
newScreenRange = @screenLineRangeForBufferRange(newBufferRange)
@trigger 'change', { oldRange: oldScreenRange, newRange: newScreenRange, bufferChanged: true }
buildLineForBufferRow: (bufferRow) ->
@buildLinesForBufferRows(bufferRow, bufferRow)
buildLinesForBufferRows: (startBufferRow, endBufferRow) ->
recursiveBuildLinesForBufferRows = (startBufferRow, endBufferRow, startBufferColumn, currentScreenLineLength=0) =>
return [] if startBufferRow > endBufferRow and not startBufferColumn?
startBufferColumn ?= 0
line = @highlighter.lineForRow(startBufferRow).splitAt(startBufferColumn)[1]
wrapScreenColumn = @findWrapColumn(line.text, @maxLineLength - currentScreenLineLength)
for fold in @foldsForBufferRow(startBufferRow)
if fold.start.column >= startBufferColumn
foldStartSceenColumn = fold.start.column - startBufferColumn
if (foldStartSceenColumn) > wrapScreenColumn - foldPlaceholderLength
wrapScreenColumn = Math.min(wrapScreenColumn, foldStartSceenColumn)
break
prefix = line.splitAt(foldStartSceenColumn)[0]
placeholder = @buildFoldPlaceholder(fold)
currentScreenLineLength = currentScreenLineLength + (prefix?.text.length ? 0) + foldPlaceholderLength
suffix = recursiveBuildLinesForBufferRows(fold.end.row, endBufferRow, fold.end.column, currentScreenLineLength)
return _.compact _.flatten [prefix, placeholder, suffix]
if wrapScreenColumn?
line = line.splitAt(wrapScreenColumn)[0]
line.screenDelta = new Point(1, 0)
[line].concat recursiveBuildLinesForBufferRows(startBufferRow, endBufferRow, startBufferColumn + wrapScreenColumn)
else
[line].concat recursiveBuildLinesForBufferRows(startBufferRow + 1, endBufferRow)
recursiveBuildLinesForBufferRows(@foldStartRowForBufferRow(startBufferRow), endBufferRow)
foldStartRowForBufferRow: (bufferRow) ->
@bufferRowForScreenRow(@screenRowForBufferRow(bufferRow))
findWrapColumn: (line, maxLineLength) ->
return unless line.length > maxLineLength
if /\s/.test(line[maxLineLength])
# search forward for the start of a word past the boundary
for column in [maxLineLength..line.length]
return column if /\S/.test(line[column])
return line.length
else
# search backward for the start of the word on the boundary
for column in [maxLineLength..0]
return column + 1 if /\s/.test(line[column])
return maxLineLength
registerFold: (bufferRow, fold) ->
@activeFolds[bufferRow] ?= []
@activeFolds[bufferRow].push(fold)
@foldsById[fold.id] = fold
unregisterFold: (bufferRow, fold) ->
folds = @activeFolds[bufferRow]
folds.splice(folds.indexOf(fold), 1)
delete @foldsById[fold.id]
foldsForBufferRow: (bufferRow) ->
folds = @activeFolds[bufferRow] or []
folds.sort (a, b) -> a.compare(b)
buildFoldPlaceholder: (fold) ->
token = { value: '...', type: 'fold-placeholder', fold }
new ScreenLineFragment([token], '...', [0, 3], fold.getRange().toDelta(), isAtomic: true)
screenLineRangeForBufferRange: (bufferRange) ->
@expandScreenRangeToLineEnds(
@lineMap.screenRangeForBufferRange(
@expandBufferRangeToLineEnds(bufferRange)))
expandScreenRangeToLineEnds: (screenRange) ->
{ start, end } = screenRange
new Range([start.row, 0], [end.row, @lineMap.lineForScreenRow(end.row).text.length])
expandBufferRangeToLineEnds: (bufferRange) ->
{ start, end } = bufferRange
new Range([start.row, 0], [end.row, Infinity])
rangeForAllLines: ->
new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
destroy: ->
@highlighter.destroy()
@buffer.off ".renderer#{@id}"
logLines: ->
@lineMap.logLines()
_.extend Renderer.prototype, EventEmitter

143
src/app/root-view.coffee Normal file
View File

@@ -0,0 +1,143 @@
$ = require 'jquery'
fs = require 'fs'
_ = require 'underscore'
{View} = require 'space-pen'
Buffer = require 'buffer'
Editor = require 'editor'
FileFinder = require 'file-finder'
Project = require 'project'
VimMode = require 'vim-mode'
CommandPanel = require 'command-panel'
module.exports =
class RootView extends View
@content: ->
@div id: 'root-view', tabindex: -1, =>
@div id: 'panes', outlet: 'panes'
editors: null
initialize: ({path}) ->
@editors = []
@createProject(path)
@on 'toggle-file-finder', => @toggleFileFinder()
@on 'show-console', -> window.showConsole()
@on 'find-in-file', =>
@commandPanel.show()
@commandPanel.editor.setText("/")
@one 'attach', => @focus()
@on 'focus', (e) =>
if @editors.length
@activeEditor().focus()
false
@commandPanel = new CommandPanel({rootView: this})
createProject: (path) ->
if path
@project = new Project(fs.directory(path))
@open(path) if fs.isFile(path)
else
@activeEditor().setBuffer(new Buffer())
open: (path) ->
@activeEditor().setBuffer(@project.open(path))
editorFocused: (editor) ->
if @panes.containsElement(editor)
_.remove(@editors, editor)
@editors.push(editor)
@setTitleToActiveEditorPath()
editor.on 'buffer-path-change.root-view', (event) =>
e = $(event.target).view()
@setTitleToActiveEditorPath()
editorRemoved: (editor) ->
if @panes.containsElement
_.remove(@editors, editor)
@adjustSplitPanes()
if @editors.length
@activeEditor().focus()
else
window.close()
setTitleToActiveEditorPath: ->
document.title = @activeEditor().buffer.path
activeEditor: ->
if @editors.length
_.last(@editors)
else
editor = new Editor()
@editors.push(editor)
editor.appendTo(@panes)
editor.focus()
adjustSplitPanes: (element = @panes.children(':first'))->
if element.hasClass('row')
totalUnits = @horizontalGridUnits(element)
unitsSoFar = 0
for child in element.children()
child = $(child)
childUnits = @horizontalGridUnits(child)
child.css
width: "#{childUnits / totalUnits * 100}%"
height: '100%'
top: 0
left: "#{unitsSoFar / totalUnits * 100}%"
@adjustSplitPanes(child)
unitsSoFar += childUnits
else if element.hasClass('column')
totalUnits = @verticalGridUnits(element)
unitsSoFar = 0
for child in element.children()
child = $(child)
childUnits = @verticalGridUnits(child)
child.css
width: '100%'
height: "#{childUnits / totalUnits * 100}%"
top: "#{unitsSoFar / totalUnits * 100}%"
left: 0
@adjustSplitPanes(child)
unitsSoFar += childUnits
horizontalGridUnits: (element) ->
if element.is('.row, .column')
childUnits = (@horizontalGridUnits($(child)) for child in element.children())
if element.hasClass('row')
_.sum(childUnits)
else # it's a column
Math.max(childUnits...)
else
1
verticalGridUnits: (element) ->
if element.is('.row, .column')
childUnits = (@verticalGridUnits($(child)) for child in element.children())
if element.hasClass('column')
_.sum(childUnits)
else # it's a row
Math.max(childUnits...)
else
1
toggleFileFinder: ->
return unless @project
if @fileFinder and @fileFinder.parent()[0]
@fileFinder.remove()
@fileFinder = null
@activeEditor().focus()
else
@project.getFilePaths().done (paths) =>
relativePaths = (path.replace(@project.path, "") for path in paths)
@fileFinder = new FileFinder
paths: relativePaths
selected: (relativePath) => @open(relativePath)
@append @fileFinder

View File

@@ -0,0 +1,54 @@
_ = require 'underscore'
Point = require 'point'
module.exports =
class ScreenLineFragment
isAtomic: false
state: null
constructor: (@tokens, @text, screenDelta, bufferDelta, extraFields) ->
@screenDelta = Point.fromObject(screenDelta)
@bufferDelta = Point.fromObject(bufferDelta)
_.extend(this, extraFields)
splitAt: (column) ->
return [new ScreenLineFragment([], '', [0, 0], [0, 0]), this] if column == 0
rightTokens = new Array(@tokens...)
leftTokens = []
leftTextLength = 0
while leftTextLength < column
if leftTextLength + rightTokens[0].value.length > column
rightTokens[0..0] = @splitTokenAt(rightTokens[0], column - leftTextLength)
nextToken = rightTokens.shift()
leftTextLength += nextToken.value.length
leftTokens.push nextToken
leftText = _.pluck(leftTokens, 'value').join('')
rightText = _.pluck(rightTokens, 'value').join('')
[leftScreenDelta, rightScreenDelta] = @screenDelta.splitAt(column)
[leftBufferDelta, rightBufferDelta] = @bufferDelta.splitAt(column)
leftFragment = new ScreenLineFragment(leftTokens, leftText, leftScreenDelta, leftBufferDelta, {@state})
rightFragment = new ScreenLineFragment(rightTokens, rightText, rightScreenDelta, rightBufferDelta, {@state})
[leftFragment, rightFragment]
splitTokenAt: (token, splitIndex) ->
{ type, value } = token
value1 = value.substring(0, splitIndex)
value2 = value.substring(splitIndex)
[{value: value1, type }, {value: value2, type}]
concat: (other) ->
tokens = @tokens.concat(other.tokens)
text = @text + other.text
screenDelta = @screenDelta.add(other.screenDelta)
bufferDelta = @bufferDelta.add(other.bufferDelta)
new ScreenLineFragment(tokens, text, screenDelta, bufferDelta, {state: other.state})
isSoftWrapped: ->
@screenDelta.row == 1 and @bufferDelta.row == 0
isEqual: (other) ->
_.isEqual(@tokens, other.tokens) and @screenDelta.isEqual(other.screenDelta) and @bufferDelta.isEqual(other.bufferDelta)

228
src/app/selection.coffee Normal file
View File

@@ -0,0 +1,228 @@
Anchor = require 'anchor'
Cursor = require 'cursor'
AceOutdentAdaptor = require 'ace-outdent-adaptor'
Point = require 'point'
Range = require 'range'
{View, $$} = require 'space-pen'
module.exports =
class Selection extends View
@content: ->
@div()
anchor: null
retainSelection: null
regions: null
initialize: ({@editor, @cursor}) ->
@regions = []
@cursor.on 'cursor:position-changed', => @updateAppearance()
handleBufferChange: (e) ->
return unless @anchor
@anchor.handleBufferChange(e)
placeAnchor: ->
return if @anchor
@anchor = new Anchor(@editor)
@anchor.setScreenPosition @cursor.getScreenPosition()
isEmpty: ->
@getBufferRange().isEmpty()
isReversed: ->
not @isEmpty() and @cursor.getBufferPosition().isLessThan(@anchor.getBufferPosition())
intersectsWith: (otherSelection) ->
@getScreenRange().intersectsWith(otherSelection.getScreenRange())
clearSelection: ->
@anchor = null
@updateAppearance()
updateAppearance: ->
return unless @cursor
@clearRegions()
range = @getScreenRange()
return if range.isEmpty()
rowSpan = range.end.row - range.start.row
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)
appendRegion: (rows, start, end) ->
{ lineHeight, charWidth } = @editor
css = @editor.pixelPositionForScreenPosition(start)
css.height = lineHeight * rows
if end
css.width = @editor.pixelPositionForScreenPosition(end).left - css.left
else
css.right = 0
region = ($$ -> @div class: 'selection').css(css)
@append(region)
@regions.push(region)
clearRegions: ->
region.remove() for region in @regions
@regions = []
getScreenRange: ->
if @anchor
new Range(@anchor.getScreenPosition(), @cursor.getScreenPosition())
else
new Range(@cursor.getScreenPosition(), @cursor.getScreenPosition())
setScreenRange: (range, options={}) ->
{ reverse } = options
{ start, end } = range
[start, end] = [end, start] if reverse
@cursor.setScreenPosition(start)
@modifySelection => @cursor.setScreenPosition(end)
getBufferRange: ->
@editor.bufferRangeForScreenRange(@getScreenRange())
setBufferRange: (bufferRange, options) ->
@setScreenRange(@editor.screenRangeForBufferRange(bufferRange), options)
getText: ->
@editor.buffer.getTextInRange @getBufferRange()
insertText: (text) ->
{ text, shouldOutdent } = @autoIndentText(text)
@editor.buffer.change(@getBufferRange(), text)
@autoOutdentText() if shouldOutdent
@clearSelection()
autoIndentText: (text) ->
if @editor.autoIndent
mode = @editor.getCurrentMode()
row = @cursor.getScreenPosition().row
state = @editor.stateForScreenRow(row)
if text[0] == "\n"
indent = mode.getNextLineIndent(state, @cursor.getCurrentBufferLine(), atom.tabText)
text = text[0] + indent + text[1..]
else if mode.checkOutdent(state, @cursor.getCurrentBufferLine(), text)
shouldOutdent = true
{text, shouldOutdent}
autoOutdentText: ->
screenRow = @cursor.getScreenPosition().row
bufferRow = @cursor.getBufferPosition().row
state = @editor.renderer.lineForRow(screenRow).state
@editor.getCurrentMode().autoOutdent(state, new AceOutdentAdaptor(@editor.buffer, @editor), bufferRow)
backspace: ->
@selectLeft() if @isEmpty()
@deleteSelectedText()
backspaceToBeginningOfWord: ->
@selectToBeginningOfWord() if @isEmpty()
@deleteSelectedText()
delete: ->
@selectRight() if @isEmpty()
@deleteSelectedText()
deleteToEndOfWord: ->
@selectToEndOfWord() if @isEmpty()
@deleteSelectedText()
deleteSelectedText: ->
range = @getBufferRange()
@editor.buffer.delete(range) unless range.isEmpty()
@clearSelection()
merge: (otherSelection, options) ->
@setScreenRange(@getScreenRange().union(otherSelection.getScreenRange()), options)
otherSelection.remove()
remove: ->
@cursor?.remove()
super
modifySelection: (fn) ->
@placeAnchor()
@retainSelection = true
fn()
@retainSelection = false
selectWord: ->
row = @cursor.getScreenPosition().row
column = @cursor.getScreenPosition().column
{ row, column } = @cursor.getBufferPosition()
line = @editor.buffer.lineForRow(row)
leftSide = line[0...column].split('').reverse().join('') # reverse left side
rightSide = line[column..]
regex = /^\w*/
startOffset = -regex.exec(leftSide)?[0]?.length or 0
endOffset = regex.exec(rightSide)?[0]?.length or 0
range = new Range([row, column + startOffset], [row, column + endOffset])
@setBufferRange range
selectLine: (row=@cursor.getBufferPosition().row) ->
rowLength = @editor.buffer.lineForRow(row).length
@setBufferRange new Range([row, 0], [row, rowLength])
selectToScreenPosition: (position) ->
@modifySelection => @cursor.setScreenPosition(position)
selectRight: ->
@modifySelection => @cursor.moveRight()
selectLeft: ->
@modifySelection => @cursor.moveLeft()
selectUp: ->
@modifySelection => @cursor.moveUp()
selectDown: ->
@modifySelection => @cursor.moveDown()
selectToTop: ->
@modifySelection => @cursor.moveToTop()
selectToBottom: ->
@modifySelection => @cursor.moveToBottom()
selectToBeginningOfLine: ->
@modifySelection => @cursor.moveToBeginningOfLine()
selectToEndOfLine: ->
@modifySelection => @cursor.moveToEndOfLine()
selectToBeginningOfWord: ->
@modifySelection => @cursor.moveToBeginningOfWord()
selectToEndOfWord: ->
@modifySelection => @cursor.moveToEndOfWord()
cut: (maintainPasteboard=false) ->
@copy(maintainPasteboard)
@delete()
copy: (maintainPasteboard=false) ->
return if @isEmpty()
text = @editor.buffer.getTextInRange(@getBufferRange())
text = $native.readFromPasteboard() + "\n" + text if maintainPasteboard
$native.writeToPasteboard text
fold: ->
range = @getBufferRange()
@editor.createFold(range)
@cursor.setBufferPosition(range.end)

View File

@@ -0,0 +1,30 @@
module.exports =
class UndoManager
undoHistory: null
redoHistory: null
preserveHistory: false
constructor: (@buffer) ->
@undoHistory = []
@redoHistory = []
@buffer.on 'change', (op) =>
unless @preserveHistory
@undoHistory.push(op)
@redoHistory = []
undo: ->
if op = @undoHistory.pop()
@preservingHistory =>
@buffer.change op.newRange, op.oldText
@redoHistory.push op
redo: ->
if op = @redoHistory.pop()
@preservingHistory =>
@buffer.change op.oldRange, op.newText
@undoHistory.push op
preservingHistory: (fn) ->
@preserveHistory = true
fn()
@preserveHistory = false

130
src/app/vim-mode.coffee Normal file
View File

@@ -0,0 +1,130 @@
_ = require 'underscore'
$ = require 'jquery'
operators = require 'vim-mode/operators'
commands = require 'vim-mode/commands'
motions = require 'vim-mode/motions'
module.exports =
class VimMode
editor: null
opStack: null
constructor: (@editor) ->
requireStylesheet 'vim-mode.css'
@opStack = []
@activateCommandMode()
window.keymap.bindKeys '.editor', 'escape': 'activate-command-mode'
@editor.on 'activate-command-mode', => @activateCommandMode()
@setupCommandMode()
setupCommandMode: ->
window.keymap.bindKeys '.command-mode', (e) =>
if e.keystroke.match /^\d$/
return 'command-mode:numeric-prefix'
if e.keystroke.match /^.$/
@resetCommandMode()
return false
@bindCommandModeKeys
'i': 'insert'
'd': 'delete'
'x': 'delete-right'
'h': 'move-left'
'j': 'move-down'
'k': 'move-up'
'l': 'move-right'
'w': 'move-to-next-word'
'b': 'move-to-previous-word'
'}': 'move-to-next-paragraph'
'escape': 'reset-command-mode'
'left': 'move-left'
'right': 'move-right'
@handleCommands
'insert': => @activateInsertMode()
'delete': => @delete()
'delete-right': => new commands.DeleteRight(@editor)
'move-left': => new motions.MoveLeft(@editor)
'move-up': => new motions.MoveUp(@editor)
'move-down': => new motions.MoveDown @editor
'move-right': => new motions.MoveRight @editor
'move-to-next-word': => new motions.MoveToNextWord(@editor)
'move-to-previous-word': => new motions.MoveToPreviousWord(@editor)
'move-to-next-paragraph': => new motions.MoveToNextParagraph(@editor)
'numeric-prefix': (e) => @numericPrefix(e)
'reset-command-mode': => @resetCommandMode()
bindCommandModeKeys: (bindings) ->
prefixedBindings = {}
for pattern, commandName of bindings
prefixedBindings[pattern] = "command-mode:#{commandName}"
window.keymap.bindKeys ".command-mode", prefixedBindings
handleCommands: (commands) ->
_.each commands, (fn, commandName) =>
eventName = "command-mode:#{commandName}"
@editor.on eventName, (e) =>
possibleOperator = fn(e)
@pushOperator(possibleOperator) if possibleOperator?.execute
activateInsertMode: ->
@editor.removeClass('command-mode')
@editor.addClass('insert-mode')
@editor.off 'cursor:position-changed', @moveCursorBeforeNewline
activateCommandMode: ->
@editor.removeClass('insert-mode')
@editor.addClass('command-mode')
@editor.on 'cursor:position-changed', @moveCursorBeforeNewline
resetCommandMode: ->
@opStack = []
moveCursorBeforeNewline: =>
if not @editor.getSelection().modifyingSelection and @editor.cursor.isOnEOL() and @editor.getCurrentBufferLine().length > 0
@editor.setCursorBufferColumn(@editor.getCurrentBufferLine().length - 1)
numericPrefix: (e) ->
num = parseInt(e.keyEvent.keystroke)
if @topOperator() instanceof operators.NumericPrefix
@topOperator().addDigit(num)
else
@pushOperator(new operators.NumericPrefix(num))
delete: () ->
if deleteOperation = @isDeletePending()
deleteOperation.complete = true
@processOpStack()
else
@pushOperator(new operators.Delete(@editor))
isDeletePending: () ->
for op in @opStack
return op if op instanceof operators.Delete
false
pushOperator: (op) ->
@opStack.push(op)
@processOpStack()
processOpStack: ->
return unless @topOperator().isComplete()
poppedOperator = @opStack.pop()
if @opStack.length
try
@topOperator().compose(poppedOperator)
@processOpStack()
catch e
(e instanceof operators.OperatorError) and @resetCommandMode() or throw e
else
poppedOperator.execute()
topOperator: ->
_.last @opStack

View File

@@ -0,0 +1,10 @@
class Command
constructor: (@editor) ->
isComplete: -> true
class DeleteRight extends Command
execute: ->
@editor.delete() unless @editor.getCurrentBufferLine().length == 0
module.exports = { DeleteRight }

View File

@@ -0,0 +1,89 @@
Point = require 'point'
getWordRegex = -> /(\w+)|([^\w\s]+)/g
class Motion
constructor: (@editor) ->
isComplete: -> true
class MoveLeft extends Motion
execute: ->
{column, row} = @editor.getCursorScreenPosition()
@editor.moveCursorLeft() if column > 0
select: ->
position = @editor.getCursorScreenPosition().copy()
position.column-- if position.column > 0
@editor.selectToBufferPosition(position)
class MoveRight extends Motion
execute: ->
{column, row} = @editor.getCursorScreenPosition()
@editor.moveCursorRight()
class MoveUp extends Motion
execute: ->
{column, row} = @editor.getCursorScreenPosition()
@editor.moveCursorUp() if row > 0
class MoveDown extends Motion
execute: ->
{column, row} = @editor.getCursorScreenPosition()
@editor.moveCursorDown() if row < (@editor.buffer.numLines() - 1)
class MoveToPreviousWord extends Motion
execute: ->
@editor.getCursor().moveToBeginningOfWord()
select: ->
@editor.getSelection().selectToBeginningOfWord()
class MoveToNextWord extends Motion
execute: ->
@editor.setCursorScreenPosition(@nextWordPosition())
select: ->
@editor.selectToBufferPosition(@nextWordPosition())
nextWordPosition: ->
regex = getWordRegex()
{ row, column } = @editor.getCursorScreenPosition()
rightOfCursor = @editor.buffer.lineForRow(row).substring(column)
match = regex.exec(rightOfCursor)
# If we're on top of part of a word, match the next one.
match = regex.exec(rightOfCursor) if match?.index is 0
if match
column += match.index
else if row + 1 == @editor.buffer.numLines()
column = @editor.buffer.lineForRow(row).length
else
nextLineMatch = regex.exec(@editor.buffer.lineForRow(++row))
column = nextLineMatch?.index or 0
{ row, column }
class MoveToNextParagraph extends Motion
execute: ->
@editor.setCursorScreenPosition(@nextPosition())
select: ->
@editor.selectToPosition(@nextPosition())
nextPosition: ->
regex = /[^\n]\n^$/gm
row = null
column = 0
startRow = @editor.getCursorBufferRow() + 1
for r in [startRow..@editor.buffer.getLastRow()]
if @editor.buffer.lineForRow(r).length == 0
row = r
break
if not row
row = @editor.buffer.getLastRow()
column = @editor.buffer.lastLine().length - 1
new Point(row, column)
module.exports = { Motion, MoveLeft, MoveRight, MoveUp, MoveDown, MoveToNextWord, MoveToPreviousWord, MoveToNextParagraph }

View File

@@ -0,0 +1,57 @@
_ = require 'underscore'
class OperatorError
constructor: (@message) ->
@name = "Operator Error"
class NumericPrefix
count: null
complete: null
operatorToRepeat: null
constructor: (@count) ->
@complete = false
isComplete: -> @complete
compose: (@operatorToRepeat) ->
@complete = true
if @operatorToRepeat.setCount?
@operatorToRepeat.setCount @count
@count = 1
addDigit: (digit) ->
@count = @count * 10 + digit
execute: ->
_.times @count, => @operatorToRepeat.execute()
select: ->
_.times @count, => @operatorToRepeat.select()
class Delete
motion: null
complete: null
constructor: (@editor) ->
@complete = false
isComplete: -> @complete
execute: ->
if @motion
@motion.select()
@editor.getSelection().delete()
else
@editor.buffer.deleteRow(@editor.getCursorBufferRow())
@editor.setCursorScreenPosition([@editor.getCursorScreenRow(), 0])
compose: (motion) ->
if not motion.select
throw new OperatorError("Delete must compose with a motion")
@motion = motion
@complete = true
module.exports = { NumericPrefix, Delete, OperatorError }

68
src/app/window.coffee Normal file
View File

@@ -0,0 +1,68 @@
# This a weirdo file. We don't create a Window class, we just add stuff to
# the DOM window.
fs = require 'fs'
_ = require 'underscore'
$ = require 'jquery'
windowAdditions =
rootViewParentSelector: 'body'
rootView: null
keymap: null
setUpKeymap: ->
Keymap = require 'keymap'
@keymap = new Keymap()
@keymap.bindDefaultKeys()
require(keymapPath) for keymapPath in fs.list(require.resolve("keymaps"))
@_handleKeyEvent = (e) => @keymap.handleKeyEvent(e)
$(document).on 'keydown', @_handleKeyEvent
startup: (path) ->
@attachRootView(path)
@loadUserConfiguration()
$(window).on 'close', => @close()
$(window).focus()
atom.windowOpened this
shutdown: ->
@rootView.remove()
$(window).unbind('focus')
$(window).unbind('blur')
atom.windowClosed this
attachRootView: (path) ->
@rootView = new RootView {path}
$(@rootViewParentSelector).append @rootView
loadUserConfiguration: ->
try
require atom.userConfigurationPath if fs.exists(atom.userConfigurationPath)
catch error
console.error "Failed to load `#{atom.userConfigurationPath}`", error
@showConsole()
requireStylesheet: (path) ->
fullPath = require.resolve(path)
content = fs.read(fullPath)
return if $("head style[path='#{fullPath}']").length
$('head').append "<style path='#{fullPath}'>#{content}</style>"
showConsole: ->
$native.showDevTools()
onerror: ->
@showConsole()
window[key] = value for key, value of windowAdditions
window.setUpKeymap()
RootView = require 'root-view'
require 'jquery-extensions'
require 'underscore-extensions'
requireStylesheet 'reset.css'
requireStylesheet 'atom.css'