WIP: Use rake to start compiling resources (like require.coffee)

This commit is contained in:
Nathan Sobo
2012-08-26 16:29:46 -05:00
parent 72468b7028
commit c3e748a17c
128 changed files with 35213 additions and 7 deletions

120
Rakefile Normal file
View File

@@ -0,0 +1,120 @@
require 'fileutils'
$ATOM_ARGS = []
ENV['PATH'] = "#{ENV['PATH']}:/usr/local/bin/"
BUILD_DIR = 'atom-build'
mkdir_p BUILD_DIR
desc "Build Atom via `xcodebuild`"
task :build => :"verify-prerequisites" do
output = `xcodebuild -scheme atom-release SYMROOT=#{BUILD_DIR}`
if $?.exitstatus != 0
$stderr.puts "Error #{$?.exitstatus}:\n#{output}"
exit($?.exitstatus)
end
end
desc "Clean build Atom via `xcodebuild`"
task :clean do
output = `xcodebuild clean SYMROOT=#{BUILD_DIR}`
rm_rf BUILD_DIR
end
desc "Create the Atom.app for distribution"
task :package => :build do
if path = application_path()
FileUtils.rm_rf "pkg"
FileUtils.mkdir_p "pkg"
FileUtils.cp_r path, "pkg/"
`cd pkg && zip -r atom.zip .`
else
exit(1)
end
end
desc "Run Atom"
task :run => :build do
if path = binary_path()
exitstatus = system "#{path} #{$ATOM_ARGS.join(' ')} 2> /dev/null"
exit(exitstatus)
else
exit(1)
end
end
desc "Run the specs"
task :test => :clean do
$ATOM_ARGS.push "--test", "--headless"
Rake::Task["run"].invoke
end
desc "Run the benchmarks"
task :benchmark do
$ATOM_ARGS.push "--benchmark", "--headless"
Rake::Task["run"].invoke
end
desc "Copy files to bundle and compile CoffeeScripts"
task :"copy-files-to-bundle" => :"verify-prerequisites" do
project_dir = ENV['PROJECT_DIR'] || '.'
built_dir = ENV['BUILT_PRODUCTS_DIR'] || '.'
contents_dir = ENV['CONTENTS_FOLDER_PATH'].to_s
dest = File.join(built_dir, contents_dir, "Resources")
rm_rf File.join(dest, 'static')
cp_r 'static', File.join(dest, 'static')
sh "coffee -c -o #{dest}/src/stdlib src/stdlib/require.coffee"
cp "src/stdlib/onig-reg-exp-extension.js", "#{dest}/src/stdlib"
# unless ENV['LOAD_RESOURCES_FROM_DIR']
# %w(src static vendor spec benchmark bundles themes).each do |dir|
# rm_rf File.join(dest, dir)
# cp_r dir, File.join(dest, dir)
# end
# sh "coffee -c #{dest}/src #{dest}/vendor #{dest}/spec #{dest}/benchmark"
# end
end
desc "Remove any 'fit' or 'fdescribe' focus directives from the specs"
task :nof do
system %{find . -name *spec.coffee | xargs sed -E -i "" "s/f+(it|describe) +(['\\"])/\\1 \\2/g"}
end
task :"verify-prerequisites" do
`hash coffee`
if not $?.success?
abort "error: coffee is required but it's not installed - " +
"http://coffeescript.org/ - (try `npm i -g coffee-script`)"
end
end
def application_path
applications = FileList["#{BUILD_DIR}/**/Atom.app"]
if applications.size == 0
$stderr.puts "No Atom application found in directory `#{BUILD_DIR}`"
elsif applications.size > 1
$stderr.puts "Multiple Atom applications found \n\t" + applications.join("\n\t")
else
return applications.first
end
return nil
end
def binary_path
if app_path = application_path()
binary_path = "#{app_path}/Contents/MacOS/Atom"
if File.exists?(binary_path)
return binary_path
else
$stderr.puts "Executable `#{app_path}` not found."
end
end
return nil
end

View File

@@ -126,12 +126,11 @@
],
'postbuilds': [
{
'postbuild_name': 'Copy static files',
'postbuild_name': 'Copy and Compile Static Files',
'action': [
'cp',
'-r',
'static/',
'${BUILT_PRODUCTS_DIR}/Atom.app/Contents/Resources/',
'rake',
'--trace',
'copy-files-to-bundle',
],
},
{

View File

@@ -47,7 +47,7 @@
window_info.SetAsChild(self.webView, 0, 0, self.webView.bounds.size.width, self.webView.bounds.size.height);
NSURL *url = [[NSBundle mainBundle] resourceURL];
NSString *urlString = [[url URLByAppendingPathComponent:@"index.html"] absoluteString];
NSString *urlString = [[url URLByAppendingPathComponent:@"static/index.html"] absoluteString];
urlString = [urlString stringByAppendingFormat:@"?bootstrapScript=%@.js", _bootstrapScript];
NSLog(@"%@", urlString);

View File

@@ -0,0 +1,28 @@
Range = require 'range'
module.exports =
class AceAdaptor
foldWidgets: {}
constructor: (@editSession) ->
@buffer = @editSession.buffer
getLine: (bufferRow) ->
@buffer.lineForRow(bufferRow)
getLength: ->
@buffer.getLineCount()
$findClosingBracket: (bracketType, bufferPosition) ->
@editSession.tokenizedBuffer.findClosingBracket([bufferPosition.row, bufferPosition.column - 1])
indentRows: (startRow, endRow, indentString) ->
for row in [startRow..endRow]
@buffer.insert([row, 0], indentString)
replace: (range, text) ->
range = Range.fromObject(range)
@buffer.change(range, text)
findMatchingBracket: ({row, column}) ->
@editSession.tokenizedBuffer.findOpeningBracket([row, column])

View File

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

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

@@ -0,0 +1,81 @@
Point = require 'point'
EventEmitter = require 'event-emitter'
_ = require 'underscore'
module.exports =
class Anchor
buffer: null
editSession: null # optional
bufferPosition: null
screenPosition: null
ignoreEqual: false
strong: false
constructor: (@buffer, options = {}) ->
{ @editSession, @ignoreEqual, @strong } = options
handleBufferChange: (e) ->
{ oldRange, newRange } = e
position = @getBufferPosition()
if oldRange.containsPoint(position, exclusive: true)
if @strong
@setBufferPosition(oldRange.start)
else
@destroy()
return
if @ignoreEqual
return if position.isLessThanOrEqual(oldRange.end)
else
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], bufferChange: true)
getBufferPosition: ->
@bufferPosition
setBufferPosition: (position, options={}) ->
@bufferPosition = Point.fromObject(position)
clip = options.clip ? true
@bufferPosition = @buffer.clipPosition(@bufferPosition) if clip
@refreshScreenPosition(options)
getScreenPosition: ->
@screenPosition
setScreenPosition: (position, options={}) ->
previousScreenPosition = @screenPosition
@screenPosition = Point.fromObject(position)
clip = options.clip ? true
assignBufferPosition = options.assignBufferPosition ? true
@screenPosition = @editSession.clipScreenPosition(@screenPosition, options) if clip
@bufferPosition = @editSession.bufferPositionForScreenPosition(@screenPosition, options) if assignBufferPosition
Object.freeze @screenPosition
Object.freeze @bufferPosition
unless @screenPosition.isEqual(previousScreenPosition)
@trigger 'change-screen-position', @screenPosition, bufferChange: options.bufferChange
refreshScreenPosition: (options={}) ->
return unless @editSession
screenPosition = @editSession.screenPositionForBufferPosition(@bufferPosition, options)
@setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false)
destroy: ->
@buffer.removeAnchor(this)
@editSession?.removeAnchor(this)
@trigger 'destroy'
@off()
_.extend(Anchor.prototype, EventEmitter)

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

@@ -0,0 +1,39 @@
Keymap = require 'keymap'
fs = require 'fs'
$ = require 'jquery'
_ = require 'underscore'
require 'underscore-extensions'
module.exports =
class Atom
keymap: null
windows: null
configFilePath: null
configDirPath: null
rootViewStates: null
constructor: (@loadPath, nativeMethods) ->
@windows = []
@setUpKeymap()
@configDirPath = fs.absolute("~/.atom")
@configFilePath = fs.join(@configDirPath, "atom.coffee")
@rootViewStates = {}
setUpKeymap: ->
@keymap = new Keymap()
@handleKeyEvent = (e) => @keymap.handleKeyEvent(e)
$(document).on 'keydown', @handleKeyEvent
@keymap.bindDefaultKeys()
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,52 @@
$ = require 'jquery'
_ = require 'underscore'
fs = require 'fs'
Specificity = require 'specificity'
PEG = require 'pegjs'
module.exports =
class BindingSet
selector: null
commandsByKeystrokes: null
commandForEvent: null
parser: null
constructor: (@selector, mapOrFunction, @index) ->
@parser = PEG.buildParser(fs.read(require.resolve 'keystroke-pattern.pegjs'))
@specificity = Specificity(@selector)
@commandsByKeystrokes = {}
if _.isFunction(mapOrFunction)
@commandForEvent = mapOrFunction
else
@commandsByKeystrokes = @normalizeCommandsByKeystrokes(mapOrFunction)
@commandForEvent = (event) =>
for keystrokes, command of @commandsByKeystrokes
return command if event.keystrokes == keystrokes
null
matchesKeystrokePrefix: (event) ->
eventKeystrokes = event.keystrokes.split(' ')
for keystrokes, command of @commandsByKeystrokes
bindingKeystrokes = keystrokes.split(' ')
continue unless eventKeystrokes.length < bindingKeystrokes.length
return true if _.isEqual(eventKeystrokes, bindingKeystrokes[0...eventKeystrokes.length])
false
normalizeCommandsByKeystrokes: (commandsByKeystrokes) ->
normalizedCommandsByKeystrokes = {}
for keystrokes, command of commandsByKeystrokes
normalizedCommandsByKeystrokes[@normalizeKeystrokes(keystrokes)] = command
normalizedCommandsByKeystrokes
normalizeKeystrokes: (keystrokes) ->
normalizedKeystrokes = keystrokes.split(/\s+/).map (keystroke) =>
@normalizeKeystroke(keystroke)
normalizedKeystrokes.join(' ')
normalizeKeystroke: (keystroke) ->
keys = @parser.parse(keystroke)
modifiers = keys[0...-1]
modifiers.sort()
[modifiers..., _.last(keys)].join('-')

View File

@@ -0,0 +1,57 @@
Range = require 'range'
module.exports =
class BufferChangeOperation
buffer: null
oldRange: null
oldText: null
newRange: null
newText: null
constructor: ({@buffer, @oldRange, @newText}) ->
do: ->
@oldText = @buffer.getTextInRange(@oldRange)
@newRange = @calculateNewRange(@oldRange, @newText)
@changeBuffer
oldRange: @oldRange
newRange: @newRange
oldText: @oldText
newText: @newText
undo: ->
@changeBuffer
oldRange: @newRange
newRange: @oldRange
oldText: @newText
newText: @oldText
changeBuffer: ({ oldRange, newRange, newText, oldText }) ->
{ prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange)
newTextLines = newText.split('\n')
if newTextLines.length == 1
newTextLines = [prefix + newText + suffix]
else
lastLineIndex = newTextLines.length - 1
newTextLines[0] = prefix + newTextLines[0]
newTextLines[lastLineIndex] += suffix
@buffer.replaceLines(oldRange.start.row, oldRange.end.row, newTextLines)
event = { oldRange, newRange, oldText, newText }
@buffer.trigger 'change', event
anchor.handleBufferChange(event) for anchor in @buffer.getAnchors()
@buffer.trigger 'update-anchors-after-change'
newRange
calculateNewRange: (oldRange, newText) ->
newRange = new Range(oldRange.start.copy(), oldRange.start.copy())
newTextLines = newText.split('\n')
if newTextLines.length == 1
newRange.end.column += newText.length
else
lastLineIndex = newTextLines.length - 1
newRange.end.row += lastLineIndex
newRange.end.column = newTextLines[lastLineIndex].length
newRange

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

@@ -0,0 +1,357 @@
_ = require 'underscore'
fs = require 'fs'
File = require 'file'
Point = require 'point'
Range = require 'range'
EventEmitter = require 'event-emitter'
UndoManager = require 'undo-manager'
BufferChangeOperation = require 'buffer-change-operation'
Anchor = require 'anchor'
AnchorRange = require 'anchor-range'
module.exports =
class Buffer
@idCounter = 1
undoManager: null
modified: null
modifiedOnDisk: null
lines: null
file: null
anchors: null
anchorRanges: null
refcount: 0
constructor: (path, @project) ->
@id = @constructor.idCounter++
@anchors = []
@anchorRanges = []
@lines = ['']
if path
throw "Path '#{path}' does not exist" unless fs.exists(path)
@setPath(path)
@setText(fs.read(@getPath()))
else
@setText('')
@undoManager = new UndoManager(this)
@modified = false
destroy: ->
throw new Error("Destroying buffer twice with path '#{@getPath()}'") if @destroyed
@file?.off()
@destroyed = true
@project?.removeBuffer(this)
retain: ->
@refcount++
this
release: ->
@refcount--
@destroy() if @refcount <= 0
this
subscribeToFile: ->
@file.on "contents-change", =>
if @isModified()
@modifiedOnDisk = true
else
@setText(fs.read(@file.getPath()))
@modified = false
@file.on "remove", =>
@file = null
@trigger "path-change", this
@file.on "move", =>
@trigger "path-change", this
reload: ->
@setText(fs.read(@file.getPath()))
@modified = false
@modifiedOnDisk = false
getBaseName: ->
@file?.getBaseName()
getPath: ->
@file?.getPath()
setPath: (path) ->
return if path == @getPath()
@file?.off()
@file = new File(path)
@subscribeToFile()
@file.on "contents-change", =>
if @isModified()
@modifiedOnDisk = true
@trigger "contents-change-on-disk"
else
@setText(fs.read(@file.getPath()))
@modified = false
@trigger "path-change", this
getExtension: ->
if @getPath()
@getPath().split('/').pop().split('.').pop()
else
null
getText: ->
@lines.join('\n')
setText: (text) ->
@change(@getRange(), text)
getRange: ->
new Range([0, 0], [@getLastRow(), @getLastLine().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)])
getLineCount: ->
@getLines().length
getLastRow: ->
@getLines().length - 1
getLastLine: ->
@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)
operation = new BufferChangeOperation({buffer: this, oldRange, newText})
@pushOperation(operation)
clipPosition: (position) ->
{ row, column } = Point.fromObject(position)
row = 0 if row < 0
column = 0 if column < 0
row = Math.min(@getLastRow(), row)
column = Math.min(@lineLengthForRow(row), column)
new Point(row, column)
prefixAndSuffixForRange: (range) ->
prefix: @lines[range.start.row][0...range.start.column]
suffix: @lines[range.end.row][range.end.column..]
replaceLines: (startRow, endRow, newLines) ->
@lines[startRow..endRow] = newLines
@modified = true
pushOperation: (operation, editSession) ->
if @undoManager
@undoManager.pushOperation(operation, editSession)
else
operation.do()
transact: (fn) ->
@undoManager.transact(fn)
undo: (editSession) ->
@undoManager.undo(editSession)
redo: (editSession) ->
@undoManager.redo(editSession)
save: ->
@saveAs(@getPath())
saveAs: (path) ->
if not path then throw new Error("Can't save buffer with no file path")
@trigger 'before-save'
fs.write path, @getText()
@file?.updateMd5()
@modified = false
@modifiedOnDisk = false
@setPath(path)
@trigger 'after-save'
isInConflict: ->
@isModified() and @isModifiedOnDisk()
isModifiedOnDisk: ->
@modifiedOnDisk
isModified: ->
@modified
getAnchors: -> new Array(@anchors...)
addAnchor: (options) ->
anchor = new Anchor(this, options)
@anchors.push(anchor)
anchor
addAnchorAtPosition: (position, options) ->
anchor = @addAnchor(options)
anchor.setBufferPosition(position)
anchor
addAnchorRange: (range, editSession) ->
anchorRange = new AnchorRange(range, this, editSession)
@anchorRanges.push(anchorRange)
anchorRange
removeAnchor: (anchor) ->
_.remove(@anchors, anchor)
removeAnchorRange: (anchorRange) ->
_.remove(@anchorRanges, anchorRange)
matchesInCharacterRange: (regex, startIndex, endIndex) ->
text = @getText()
matches = []
regex.lastIndex = startIndex
while match = regex.exec(text)
matchLength = match[0].length
matchStartIndex = match.index
matchEndIndex = matchStartIndex + matchLength
if matchEndIndex > endIndex
regex.lastIndex = 0
if matchStartIndex < endIndex and submatch = regex.exec(text[matchStartIndex...endIndex])
submatch.index = matchStartIndex
matches.push submatch
break
matchEndIndex++ if matchLength is 0
regex.lastIndex = matchEndIndex
matches.push match
matches
scan: (regex, iterator) ->
@scanInRange(regex, @getRange(), iterator)
scanInRange: (regex, range, iterator, reverse=false) ->
range = Range.fromObject(range)
global = regex.global
regex = new RegExp(regex.source, 'gm')
startIndex = @characterIndexForPosition(range.start)
endIndex = @characterIndexForPosition(range.end)
matches = @matchesInCharacterRange(regex, startIndex, endIndex)
lengthDelta = 0
keepLooping = null
replacementText = null
stop = -> keepLooping = false
replace = (text) -> replacementText = text
matches.reverse() if reverse
for match in matches
matchLength = match[0].length
matchStartIndex = match.index
matchEndIndex = matchStartIndex + matchLength
startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta)
endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta)
range = new Range(startPosition, endPosition)
keepLooping = true
replacementText = null
iterator(match, range, { stop, replace })
if replacementText?
@change(range, replacementText)
lengthDelta += replacementText.length - matchLength unless reverse
break unless global and keepLooping
backwardsScanInRange: (regex, range, iterator) ->
@scanInRange regex, range, iterator, true
isRowBlank: (row) ->
not /\S/.test @lineForRow(row)
previousNonBlankRow: (startRow) ->
return null if startRow == 0
startRow = Math.min(startRow, @getLastRow())
for row in [(startRow - 1)..0]
return row unless @isRowBlank(row)
null
nextNonBlankRow: (startRow) ->
lastRow = @getLastRow()
if startRow < lastRow
for row in [(startRow + 1)..lastRow]
return row unless @isRowBlank(row)
null
indentationForRow: (row) ->
@lineForRow(row).match(/^\s*/)?[0].length
setIndentationForRow: (bufferRow, newLevel) ->
currentLevel = @indentationForRow(bufferRow)
indentString = [0...newLevel].map(-> ' ').join('')
@change([[bufferRow, 0], [bufferRow, currentLevel]], indentString)
logLines: (start=0, end=@getLastRow())->
for row in [start..end]
line = @lineForRow(row)
console.log row, line, line.length
_.extend(Buffer.prototype, EventEmitter)

View File

@@ -0,0 +1,62 @@
{View} = require 'space-pen'
Anchor = require 'anchor'
Point = require 'point'
Range = require 'range'
_ = require 'underscore'
module.exports =
class CursorView extends View
@content: ->
@pre class: 'cursor idle', => @raw '&nbsp;'
editor: null
hidden: false
initialize: (@cursor, @editor) ->
@cursor.on 'change-screen-position.cursor-view', (position, { bufferChange }) =>
@updateAppearance()
@removeIdleClassTemporarily() unless bufferChange
@trigger 'cursor-move', bufferChange: bufferChange
@cursor.on 'destroy.cursor-view', => @remove()
afterAttach: (onDom) ->
return unless onDom
@updateAppearance()
@editor.syncCursorAnimations()
remove: ->
@editor.removeCursorView(this)
@cursor.off('.cursor-view')
super
updateAppearance: ->
screenPosition = @getScreenPosition()
pixelPosition = @editor.pixelPositionForScreenPosition(screenPosition)
@css(pixelPosition)
if @cursor == @editor.getLastCursor()
@editor.scrollTo(pixelPosition)
if @editor.isFoldedAtScreenRow(screenPosition.row)
@hide() unless @hidden
@hidden = true
else
@show() if @hidden
@hidden = false
getBufferPosition: ->
@cursor.getBufferPosition()
getScreenPosition: ->
@cursor.getScreenPosition()
removeIdleClassTemporarily: ->
@removeClass 'idle'
window.clearTimeout(@idleTimeout) if @idleTimeout
@idleTimeout = window.setTimeout (=> @addClass 'idle'), 200
resetCursorAnimation: ->
window.clearTimeout(@idleTimeout) if @idleTimeout
@removeClass 'idle'
_.defer => @addClass 'idle'

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

@@ -0,0 +1,162 @@
Point = require 'point'
Range = require 'range'
Anchor = require 'anchor'
EventEmitter = require 'event-emitter'
_ = require 'underscore'
module.exports =
class Cursor
screenPosition: null
bufferPosition: null
goalColumn: null
wordRegex: /(\w+)|([^\w\s]+)/g
constructor: ({@editSession, screenPosition, bufferPosition}) ->
@anchor = @editSession.addAnchor(strong: true)
@anchor.on 'change-screen-position', (args...) => @trigger 'change-screen-position', args...
@setScreenPosition(screenPosition) if screenPosition
@setBufferPosition(bufferPosition) if bufferPosition
destroy: ->
@anchor.destroy()
@editSession.removeCursor(this)
@trigger 'destroy'
setScreenPosition: (screenPosition, options) ->
@goalColumn = null
@clearSelection()
@anchor.setScreenPosition(screenPosition, options)
getScreenPosition: ->
@anchor.getScreenPosition()
setBufferPosition: (bufferPosition, options) ->
@goalColumn = null
@clearSelection()
@anchor.setBufferPosition(bufferPosition, options)
getBufferPosition: ->
@anchor.getBufferPosition()
clearSelection: ->
if @selection
@selection.clear() unless @selection.retainSelection
getScreenRow: ->
@getScreenPosition().row
getBufferRow: ->
@getBufferPosition().row
getCurrentBufferLine: ->
@editSession.lineForBufferRow(@getBufferRow())
refreshScreenPosition: ->
@anchor.refreshScreenPosition()
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
moveLeft: ->
{ row, column } = @getScreenPosition()
[row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity]
@setScreenPosition({row, column})
moveRight: ->
{ row, column } = @getScreenPosition()
@setScreenPosition([row, column + 1], skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true)
moveToTop: ->
@setBufferPosition([0,0])
moveToBottom: ->
@setBufferPosition(@editSession.getEofBufferPosition())
moveToBeginningOfLine: ->
@setBufferPosition([@getBufferRow(), 0])
moveToFirstCharacterOfLine: ->
position = @getBufferPosition()
range = @editSession.bufferRangeForBufferRow(position.row)
newPosition = null
@editSession.scanInRange /^\s*/, range, (match, matchRange) =>
newPosition = matchRange.end
return unless newPosition
newPosition = [position.row, 0] if newPosition.isEqual(position)
@setBufferPosition(newPosition)
moveToEndOfLine: ->
@setBufferPosition([@getBufferRow(), Infinity])
moveToBeginningOfWord: ->
@setBufferPosition(@getBeginningOfCurrentWordBufferPosition())
moveToEndOfWord: ->
@setBufferPosition(@getEndOfCurrentWordBufferPosition())
moveToNextWord: ->
@setBufferPosition(@getBeginningOfNextWordBufferPosition())
getBeginningOfCurrentWordBufferPosition: (options = {}) ->
allowPrevious = options.allowPrevious ? true
currentBufferPosition = @getBufferPosition()
previousRow = Math.max(0, currentBufferPosition.row - 1)
previousLinesRange = [[previousRow, 0], currentBufferPosition]
beginningOfWordPosition = currentBufferPosition
@editSession.backwardsScanInRange @wordRegex, previousLinesRange, (match, matchRange, { stop }) =>
if matchRange.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
beginningOfWordPosition = matchRange.start
stop()
beginningOfWordPosition
getEndOfCurrentWordBufferPosition: (options = {}) ->
allowNext = options.allowNext ? true
currentBufferPosition = @getBufferPosition()
range = [currentBufferPosition, @editSession.getEofBufferPosition()]
endOfWordPosition = null
@editSession.scanInRange @wordRegex, range, (match, matchRange, { stop }) =>
endOfWordPosition = matchRange.end
if not allowNext and matchRange.start.isGreaterThan(currentBufferPosition)
endOfWordPosition = currentBufferPosition
stop()
endOfWordPosition
getBeginningOfNextWordBufferPosition: ->
currentBufferPosition = @getBufferPosition()
eofBufferPosition = @editSession.getEofBufferPosition()
range = [currentBufferPosition, eofBufferPosition]
nextWordPosition = eofBufferPosition
@editSession.scanInRange @wordRegex, range, (match, matchRange, { stop }) =>
if matchRange.start.isGreaterThan(currentBufferPosition)
nextWordPosition = matchRange.start
stop()
nextWordPosition
getCurrentWordBufferRange: ->
new Range(@getBeginningOfCurrentWordBufferPosition(allowPrevious: false), @getEndOfCurrentWordBufferPosition(allowNext: false))
getCurrentLineBufferRange: ->
@editSession.bufferRangeForBufferRow(@getBufferRow())
getCurrentWordPrefix: ->
@editSession.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])
isAtBeginningOfLine: ->
@getBufferPosition().column == 0
isAtEndOfLine: ->
@getBufferPosition().isEqual(@getCurrentLineBufferRange().end)
_.extend Cursor.prototype, EventEmitter

38
src/app/directory.coffee Normal file
View File

@@ -0,0 +1,38 @@
_ = require 'underscore'
fs = require 'fs'
File = require 'file'
EventEmitter = require 'event-emitter'
module.exports =
class Directory
path: null
constructor: (@path) ->
getBaseName: ->
fs.base(@path) + '/'
getEntries: ->
directories = []
files = []
for path in fs.list(@path)
if fs.isDirectory(path)
directories.push(new Directory(path))
else
files.push(new File(path))
directories.concat(files)
afterSubscribe: ->
@subscribeToNativeChangeEvents() if @subscriptionCount() == 1
afterUnsubscribe: ->
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
subscribeToNativeChangeEvents: ->
@watchId = $native.watchPath @path, (eventType) =>
@trigger "contents-change" if eventType is "contents-change"
unsubscribeFromNativeChangeEvents: ->
$native.unwatchPath(@path, @watchId)
_.extend Directory.prototype, EventEmitter

View File

@@ -0,0 +1,281 @@
_ = require 'underscore'
TokenizedBuffer = require 'tokenized-buffer'
LineMap = require 'line-map'
Point = require 'point'
EventEmitter = require 'event-emitter'
Range = require 'range'
Fold = require 'fold'
ScreenLine = require 'screen-line'
Token = require 'token'
module.exports =
class DisplayBuffer
@idCounter: 1
lineMap: null
languageMode: null
tokenizedBuffer: null
activeFolds: null
foldsById: null
lastTokenizedBufferChangeEvent: null
constructor: (@buffer, options={}) ->
@id = @constructor.idCounter++
options.tabText ?= ' '
@languageMode = options.languageMode
@tokenizedBuffer = new TokenizedBuffer(@buffer, options)
@softWrapColumn = options.softWrapColumn ? Infinity
@activeFolds = {}
@foldsById = {}
@buildLineMap()
@tokenizedBuffer.on 'change', (e) => @lastTokenizedBufferChangeEvent = e
@buffer.on "change.displayBuffer#{@id}", (e) => @handleBufferChange(e)
buildLineMap: ->
@lineMap = new LineMap
@lineMap.insertAtBufferRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow())
setSoftWrapColumn: (@softWrapColumn) ->
oldRange = @rangeForAllLines()
@buildLineMap()
newRange = @rangeForAllLines()
@trigger 'change', { oldRange, newRange, lineNumbersChanged: true }
lineForRow: (row) ->
@lineMap.lineForScreenRow(row)
linesForRows: (startRow, endRow) ->
@lineMap.linesForScreenRows(startRow, endRow)
getLines: ->
@lineMap.linesForScreenRows(0, @lineMap.lastScreenRow())
bufferRowsForScreenRows: (startRow, endRow) ->
@lineMap.bufferRowsForScreenRows(startRow, endRow)
foldAll: ->
for currentRow in [0..@buffer.getLastRow()]
[startRow, endRow] = @languageMode.rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
@createFold(startRow, endRow)
unfoldAll: ->
for row in [@buffer.getLastRow()..0]
@activeFolds[row]?.forEach (fold) => @destroyFold(fold)
foldBufferRow: (bufferRow) ->
for currentRow in [bufferRow..0]
[startRow, endRow] = @languageMode.rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow? and startRow <= bufferRow <= endRow
fold = @largestFoldStartingAtBufferRow(startRow)
continue if fold
@createFold(startRow, endRow)
return
unfoldBufferRow: (bufferRow) ->
@largestFoldContainingBufferRow(bufferRow)?.destroy()
createFold: (startRow, endRow) ->
return fold if fold = @foldFor(startRow, endRow)
fold = new Fold(this, startRow, endRow)
@registerFold(fold)
unless @isFoldContainedByActiveFold(fold)
bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)])
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
lines = @buildLineForBufferRow(startRow)
@lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines)
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true
fold
isFoldContainedByActiveFold: (fold) ->
for row, folds of @activeFolds
for otherFold in folds
return otherFold if fold != otherFold and fold.isContainedByFold(otherFold)
foldFor: (startRow, endRow) ->
_.find @activeFolds[startRow] ? [], (fold) ->
fold.startRow == startRow and fold.endRow == endRow
destroyFold: (fold) ->
@unregisterFold(fold.startRow, fold)
unless @isFoldContainedByActiveFold(fold)
{ startRow, endRow } = fold
bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)])
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
lines = @buildLinesForBufferRows(startRow, endRow)
@lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines)
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true
destroyFoldsContainingBufferRow: (bufferRow) ->
for row, folds of @activeFolds
for fold in new Array(folds...)
fold.destroy() if fold.getBufferRange().containsRow(bufferRow)
registerFold: (fold) ->
@activeFolds[fold.startRow] ?= []
@activeFolds[fold.startRow].push(fold)
@foldsById[fold.id] = fold
unregisterFold: (bufferRow, fold) ->
folds = @activeFolds[bufferRow]
_.remove(folds, fold)
delete @foldsById[fold.id]
delete @activeFolds[bufferRow] if folds.length == 0
largestFoldStartingAtBufferRow: (bufferRow) ->
return unless folds = @activeFolds[bufferRow]
(folds.sort (a, b) -> b.endRow - a.endRow)[0]
largestFoldStartingAtScreenRow: (screenRow) ->
@largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow))
largestFoldContainingBufferRow: (bufferRow) ->
largestFold = null
for currentBufferRow in [bufferRow..0]
if fold = @largestFoldStartingAtBufferRow(currentBufferRow)
largestFold = fold if fold.endRow >= bufferRow
largestFold
screenLineRangeForBufferRange: (bufferRange) ->
@expandScreenRangeToLineEnds(
@lineMap.screenRangeForBufferRange(
@expandBufferRangeToLineEnds(bufferRange)))
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()
getLastRow: ->
@lineCount() - 1
maxLineLength: ->
@lineMap.maxScreenLineLength()
screenPositionForBufferPosition: (position, options) ->
@lineMap.screenPositionForBufferPosition(position, options)
bufferPositionForScreenPosition: (position, options) ->
@lineMap.bufferPositionForScreenPosition(position, options)
stateForScreenRow: (screenRow) ->
@tokenizedBuffer.stackForRow(screenRow)
clipScreenPosition: (position, options) ->
@lineMap.clipScreenPosition(position, options)
handleBufferChange: (e) ->
allFolds = [] # Folds can modify @activeFolds, so first make sure we have a stable array of folds
allFolds.push(folds...) for row, folds of @activeFolds
fold.handleBufferChange(e) for fold in allFolds
@handleTokenizedBufferChange(@lastTokenizedBufferChangeEvent)
handleTokenizedBufferChange: (e) ->
newRange = e.newRange.copy()
newRange.start.row = @bufferRowForScreenRow(@screenRowForBufferRow(newRange.start.row))
oldScreenRange = @screenLineRangeForBufferRange(e.oldRange)
newScreenLines = @buildLinesForBufferRows(newRange.start.row, newRange.end.row)
@lineMap.replaceScreenRows oldScreenRange.start.row, oldScreenRange.end.row, newScreenLines
newScreenRange = @screenLineRangeForBufferRange(newRange)
@trigger 'change',
oldRange: oldScreenRange
newRange: newScreenRange
bufferChanged: true
lineNumbersChanged: !e.oldRange.coversSameRows(newRange) or !oldScreenRange.coversSameRows(newScreenRange)
buildLineForBufferRow: (bufferRow) ->
@buildLinesForBufferRows(bufferRow, bufferRow)
buildLinesForBufferRows: (startBufferRow, endBufferRow) ->
lineFragments = []
startBufferColumn = null
currentBufferRow = startBufferRow
currentScreenLineLength = 0
startBufferColumn = 0
while currentBufferRow <= endBufferRow
screenLine = @tokenizedBuffer.lineForScreenRow(currentBufferRow)
screenLine.foldable = @languageMode.doesBufferRowStartFold(currentBufferRow)
if fold = @largestFoldStartingAtBufferRow(currentBufferRow)
screenLine = screenLine.copy()
screenLine.fold = fold
screenLine.bufferDelta = fold.getBufferDelta()
lineFragments.push(screenLine)
currentBufferRow = fold.endRow + 1
continue
startBufferColumn ?= 0
screenLine = screenLine.splitAt(startBufferColumn)[1] if startBufferColumn > 0
wrapScreenColumn = @findWrapColumn(screenLine.text, @softWrapColumn)
if wrapScreenColumn?
screenLine = screenLine.splitAt(wrapScreenColumn)[0]
screenLine.screenDelta = new Point(1, 0)
startBufferColumn += wrapScreenColumn
else
currentBufferRow++
startBufferColumn = 0
lineFragments.push(screenLine)
lineFragments
findWrapColumn: (line, softWrapColumn) ->
return unless line.length > softWrapColumn
if /\s/.test(line[softWrapColumn])
# search forward for the start of a word past the boundary
for column in [softWrapColumn..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 [softWrapColumn..0]
return column + 1 if /\s/.test(line[column])
return softWrapColumn
expandScreenRangeToLineEnds: (screenRange) ->
screenRange = Range.fromObject(screenRange)
{ start, end } = screenRange
new Range([start.row, 0], [end.row, @lineMap.lineForScreenRow(end.row).text.length])
expandBufferRangeToLineEnds: (bufferRange) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
new Range([start.row, 0], [end.row, Infinity])
rangeForAllLines: ->
new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
destroy: ->
@tokenizedBuffer.destroy()
@buffer.off ".displayBuffer#{@id}"
logLines: (start, end) ->
@lineMap.logLines(start, end)
_.extend DisplayBuffer.prototype, EventEmitter

545
src/app/edit-session.coffee Normal file
View File

@@ -0,0 +1,545 @@
Point = require 'point'
Buffer = require 'buffer'
Anchor = require 'anchor'
LanguageMode = require 'language-mode'
DisplayBuffer = require 'display-buffer'
Cursor = require 'cursor'
Selection = require 'selection'
EventEmitter = require 'event-emitter'
Range = require 'range'
AnchorRange = require 'anchor-range'
_ = require 'underscore'
module.exports =
class EditSession
@idCounter: 1
@deserialize: (state, project) ->
session = project.buildEditSessionForPath(state.buffer)
session.setScrollTop(state.scrollTop)
session.setScrollLeft(state.scrollLeft)
session.setCursorScreenPosition(state.cursorScreenPosition)
session
scrollTop: 0
scrollLeft: 0
languageMode: null
displayBuffer: null
anchors: null
anchorRanges: null
cursors: null
selections: null
autoIndent: false # TODO: re-enabled auto-indent after fixing the rest of tokenization
softTabs: true
softWrap: false
constructor: ({@project, @buffer, @tabText, @autoIndent, @softTabs, @softWrap}) ->
@id = @constructor.idCounter++
@softTabs ?= true
@languageMode = new LanguageMode(this, @buffer.getExtension())
@displayBuffer = new DisplayBuffer(@buffer, { @languageMode, @tabText })
@tokenizedBuffer = @displayBuffer.tokenizedBuffer
@anchors = []
@anchorRanges = []
@cursors = []
@selections = []
@addCursorAtScreenPosition([0, 0])
@buffer.retain()
@buffer.on "path-change.edit-session-#{@id}", =>
@trigger "buffer-path-change"
@buffer.on "contents-change-on-disk.edit-session-#{@id}", =>
@trigger "buffer-contents-change-on-disk"
@buffer.on "update-anchors-after-change.edit-session-#{@id}", =>
@mergeCursors()
@displayBuffer.on "change.edit-session-#{@id}", (e) =>
@trigger 'screen-lines-change', e
unless e.bufferChanged
anchor.refreshScreenPosition() for anchor in @getAnchors()
destroy: ->
throw new Error("Edit session already destroyed") if @destroyed
@destroyed = true
@buffer.off ".edit-session-#{@id}"
@buffer.release()
@displayBuffer.off ".edit-session-#{@id}"
@displayBuffer.destroy()
@project.removeEditSession(this)
anchor.destroy() for anchor in @getAnchors()
anchorRange.destroy() for anchorRange in @getAnchorRanges()
serialize: ->
buffer: @buffer.getPath()
scrollTop: @getScrollTop()
scrollLeft: @getScrollLeft()
cursorScreenPosition: @getCursorScreenPosition().serialize()
copy: ->
EditSession.deserialize(@serialize(), @project)
isEqual: (other) ->
return false unless other instanceof EditSession
@buffer == other.buffer and
@scrollTop == other.getScrollTop() and
@scrollLeft == other.getScrollLeft() and
@getCursorScreenPosition().isEqual(other.getCursorScreenPosition())
setScrollTop: (@scrollTop) ->
getScrollTop: -> @scrollTop
setScrollLeft: (@scrollLeft) ->
getScrollLeft: -> @scrollLeft
setSoftWrapColumn: (@softWrapColumn) -> @displayBuffer.setSoftWrapColumn(@softWrapColumn)
setAutoIndent: (@autoIndent) ->
setSoftTabs: (@softTabs) ->
getSoftWrap: -> @softWrap
setSoftWrap: (@softWrap) ->
clipBufferPosition: (bufferPosition) ->
@buffer.clipPosition(bufferPosition)
getFileExtension: -> @buffer.getExtension()
getPath: -> @buffer.getPath()
isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow)
nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow)
indentationForBufferRow: (bufferRow) -> @buffer.indentationForRow(bufferRow)
getEofBufferPosition: -> @buffer.getEofPosition()
getLastBufferRow: -> @buffer.getLastRow()
bufferRangeForBufferRow: (row) -> @buffer.rangeForRow(row)
lineForBufferRow: (row) -> @buffer.lineForRow(row)
scanInRange: (args...) -> @buffer.scanInRange(args...)
backwardsScanInRange: (args...) -> @buffer.backwardsScanInRange(args...)
screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options)
bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options)
screenRangeForBufferRange: (range) -> @displayBuffer.screenRangeForBufferRange(range)
bufferRangeForScreenRange: (range) -> @displayBuffer.bufferRangeForScreenRange(range)
clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options)
lineForScreenRow: (row) -> @displayBuffer.lineForRow(row)
linesForScreenRows: (start, end) -> @displayBuffer.linesForRows(start, end)
stateForScreenRow: (screenRow) -> @displayBuffer.stateForScreenRow(screenRow)
screenLineCount: -> @displayBuffer.lineCount()
maxScreenLineLength: -> @displayBuffer.maxLineLength()
getLastScreenRow: -> @displayBuffer.getLastRow()
bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)
logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)
insertText: (text, options) ->
@mutateSelectedText (selection) -> selection.insertText(text, options)
insertNewline: ->
@insertText('\n')
insertNewlineBelow: ->
@moveCursorToEndOfLine()
@insertNewline()
indent: ->
currentRow = @getCursorBufferPosition().row
if @getSelection().isEmpty()
if @softTabs
@insertText(@tabText)
else
@insertText('\t')
else
@indentSelectedRows()
backspace: ->
@mutateSelectedText (selection) -> selection.backspace()
backspaceToBeginningOfWord: ->
@mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord()
delete: ->
@mutateSelectedText (selection) -> selection.delete()
deleteToEndOfWord: ->
@mutateSelectedText (selection) -> selection.deleteToEndOfWord()
indentSelectedRows: ->
@mutateSelectedText (selection) -> selection.indentSelectedRows()
outdentSelectedRows: ->
@mutateSelectedText (selection) -> selection.outdentSelectedRows()
toggleLineCommentsInSelection: ->
@mutateSelectedText (selection) -> selection.toggleLineComments()
cutToEndOfLine: ->
maintainPasteboard = false
@mutateSelectedText (selection) ->
selection.cutToEndOfLine(maintainPasteboard)
maintainPasteboard = true
cutSelectedText: ->
maintainPasteboard = false
@mutateSelectedText (selection) ->
selection.cut(maintainPasteboard)
maintainPasteboard = true
copySelectedText: ->
maintainPasteboard = false
for selection in @getSelections()
selection.copy(maintainPasteboard)
maintainPasteboard = true
pasteText: ->
@insertText($native.readFromPasteboard())
undo: ->
@buffer.undo(this)
redo: ->
@buffer.redo(this)
foldAll: ->
@displayBuffer.foldAll()
unfoldAll: ->
@displayBuffer.unfoldAll()
foldCurrentRow: ->
bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
@foldBufferRow(bufferRow)
foldBufferRow: (bufferRow) ->
@displayBuffer.foldBufferRow(bufferRow)
unfoldCurrentRow: ->
bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
@unfoldBufferRow(bufferRow)
unfoldBufferRow: (bufferRow) ->
@displayBuffer.unfoldBufferRow(bufferRow)
foldSelection: ->
selection.fold() for selection in @getSelections()
createFold: (startRow, endRow) ->
@displayBuffer.createFold(startRow, endRow)
destroyFoldsContainingBufferRow: (bufferRow) ->
@displayBuffer.destroyFoldsContainingBufferRow(bufferRow)
destroyFoldsIntersectingBufferRange: (bufferRange) ->
for row in [bufferRange.start.row..bufferRange.end.row]
@destroyFoldsContainingBufferRow(row)
destroyFold: (foldId) ->
fold = @displayBuffer.foldsById[foldId]
fold.destroy()
@setCursorBufferPosition([fold.startRow, 0])
isFoldedAtScreenRow: (screenRow) ->
@lineForScreenRow(screenRow).fold?
largestFoldContainingBufferRow: (bufferRow) ->
@displayBuffer.largestFoldContainingBufferRow(bufferRow)
largestFoldStartingAtScreenRow: (screenRow) ->
@displayBuffer.largestFoldStartingAtScreenRow(screenRow)
autoIndentBufferRows: (startRow, endRow) ->
@languageMode.autoIndentBufferRows(startRow, endRow)
autoIndentBufferRow: (bufferRow) ->
@languageMode.autoIndentBufferRow(bufferRow)
autoIncreaseIndentForBufferRow: (bufferRow) ->
@languageMode.autoIncreaseIndentForBufferRow(bufferRow)
autoDecreaseIndentForRow: (bufferRow) ->
@languageMode.autoDecreaseIndentForBufferRow(bufferRow)
toggleLineCommentsInRange: (range) ->
@languageMode.toggleLineCommentsInRange(range)
mutateSelectedText: (fn) ->
@transact => fn(selection) for selection in @getSelections()
transact: (fn) ->
@buffer.transact =>
oldSelectedRanges = @getSelectedBufferRanges()
@pushOperation
undo: (editSession) ->
editSession?.setSelectedBufferRanges(oldSelectedRanges)
fn()
newSelectedRanges = @getSelectedBufferRanges()
@pushOperation
redo: (editSession) ->
editSession?.setSelectedBufferRanges(newSelectedRanges)
pushOperation: (operation) ->
@buffer.pushOperation(operation, this)
getAnchors: ->
new Array(@anchors...)
getAnchorRanges: ->
new Array(@anchorRanges...)
addAnchor: (options={}) ->
anchor = @buffer.addAnchor(_.extend({editSession: this}, options))
@anchors.push(anchor)
anchor
addAnchorAtBufferPosition: (bufferPosition, options) ->
anchor = @addAnchor(options)
anchor.setBufferPosition(bufferPosition)
anchor
addAnchorRange: (range) ->
anchorRange = @buffer.addAnchorRange(range, this)
@anchorRanges.push(anchorRange)
anchorRange
removeAnchor: (anchor) ->
_.remove(@anchors, anchor)
removeAnchorRange: (anchorRange) ->
_.remove(@anchorRanges, anchorRange)
hasMultipleCursors: ->
@getCursors().length > 1
getCursors: -> new Array(@cursors...)
getCursor: (index=0) ->
@cursors[index]
getLastCursor: ->
_.last(@cursors)
addCursorAtScreenPosition: (screenPosition) ->
@addCursor(new Cursor(editSession: this, screenPosition: screenPosition))
addCursorAtBufferPosition: (bufferPosition) ->
@addCursor(new Cursor(editSession: this, bufferPosition: bufferPosition))
addCursor: (cursor=new Cursor(editSession: this, screenPosition: [0,0])) ->
@cursors.push(cursor)
@trigger 'add-cursor', cursor
@addSelectionForCursor(cursor)
cursor
removeCursor: (cursor) ->
_.remove(@cursors, cursor)
addSelectionForCursor: (cursor) ->
selection = new Selection(editSession: this, cursor: cursor)
@selections.push(selection)
@trigger 'add-selection', selection
selection
addSelectionForBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
@destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
@addCursor().selection.setBufferRange(bufferRange, options)
@mergeIntersectingSelections()
setSelectedBufferRange: (bufferRange, options) ->
@setSelectedBufferRanges([bufferRange], options)
setSelectedBufferRanges: (bufferRanges, options={}) ->
throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length
selections = @getSelections()
selection.destroy() for selection in selections[bufferRanges.length...]
for bufferRange, i in bufferRanges
bufferRange = Range.fromObject(bufferRange)
if selections[i]
selections[i].setBufferRange(bufferRange, options)
else
@addSelectionForBufferRange(bufferRange, options)
@mergeIntersectingSelections(options)
removeSelection: (selection) ->
_.remove(@selections, selection)
clearSelections: ->
lastSelection = @getLastSelection()
for selection in @getSelections() when selection != lastSelection
selection.destroy()
lastSelection.clear()
clearAllSelections: ->
selection.destroy() for selection in @getSelections()
getSelections: -> new Array(@selections...)
getSelection: (index) ->
index ?= @selections.length - 1
@selections[index]
getLastSelection: ->
_.last(@selections)
getSelectionsOrderedByBufferPosition: ->
@getSelections().sort (a, b) ->
aRange = a.getBufferRange()
bRange = b.getBufferRange()
aRange.end.compare(bRange.end)
getLastSelectionInBuffer: ->
_.last(@getSelectionsOrderedByBufferPosition())
selectionIntersectsBufferRange: (bufferRange) ->
_.any @getSelections(), (selection) ->
selection.intersectsBufferRange(bufferRange)
setCursorScreenPosition: (position) ->
@moveCursors (cursor) -> cursor.setScreenPosition(position)
getCursorScreenPosition: ->
@getLastCursor().getScreenPosition()
setCursorBufferPosition: (position) ->
@moveCursors (cursor) -> cursor.setBufferPosition(position)
getCursorBufferPosition: ->
@getLastCursor().getBufferPosition()
getSelectedScreenRange: ->
@getLastSelection().getScreenRange()
getSelectedBufferRange: ->
@getLastSelection().getBufferRange()
getSelectedBufferRanges: ->
selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition()
getSelectedText: ->
@getLastSelection().getText()
getTextInBufferRange: (range) ->
@buffer.getTextInRange(range)
moveCursorUp: ->
@moveCursors (cursor) -> cursor.moveUp()
moveCursorDown: ->
@moveCursors (cursor) -> cursor.moveDown()
moveCursorLeft: ->
@moveCursors (cursor) -> cursor.moveLeft()
moveCursorRight: ->
@moveCursors (cursor) -> cursor.moveRight()
moveCursorToTop: ->
@moveCursors (cursor) -> cursor.moveToTop()
moveCursorToBottom: ->
@moveCursors (cursor) -> cursor.moveToBottom()
moveCursorToBeginningOfLine: ->
@moveCursors (cursor) -> cursor.moveToBeginningOfLine()
moveCursorToFirstCharacterOfLine: ->
@moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine()
moveCursorToEndOfLine: ->
@moveCursors (cursor) -> cursor.moveToEndOfLine()
moveCursorToNextWord: ->
@moveCursors (cursor) -> cursor.moveToNextWord()
moveCursorToBeginningOfWord: ->
@moveCursors (cursor) -> cursor.moveToBeginningOfWord()
moveCursorToEndOfWord: ->
@moveCursors (cursor) -> cursor.moveToEndOfWord()
moveCursors: (fn) ->
fn(cursor) for cursor in @getCursors()
@mergeCursors()
selectToScreenPosition: (position) ->
lastSelection = @getLastSelection()
lastSelection.selectToScreenPosition(position)
@mergeIntersectingSelections(reverse: lastSelection.isReversed())
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()
selectAll: ->
@expandSelectionsForward (selection) => selection.selectAll()
selectToBottom: ->
@expandSelectionsForward (selection) => selection.selectToBottom()
selectToBeginningOfLine: ->
@expandSelectionsBackward (selection) => selection.selectToBeginningOfLine()
selectToEndOfLine: ->
@expandSelectionsForward (selection) => selection.selectToEndOfLine()
selectLine: ->
@expandSelectionsForward (selection) => selection.selectLine()
expandLastSelectionOverLine: ->
@getLastSelection().expandOverLine()
selectToBeginningOfWord: ->
@expandSelectionsBackward (selection) => selection.selectToBeginningOfWord()
selectToEndOfWord: ->
@expandSelectionsForward (selection) => selection.selectToEndOfWord()
selectWord: ->
@expandSelectionsForward (selection) => selection.selectWord()
expandLastSelectionOverWord: ->
@getLastSelection().expandOverWord()
mergeCursors: ->
positions = []
for cursor in new Array(@getCursors()...)
position = cursor.getBufferPosition().toString()
if position in positions
cursor.destroy()
else
positions.push(position)
expandSelectionsForward: (fn) ->
fn(selection) for selection in @getSelections()
@mergeIntersectingSelections()
expandSelectionsBackward: (fn) ->
fn(selection) for selection in @getSelections()
@mergeIntersectingSelections(reverse: 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
inspect: ->
JSON.stringify @serialize()
_.extend(EditSession.prototype, EventEmitter)

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

@@ -0,0 +1,869 @@
{View, $$} = require 'space-pen'
Buffer = require 'buffer'
Gutter = require 'gutter'
Point = require 'point'
Range = require 'range'
EditSession = require 'edit-session'
CursorView = require 'cursor-view'
SelectionView = require 'selection-view'
Native = require 'native'
fs = require 'fs'
$ = require 'jquery'
_ = require 'underscore'
module.exports =
class Editor extends View
@idCounter: 1
@content: (params) ->
@div class: @classes(params), tabindex: -1, =>
@input class: 'hidden-input', outlet: 'hiddenInput'
@div class: 'flexbox', =>
@subview 'gutter', new Gutter
@div class: 'scroll-view', outlet: 'scrollView', =>
@div class: 'lines', outlet: 'renderedLines', =>
@div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', =>
@div outlet: 'verticalScrollbarContent'
@classes: ({mini} = {}) ->
classes = ['editor']
classes.push 'mini' if mini
classes.join(' ')
vScrollMargin: 2
hScrollMargin: 10
lineHeight: null
charWidth: null
charHeight: null
cursorViews: null
selectionViews: null
lineCache: null
isFocused: false
activeEditSession: null
editSessions: null
attached: false
lineOverdraw: 100
@deserialize: (state, rootView) ->
editSessions = state.editSessions.map (state) -> EditSession.deserialize(state, rootView.project)
editor = new Editor(editSession: editSessions[state.activeEditSessionIndex], mini: state.mini)
editor.editSessions = editSessions
editor.isFocused = state.isFocused
editor
initialize: ({editSession, @mini} = {}) ->
requireStylesheet 'editor.css'
@id = Editor.idCounter++
@lineCache = []
@bindKeys()
@handleEvents()
@cursorViews = []
@selectionViews = []
@editSessions = []
if editSession?
@editSessions.push editSession
@setActiveEditSessionIndex(0)
else if @mini
editSession = new EditSession
buffer: new Buffer()
softWrap: false
tabText: " "
autoIndent: false
softTabs: true
@editSessions.push editSession
@setActiveEditSessionIndex(0)
else
throw new Error("Editor initialization requires an editSession")
serialize: ->
@saveActiveEditSession()
viewClass: "Editor"
editSessions: @editSessions.map (session) -> session.serialize()
activeEditSessionIndex: @getActiveEditSessionIndex()
isFocused: @isFocused
copy: ->
Editor.deserialize(@serialize(), @rootView())
bindKeys: ->
editorBindings =
'move-right': @moveCursorRight
'move-left': @moveCursorLeft
'move-down': @moveCursorDown
'move-up': @moveCursorUp
'move-to-next-word': @moveCursorToNextWord
'move-to-previous-word': @moveCursorToPreviousWord
'select-right': @selectRight
'select-left': @selectLeft
'select-up': @selectUp
'select-down': @selectDown
'select-word': @selectWord
'newline': @insertNewline
'indent': @indent
'indent-selected-rows': @indentSelectedRows
'outdent-selected-rows': @outdentSelectedRows
'backspace': @backspace
'backspace-to-beginning-of-word': @backspaceToBeginningOfWord
'delete': @delete
'delete-to-end-of-word': @deleteToEndOfWord
'cut-to-end-of-line': @cutToEndOfLine
'cut': @cutSelection
'copy': @copySelection
'paste': @paste
'undo': @undo
'redo': @redo
'move-to-top': @moveCursorToTop
'move-to-bottom': @moveCursorToBottom
'move-to-beginning-of-line': @moveCursorToBeginningOfLine
'move-to-end-of-line': @moveCursorToEndOfLine
'move-to-first-character-of-line': @moveCursorToFirstCharacterOfLine
'move-to-beginning-of-word': @moveCursorToBeginningOfWord
'move-to-end-of-word': @moveCursorToEndOfWord
'select-to-top': @selectToTop
'select-to-bottom': @selectToBottom
'select-to-end-of-line': @selectToEndOfLine
'select-to-beginning-of-line': @selectToBeginningOfLine
'select-to-end-of-word': @selectToEndOfWord
'select-to-beginning-of-word': @selectToBeginningOfWord
'select-all': @selectAll
if not @mini
_.extend editorBindings,
'save': @save
'newline-below': @insertNewlineBelow
'toggle-soft-wrap': @toggleSoftWrap
'fold-all': @foldAll
'unfold-all': @unfoldAll
'fold-current-row': @foldCurrentRow
'unfold-current-row': @unfoldCurrentRow
'fold-selection': @foldSelection
'split-left': @splitLeft
'split-right': @splitRight
'split-up': @splitUp
'split-down': @splitDown
'close': @close
'show-next-buffer': @loadNextEditSession
'show-previous-buffer': @loadPreviousEditSession
'toggle-line-comments': @toggleLineCommentsInSelection
for name, method of editorBindings
do (name, method) =>
@on name, => method.call(this); false
getCursor: (index) -> @activeEditSession.getCursor(index)
getCursors: -> @activeEditSession.getCursors()
getLastCursor: -> @activeEditSession.getLastCursor()
addCursorAtScreenPosition: (screenPosition) -> @activeEditSession.addCursorAtScreenPosition(screenPosition)
addCursorAtBufferPosition: (bufferPosition) -> @activeEditSession.addCursorAtBufferPosition(bufferPosition)
moveCursorUp: -> @activeEditSession.moveCursorUp()
moveCursorDown: -> @activeEditSession.moveCursorDown()
moveCursorLeft: -> @activeEditSession.moveCursorLeft()
moveCursorRight: -> @activeEditSession.moveCursorRight()
moveCursorToNextWord: -> @activeEditSession.moveCursorToNextWord()
moveCursorToBeginningOfWord: -> @activeEditSession.moveCursorToBeginningOfWord()
moveCursorToEndOfWord: -> @activeEditSession.moveCursorToEndOfWord()
moveCursorToTop: -> @activeEditSession.moveCursorToTop()
moveCursorToBottom: -> @activeEditSession.moveCursorToBottom()
moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine()
moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine()
moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine()
setCursorScreenPosition: (position) -> @activeEditSession.setCursorScreenPosition(position)
getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition()
setCursorBufferPosition: (position) -> @activeEditSession.setCursorBufferPosition(position)
getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition()
getSelection: (index) -> @activeEditSession.getSelection(index)
getSelections: -> @activeEditSession.getSelections()
getSelectionsOrderedByBufferPosition: -> @activeEditSession.getSelectionsOrderedByBufferPosition()
getLastSelectionInBuffer: -> @activeEditSession.getLastSelectionInBuffer()
getSelectedText: -> @activeEditSession.getSelectedText()
getSelectedBufferRanges: -> @activeEditSession.getSelectedBufferRanges()
getSelectedBufferRange: -> @activeEditSession.getSelectedBufferRange()
setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options)
setSelectedBufferRanges: (bufferRanges, options) -> @activeEditSession.setSelectedBufferRanges(bufferRanges, options)
addSelectionForBufferRange: (bufferRange, options) -> @activeEditSession.addSelectionForBufferRange(bufferRange, options)
selectRight: -> @activeEditSession.selectRight()
selectLeft: -> @activeEditSession.selectLeft()
selectUp: -> @activeEditSession.selectUp()
selectDown: -> @activeEditSession.selectDown()
selectToTop: -> @activeEditSession.selectToTop()
selectToBottom: -> @activeEditSession.selectToBottom()
selectAll: -> @activeEditSession.selectAll()
selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine()
selectToEndOfLine: -> @activeEditSession.selectToEndOfLine()
selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord()
selectToEndOfWord: -> @activeEditSession.selectToEndOfWord()
selectWord: -> @activeEditSession.selectWord()
selectToScreenPosition: (position) -> @activeEditSession.selectToScreenPosition(position)
clearSelections: -> @activeEditSession.clearSelections()
backspace: -> @activeEditSession.backspace()
backspaceToBeginningOfWord: -> @activeEditSession.backspaceToBeginningOfWord()
delete: -> @activeEditSession.delete()
deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord()
cutToEndOfLine: -> @activeEditSession.cutToEndOfLine()
insertText: (text) -> @activeEditSession.insertText(text)
insertNewline: -> @activeEditSession.insertNewline()
insertNewlineBelow: -> @activeEditSession.insertNewlineBelow()
indent: -> @activeEditSession.indent()
indentSelectedRows: -> @activeEditSession.indentSelectedRows()
outdentSelectedRows: -> @activeEditSession.outdentSelectedRows()
cutSelection: -> @activeEditSession.cutSelectedText()
copySelection: -> @activeEditSession.copySelectedText()
paste: -> @activeEditSession.pasteText()
undo: -> @activeEditSession.undo()
redo: -> @activeEditSession.redo()
createFold: (startRow, endRow) -> @activeEditSession.createFold(startRow, endRow)
foldCurrentRow: -> @activeEditSession.foldCurrentRow()
unfoldCurrentRow: -> @activeEditSession.unfoldCurrentRow()
foldAll: -> @activeEditSession.foldAll()
unfoldAll: -> @activeEditSession.unfoldAll()
foldSelection: -> @activeEditSession.foldSelection()
destroyFold: (foldId) -> @activeEditSession.destroyFold(foldId)
destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow)
isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow)
lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow)
linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end)
screenLineCount: -> @activeEditSession.screenLineCount()
setSoftWrapColumn: (softWrapColumn) ->
softWrapColumn ?= @calcSoftWrapColumn()
@activeEditSession.setSoftWrapColumn(softWrapColumn) if softWrapColumn
maxScreenLineLength: -> @activeEditSession.maxScreenLineLength()
getLastScreenRow: -> @activeEditSession.getLastScreenRow()
clipScreenPosition: (screenPosition, options={}) -> @activeEditSession.clipScreenPosition(screenPosition, options)
screenPositionForBufferPosition: (position, options) -> @activeEditSession.screenPositionForBufferPosition(position, options)
bufferPositionForScreenPosition: (position, options) -> @activeEditSession.bufferPositionForScreenPosition(position, options)
screenRangeForBufferRange: (range) -> @activeEditSession.screenRangeForBufferRange(range)
bufferRangeForScreenRange: (range) -> @activeEditSession.bufferRangeForScreenRange(range)
bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow)
stateForScreenRow: (row) -> @activeEditSession.stateForScreenRow(row)
setText: (text) -> @getBuffer().setText(text)
getText: -> @getBuffer().getText()
getPath: -> @getBuffer().getPath()
getLastBufferRow: -> @getBuffer().getLastRow()
getTextInRange: (range) -> @getBuffer().getTextInRange(range)
getEofPosition: -> @getBuffer().getEofPosition()
lineForBufferRow: (row) -> @getBuffer().lineForRow(row)
lineLengthForBufferRow: (row) -> @getBuffer().lineLengthForRow(row)
rangeForBufferRow: (row) -> @getBuffer().rangeForRow(row)
scanInRange: (args...) -> @getBuffer().scanInRange(args...)
backwardsScanInRange: (args...) -> @getBuffer().backwardsScanInRange(args...)
handleEvents: ->
@on 'focus', =>
@hiddenInput.focus()
false
@hiddenInput.on 'focus', =>
@rootView()?.editorFocused(this)
@isFocused = true
@addClass 'focused'
@hiddenInput.on 'focusout', =>
@isFocused = false
@removeClass 'focused'
@renderedLines.on 'mousedown', '.fold.line', (e) =>
@destroyFold($(e.currentTarget).attr('fold-id'))
false
@renderedLines.on 'mousedown', (e) =>
clickCount = e.originalEvent.detail
screenPosition = @screenPositionFromMouseEvent(e)
if clickCount == 1
if e.metaKey
@addCursorAtScreenPosition(screenPosition)
else if e.shiftKey
@selectToScreenPosition(screenPosition)
else
@setCursorScreenPosition(screenPosition)
else if clickCount == 2
if e.shiftKey
@activeEditSession.expandLastSelectionOverWord()
else
@activeEditSession.selectWord()
else if clickCount >= 3
if e.shiftKey
@activeEditSession.expandLastSelectionOverLine()
else
@activeEditSession.selectLine()
@selectOnMousemoveUntilMouseup()
@on "textInput", (e) =>
@insertText(e.originalEvent.data)
false
@scrollView.on 'mousewheel', (e) =>
e = e.originalEvent
if e.wheelDeltaY
newEvent = document.createEvent("WheelEvent");
newEvent.initWebKitWheelEvent(0, e.wheelDeltaY, e.view, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey)
@verticalScrollbar.get(0).dispatchEvent(newEvent)
false
@verticalScrollbar.on 'scroll', =>
@scrollTop(@verticalScrollbar.scrollTop(), adjustVerticalScrollbar: false)
@scrollView.on 'scroll', =>
if @scrollView.scrollLeft() == 0
@gutter.removeClass('drop-shadow')
else
@gutter.addClass('drop-shadow')
selectOnMousemoveUntilMouseup: ->
moveHandler = (e) => @selectToScreenPosition(@screenPositionFromMouseEvent(e))
@on 'mousemove', moveHandler
$(document).one 'mouseup', =>
@off 'mousemove', moveHandler
reverse = @activeEditSession.getLastSelection().isReversed()
@activeEditSession.mergeIntersectingSelections({reverse})
@syncCursorAnimations()
afterAttach: (onDom) ->
return if @attached or not onDom
@attached = true
@clearRenderedLines()
@subscribeToFontSize()
@calculateDimensions()
@hiddenInput.width(@charWidth)
@setSoftWrapColumn() if @activeEditSession.getSoftWrap()
$(window).on "resize.editor#{@id}", =>
@updateRenderedLines()
@focus() if @isFocused
@renderWhenAttached()
@trigger 'editor-open', [this]
edit: (editSession) ->
index = @editSessions.indexOf(editSession)
if index == -1
index = @editSessions.length
@editSessions.push(editSession)
@setActiveEditSessionIndex(index)
getBuffer: -> @activeEditSession.buffer
destroyActiveEditSession: ->
if @editSessions.length == 1
@remove()
else
editSession = @activeEditSession
@loadPreviousEditSession()
_.remove(@editSessions, editSession)
editSession.destroy()
loadNextEditSession: ->
nextIndex = (@getActiveEditSessionIndex() + 1) % @editSessions.length
@setActiveEditSessionIndex(nextIndex)
loadPreviousEditSession: ->
previousIndex = @getActiveEditSessionIndex() - 1
previousIndex = @editSessions.length - 1 if previousIndex < 0
@setActiveEditSessionIndex(previousIndex)
getActiveEditSessionIndex: ->
return index for session, index in @editSessions when session == @activeEditSession
setActiveEditSessionIndex: (index) ->
throw new Error("Edit session not found") unless @editSessions[index]
if @activeEditSession
@saveActiveEditSession()
@activeEditSession.off()
@activeEditSession = @editSessions[index]
@activeEditSession.on "buffer-contents-change-on-disk", =>
@showBufferConflictAlert(@activeEditSession)
@activeEditSession.on "buffer-path-change", =>
@trigger 'editor-path-change'
@trigger 'editor-path-change'
@renderWhenAttached()
if @attached and @activeEditSession.buffer.isInConflict()
@showBufferConflictAlert(@activeEditSession)
showBufferConflictAlert: (editSession) ->
message = editSession.getPath()
detailedMessage = "Has changed on disk. Do you want to reload it?"
Native.alert message, detailedMessage, [
["Reload", => editSession.buffer.reload()]
["Cancel", => ],
]
activateEditSessionForPath: (path) ->
for editSession, index in @editSessions
if editSession.buffer.getPath() == path
@setActiveEditSessionIndex(index)
return @activeEditSession
false
getOpenBufferPaths: ->
editSession.buffer.getPath() for editSession in @editSessions when editSession.buffer.getPath()?
scrollTop: (scrollTop, options) ->
return @cachedScrollTop or 0 unless scrollTop?
maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height()
scrollTop = Math.floor(Math.min(maxScrollTop, Math.max(0, scrollTop)))
return if scrollTop == @cachedScrollTop
@cachedScrollTop = scrollTop
@updateRenderedLines() if @attached
@renderedLines.css('top', -scrollTop)
@gutter.lineNumbers.css('top', -scrollTop)
if options?.adjustVerticalScrollbar ? true
@verticalScrollbar.scrollTop(scrollTop)
scrollBottom: (scrollBottom) ->
if scrollBottom?
@scrollTop(scrollBottom - @scrollView.height())
else
@scrollTop() + @scrollView.height()
scrollToBottom: ->
@scrollBottom(@scrollView.prop('scrollHeight'))
scrollTo: (pixelPosition) ->
return unless @attached
@scrollVertically(pixelPosition)
@scrollHorizontally(pixelPosition)
scrollVertically: (pixelPosition) ->
linesInView = @scrollView.height() / @lineHeight
maxScrollMargin = Math.floor((linesInView - 1) / 2)
scrollMargin = Math.min(@vScrollMargin, maxScrollMargin)
margin = scrollMargin * @lineHeight
desiredTop = pixelPosition.top - margin
desiredBottom = pixelPosition.top + @lineHeight + margin
scrollViewHeight = @scrollView.height()
if desiredBottom > @scrollTop() + scrollViewHeight
@scrollTop(desiredBottom - scrollViewHeight)
else if desiredTop < @scrollTop()
@scrollTop(desiredTop)
scrollHorizontally: (pixelPosition) ->
return if @activeEditSession.getSoftWrap()
charsInView = @scrollView.width() / @charWidth
maxScrollMargin = Math.floor((charsInView - 1) / 2)
scrollMargin = Math.min(@hScrollMargin, maxScrollMargin)
margin = scrollMargin * @charWidth
desiredRight = pixelPosition.left + @charWidth + margin
desiredLeft = pixelPosition.left - margin
if desiredRight > @scrollView.scrollRight()
@scrollView.scrollRight(desiredRight)
else if desiredLeft < @scrollView.scrollLeft()
@scrollView.scrollLeft(desiredLeft)
highlightFoldsContainingBufferRange: (bufferRange) ->
screenLines = @linesForScreenRows(@firstRenderedScreenRow, @lastRenderedScreenRow)
for screenLine, i in screenLines
if fold = screenLine.fold
screenRow = @firstRenderedScreenRow + i
element = @lineElementForScreenRow(screenRow)
if bufferRange.intersectsWith(fold.getBufferRange())
element.addClass('selected')
else
element.removeClass('selected')
setScrollPositionFromActiveEditSession: ->
@scrollTop(@activeEditSession.scrollTop ? 0)
@scrollView.scrollLeft(@activeEditSession.scrollLeft ? 0)
saveActiveEditSession: ->
@activeEditSession.setScrollTop(@scrollTop())
@activeEditSession.setScrollLeft(@scrollView.scrollLeft())
toggleSoftWrap: ->
@setSoftWrap(not @activeEditSession.getSoftWrap())
calcSoftWrapColumn: ->
if @activeEditSession.getSoftWrap()
Math.floor(@scrollView.width() / @charWidth)
else
Infinity
setSoftWrap: (softWrap, softWrapColumn=undefined) ->
@activeEditSession.setSoftWrap(softWrap)
@setSoftWrapColumn(softWrapColumn) if @attached
if @activeEditSession.getSoftWrap()
@addClass 'soft-wrap'
@_setSoftWrapColumn = => @setSoftWrapColumn()
$(window).on 'resize', @_setSoftWrapColumn
else
@removeClass 'soft-wrap'
$(window).off 'resize', @_setSoftWrapColumn
save: ->
if not @getPath()
path = Native.saveDialog()
return false if not path
@getBuffer().saveAs(path)
else
@getBuffer().save()
true
subscribeToFontSize: ->
return unless rootView = @rootView()
@setFontSize(rootView.getFontSize())
rootView.on "font-size-change.editor#{@id}", => @setFontSize(rootView.getFontSize())
setFontSize: (fontSize) ->
if fontSize?
@css('font-size', fontSize + 'px')
@calculateDimensions()
@updateCursorViews()
@updateRenderedLines()
newSplitEditor: ->
new Editor { editSession: @activeEditSession.copy() }
splitLeft: ->
@pane()?.splitLeft(@newSplitEditor()).wrappedView
splitRight: ->
@pane()?.splitRight(@newSplitEditor()).wrappedView
splitUp: ->
@pane()?.splitUp(@newSplitEditor()).wrappedView
splitDown: ->
@pane()?.splitDown(@newSplitEditor()).wrappedView
pane: ->
@parent('.pane').view()
rootView: ->
@parents('#root-view').view()
close: ->
return if @mini
if @getBuffer().isModified()
filename = if @getPath() then fs.base(@getPath()) else "untitled buffer"
message = "'#{filename}' has changes, do you want to save them?"
detailedMessage = "Your changes will be lost if you don't save them"
buttons = [
["Save", => @save() and @destroyActiveEditSession()]
["Cancel", =>]
["Don't save", => @destroyActiveEditSession()]
]
Native.alert message, detailedMessage, buttons
else
@destroyActiveEditSession()
remove: (selector, keepData) ->
return super if keepData
@trigger 'before-remove'
@destroyEditSessions()
$(window).off ".editor#{@id}"
rootView = @rootView()
rootView?.off ".editor#{@id}"
if @pane() then @pane().remove() else super
rootView?.focus()
getEditSessions: ->
new Array(@editSessions...)
destroyEditSessions: ->
for session in @getEditSessions()
session.destroy()
renderWhenAttached: ->
return unless @attached
@removeAllCursorAndSelectionViews()
@addCursorView(cursor) for cursor in @activeEditSession.getCursors()
@addSelectionView(selection) for selection in @activeEditSession.getSelections()
@activeEditSession.on 'add-cursor', (cursor) => @addCursorView(cursor)
@activeEditSession.on 'add-selection', (selection) => @addSelectionView(selection)
@prepareForScrolling()
@setScrollPositionFromActiveEditSession()
@renderLines()
@activeEditSession.on 'screen-lines-change', (e) => @handleDisplayBufferChange(e)
getCursorView: (index) ->
index ?= @cursorViews.length - 1
@cursorViews[index]
getCursorViews: ->
new Array(@cursorViews...)
addCursorView: (cursor) ->
cursorView = new CursorView(cursor, this)
@cursorViews.push(cursorView)
@renderedLines.append(cursorView)
cursorView
removeCursorView: (cursorView) ->
_.remove(@cursorViews, cursorView)
updateCursorViews: ->
for cursorView in @getCursorViews()
cursorView.updateAppearance()
syncCursorAnimations: ->
for cursorView in @getCursorViews()
do (cursorView) -> cursorView.resetCursorAnimation()
getSelectionView: (index) ->
index ?= @selectionViews.length - 1
@selectionViews[index]
getSelectionViews: ->
new Array(@selectionViews...)
addSelectionView: (selection) ->
selectionView = new SelectionView({editor: this, selection})
@selectionViews.push(selectionView)
@renderedLines.append(selectionView)
selectionView
removeSelectionView: (selectionView) ->
_.remove(@selectionViews, selectionView)
removeAllCursorAndSelectionViews: ->
cursorView.remove() for cursorView in @getCursorViews()
selectionView.remove() for selectionView in @getSelectionViews()
calculateDimensions: ->
fragment = $('<div class="line" style="position: absolute; visibility: hidden;"><span>x</span></div>')
@renderedLines.append(fragment)
@charWidth = fragment.width()
@charHeight = fragment.find('span').height()
@lineHeight = fragment.outerHeight()
@height(@lineHeight) if @mini
fragment.remove()
@gutter.calculateDimensions()
prepareForScrolling: ->
@adjustHeightOfRenderedLines()
@adjustWidthOfRenderedLines()
adjustHeightOfRenderedLines: ->
heightOfRenderedLines = @lineHeight * @screenLineCount()
@verticalScrollbarContent.height(heightOfRenderedLines)
@renderedLines.css('padding-bottom', heightOfRenderedLines)
adjustWidthOfRenderedLines: ->
width = @charWidth * @maxScreenLineLength()
if width > @scrollView.width()
@renderedLines.width(width)
else
@renderedLines.css('width', '')
handleScrollHeightChange: ->
scrollHeight = @lineHeight * @screenLineCount()
@verticalScrollbarContent.height(scrollHeight)
@scrollBottom(scrollHeight) if @scrollBottom() > scrollHeight
renderLines: ->
@clearRenderedLines()
@updateRenderedLines()
clearRenderedLines: ->
@lineCache = []
@renderedLines.find('.line').remove()
@firstRenderedScreenRow = -1
@lastRenderedScreenRow = -1
updateRenderedLines: ->
firstVisibleScreenRow = @getFirstVisibleScreenRow()
lastVisibleScreenRow = @getLastVisibleScreenRow()
renderFrom = Math.max(0, firstVisibleScreenRow - @lineOverdraw)
renderTo = Math.min(@getLastScreenRow(), lastVisibleScreenRow + @lineOverdraw)
if firstVisibleScreenRow < @firstRenderedScreenRow
@removeLineElements(Math.max(@firstRenderedScreenRow, renderTo + 1), @lastRenderedScreenRow)
@lastRenderedScreenRow = renderTo
newLines = @buildLineElements(renderFrom, Math.min(@firstRenderedScreenRow - 1, renderTo))
@insertLineElements(renderFrom, newLines)
@firstRenderedScreenRow = renderFrom
renderedLines = true
if lastVisibleScreenRow > @lastRenderedScreenRow
if 0 <= @firstRenderedScreenRow < renderFrom
@removeLineElements(@firstRenderedScreenRow, Math.min(@lastRenderedScreenRow, renderFrom - 1))
@firstRenderedScreenRow = renderFrom
startRowOfNewLines = Math.max(@lastRenderedScreenRow + 1, renderFrom)
newLines = @buildLineElements(startRowOfNewLines, renderTo)
@insertLineElements(startRowOfNewLines, newLines)
@lastRenderedScreenRow = renderTo
renderedLines = true
if renderedLines
@gutter.renderLineNumbers(renderFrom, renderTo)
@updatePaddingOfRenderedLines()
updatePaddingOfRenderedLines: ->
paddingTop = @firstRenderedScreenRow * @lineHeight
@renderedLines.css('padding-top', paddingTop)
@gutter.lineNumbers.css('padding-top', paddingTop)
paddingBottom = (@getLastScreenRow() - @lastRenderedScreenRow) * @lineHeight
@renderedLines.css('padding-bottom', paddingBottom)
@gutter.lineNumbers.css('padding-bottom', paddingBottom)
getFirstVisibleScreenRow: ->
Math.floor(@scrollTop() / @lineHeight)
getLastVisibleScreenRow: ->
Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1
handleDisplayBufferChange: (e) ->
oldScreenRange = e.oldRange
newScreenRange = e.newRange
if @attached
@handleScrollHeightChange() unless newScreenRange.coversSameRows(oldScreenRange)
@adjustWidthOfRenderedLines()
return if oldScreenRange.start.row > @lastRenderedScreenRow
maxEndRow = Math.max(@getLastVisibleScreenRow() + @lineOverdraw, @lastRenderedScreenRow)
@gutter.renderLineNumbers(@firstRenderedScreenRow, maxEndRow) if e.lineNumbersChanged
newScreenRange = newScreenRange.copy()
oldScreenRange = oldScreenRange.copy()
endOfShortestRange = Math.min(oldScreenRange.end.row, newScreenRange.end.row)
delta = @firstRenderedScreenRow - endOfShortestRange
if delta > 0
newScreenRange.start.row += delta
newScreenRange.end.row += delta
oldScreenRange.start.row += delta
oldScreenRange.end.row += delta
newScreenRange.start.row = Math.max(newScreenRange.start.row, @firstRenderedScreenRow)
oldScreenRange.end.row = Math.min(oldScreenRange.end.row, @lastRenderedScreenRow)
oldScreenRange.start.row = Math.max(oldScreenRange.start.row, @firstRenderedScreenRow)
newScreenRange.end.row = Math.min(newScreenRange.end.row, maxEndRow)
lineElements = @buildLineElements(newScreenRange.start.row, newScreenRange.end.row)
@replaceLineElements(oldScreenRange.start.row, oldScreenRange.end.row, lineElements)
rowDelta = newScreenRange.end.row - oldScreenRange.end.row
@lastRenderedScreenRow += rowDelta
@updateRenderedLines() if rowDelta < 0
if @lastRenderedScreenRow > maxEndRow
@removeLineElements(maxEndRow + 1, @lastRenderedScreenRow)
@lastRenderedScreenRow = maxEndRow
@updatePaddingOfRenderedLines()
buildLineElements: (startRow, endRow) ->
charWidth = @charWidth
charHeight = @charHeight
lines = @activeEditSession.linesForScreenRows(startRow, endRow)
activeEditSession = @activeEditSession
$$ ->
for line in lines
if fold = line.fold
lineAttributes = { class: 'fold line', 'fold-id': fold.id }
if activeEditSession.selectionIntersectsBufferRange(fold.getBufferRange())
lineAttributes.class += ' selected'
else
lineAttributes = { class: 'line' }
@div lineAttributes, =>
if line.text == ''
@raw '&nbsp;' if line.text == ''
else
for token in line.tokens
@span { class: token.getCssClassString() }, token.value
insertLineElements: (row, lineElements) ->
@spliceLineElements(row, 0, lineElements)
replaceLineElements: (startRow, endRow, lineElements) ->
@spliceLineElements(startRow, endRow - startRow + 1, lineElements)
removeLineElements: (startRow, endRow) ->
@spliceLineElements(startRow, endRow - startRow + 1)
spliceLineElements: (startScreenRow, rowCount, lineElements) ->
throw new Error("Splicing at a negative start row: #{startScreenRow}") if startScreenRow < 0
if startScreenRow < @firstRenderedScreenRow
startRow = 0
else
startRow = startScreenRow - @firstRenderedScreenRow
endRow = startRow + rowCount
elementToInsertBefore = @lineCache[startRow]
elementsToReplace = @lineCache[startRow...endRow]
@lineCache[startRow...endRow] = lineElements?.toArray() or []
lines = @renderedLines[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)
lineElementForScreenRow: (screenRow) ->
element = @lineCache[screenRow - @firstRenderedScreenRow]
$(element)
logScreenLines: (start, end) ->
@activeEditSession.logScreenLines(start, end)
toggleLineCommentsInSelection: ->
@activeEditSession.toggleLineCommentsInSelection()
logRenderedLines: ->
@renderedLines.find('.line').each (n) ->
console.log n, $(this).text()
pixelPositionForScreenPosition: (position) ->
position = Point.fromObject(position)
{ top: position.row * @lineHeight, left: position.column * @charWidth }
pixelOffsetForScreenPosition: (position) ->
{top, left} = @pixelPositionForScreenPosition(position)
offset = @renderedLines.offset()
{top: top + offset.top, left: left + offset.left}
screenPositionFromPixelPosition: ({top, left}) ->
screenPosition = new Point(Math.floor(top / @lineHeight), Math.floor(left / @charWidth))
screenPositionFromMouseEvent: (e) ->
{ pageX, pageY } = e
@screenPositionFromPixelPosition
top: pageY - @scrollView.offset().top + @scrollTop()
left: pageX - @scrollView.offset().left + @scrollView.scrollLeft()

View File

@@ -0,0 +1,62 @@
_ = 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)
@afterSubscribe?()
trigger: (eventName, args...) ->
[eventName, namespace] = eventName.split('.')
if namespace
@eventHandlersByNamespace?[namespace]?[eventName]?.forEach (handler) -> handler(args...)
else
@eventHandlersByEventName?[eventName]?.forEach (handler) -> handler(args...)
off: (eventName='', handler) ->
[eventName, namespace] = eventName.split('.')
eventName = undefined if eventName == ''
subscriptionCountBefore = @subscriptionCount()
if !eventName? and !namespace?
@eventHandlersByEventName = {}
@eventHandlersByNamespace = {}
else if namespace
if eventName
handlers = @eventHandlersByNamespace?[namespace]?[eventName] ? []
for handler in new Array(handlers...)
_.remove(handlers, handler)
@off eventName, handler
return
else
for eventName, handlers of @eventHandlersByNamespace?[namespace] ? {}
for handler in new Array(handlers...)
_.remove(handlers, handler)
@off eventName, handler
return
else
if handler
_.remove(@eventHandlersByEventName[eventName], handler)
else
delete @eventHandlersByEventName?[eventName]
@afterUnsubscribe?() if @subscriptionCount() < subscriptionCountBefore
subscriptionCount: ->
count = 0
for name, handlers of @eventHandlersByEventName
count += handlers.length
count

50
src/app/file.coffee Normal file
View File

@@ -0,0 +1,50 @@
EventEmitter = require 'event-emitter'
fs = require 'fs'
_ = require 'underscore'
module.exports =
class File
path: null
md5: null
constructor: (@path) ->
throw "Creating file with path that is not a file: #{@path}" unless fs.isFile(@path)
@updateMd5()
setPath: (@path) ->
getPath: ->
@path
getBaseName: ->
fs.base(@path)
updateMd5: ->
@md5 = fs.md5ForPath(@path)
afterSubscribe: ->
@subscribeToNativeChangeEvents() if @subscriptionCount() == 1
afterUnsubscribe: ->
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
subscribeToNativeChangeEvents: ->
@watchId = $native.watchPath @path, (eventType, path) =>
if eventType is "remove"
@trigger "remove"
@off()
else if eventType is "move"
@setPath(path)
@trigger "move"
else if eventType is "contents-change"
newMd5 = fs.md5ForPath(@getPath())
return if newMd5 == @md5
@md5 = newMd5
@trigger 'contents-change'
unsubscribeFromNativeChangeEvents: ->
$native.unwatchPath(@path, @watchId)
_.extend File.prototype, EventEmitter

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

@@ -0,0 +1,74 @@
Range = require 'range'
Point = require 'point'
module.exports =
class Fold
@idCounter: 1
displayBuffer: null
startRow: null
endRow: null
constructor: (@displayBuffer, @startRow, @endRow) ->
@id = @constructor.idCounter++
destroy: ->
@displayBuffer.destroyFold(this)
inspect: ->
"Fold(#{@startRow}, #{@endRow})"
getBufferRange: ({includeNewline}={}) ->
if includeNewline
end = [@endRow + 1, 0]
else
end = [@endRow, Infinity]
new Range([@startRow, 0], end)
getBufferDelta: ->
new Point(@endRow - @startRow + 1, 0)
handleBufferChange: (event) ->
oldStartRow = @startRow
if @isContainedByRange(event.oldRange)
@displayBuffer.unregisterFold(@startRow, this)
return
@updateStartRow(event)
@updateEndRow(event)
if @startRow != oldStartRow
@displayBuffer.unregisterFold(oldStartRow, this)
@displayBuffer.registerFold(this)
isContainedByRange: (range) ->
range.start.row <= @startRow and @endRow <= range.end.row
isContainedByFold: (fold) ->
@isContainedByRange(fold.getBufferRange())
updateStartRow: (event) ->
{ newRange, oldRange } = event
if oldRange.end.row < @startRow
delta = newRange.end.row - oldRange.end.row
else if newRange.end.row < @startRow
delta = newRange.end.row - @startRow
else
delta = 0
@startRow += delta
updateEndRow: (event) ->
{ newRange, oldRange } = event
if oldRange.end.row <= @endRow
delta = newRange.end.row - oldRange.end.row
else if newRange.end.row <= @endRow
delta = newRange.end.row - @endRow
else
delta = 0
@endRow += delta

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

@@ -0,0 +1,27 @@
{View, $$$} = require 'space-pen'
$ = require 'jquery'
_ = require 'underscore'
module.exports =
class Gutter extends View
@content: ->
@div class: 'gutter', =>
@div outlet: 'lineNumbers', class: 'line-numbers'
editor: ->
editor = @parentView
renderLineNumbers: (startScreenRow, endScreenRow) ->
lastScreenRow = -1
rows = @editor().bufferRowsForScreenRows(startScreenRow, endScreenRow)
@lineNumbers[0].innerHTML = $$$ ->
for row in rows
@div {class: 'line-number'}, if row == lastScreenRow then '' else row + 1
lastScreenRow = row
@calculateDimensions()
calculateDimensions: ->
@lineNumbers.width(@editor().getLastBufferRow().toString().length * @editor().charWidth)

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

@@ -0,0 +1,122 @@
$ = require 'jquery'
_ = require 'underscore'
fs = require 'fs'
BindingSet = require 'binding-set'
Specificity = require 'specificity'
module.exports =
class Keymap
bindingSets: null
queuedKeystrokes: 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.configFilePath)
$(document).on 'open', =>
path = $native.openDialog()
atom.open(path) if path
bindKeys: (selector, bindings) ->
index = @bindingSets.length
@bindingSets.unshift(new BindingSet(selector, bindings, index))
bindingsForElement: (element) ->
keystrokeMap = {}
currentNode = $(element)
while currentNode.length
bindingSets = @bindingSetsForNode(currentNode)
_.defaults(keystrokeMap, set.commandsByKeystrokes) for set in bindingSets
currentNode = currentNode.parent()
keystrokeMap
handleKeyEvent: (event) ->
event.keystrokes = @multiKeystrokeStringForEvent(event)
isMultiKeystroke = @queuedKeystrokes?
@queuedKeystrokes = null
currentNode = $(event.target)
while currentNode.length
candidateBindingSets = @bindingSetsForNode(currentNode)
for bindingSet in candidateBindingSets
command = bindingSet.commandForEvent(event)
if command
continue if @triggerCommandEvent(event, command)
return false
else if command == false
return false
if bindingSet.matchesKeystrokePrefix(event)
@queuedKeystrokes = event.keystrokes
return false
currentNode = currentNode.parent()
!isMultiKeystroke
bindingSetsForNode: (node) ->
bindingSets = @bindingSets.filter (set) -> node.is(set.selector)
bindingSets.sort (a, b) ->
if b.specificity == a.specificity
b.index - a.index
else
b.specificity - a.specificity
triggerCommandEvent: (keyEvent, commandName) ->
commandEvent = $.Event(commandName)
commandEvent.keyEvent = keyEvent
aborted = false
commandEvent.abortKeyBinding = ->
@stopImmediatePropagation()
aborted = true
$(keyEvent.target).trigger(commandEvent)
aborted
multiKeystrokeStringForEvent: (event) ->
currentKeystroke = @keystrokeStringForEvent(event)
if @queuedKeystrokes
@queuedKeystrokes + ' ' + currentKeystroke
else
currentKeystroke
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,7 @@
window.keymap.bindKeys '*'
'meta-w': 'close'
'alt-meta-i': 'toggle-dev-tools'
right: 'move-right'
left: 'move-left'
down: 'move-down'
up: 'move-up'

View File

@@ -0,0 +1,38 @@
window.keymap.bindKeys '.editor',
'meta-s': 'save'
'shift-right': 'select-right'
'shift-left': 'select-left'
'shift-up': 'select-up'
'shift-down': 'select-down'
'meta-a': 'select-all'
'enter': 'newline'
'meta-enter': 'newline-below'
'tab': 'indent'
'backspace': 'backspace'
'shift-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'
'ctrl-[': 'fold-current-row'
'ctrl-]': 'unfold-current-row'
'ctrl-{': 'fold-all'
'ctrl-}': 'unfold-all'
'alt-meta-ctrl-f': 'fold-selection'
'alt-meta-left': 'split-left'
'alt-meta-right': 'split-right'
'alt-meta-up': 'split-up'
'alt-meta-down': 'split-down'
'shift-tab': 'outdent-selected-rows'
'meta-[': 'outdent-selected-rows'
'meta-]': 'indent-selected-rows'
'meta-{': 'show-previous-buffer'
'meta-}': 'show-next-buffer'
'meta-+': 'increase-font-size'
'meta--': 'decrease-font-size'
'meta-/': 'toggle-line-comments'
'ctrl-w w': 'focus-next-pane'
'ctrl-W': 'select-word'

View File

@@ -0,0 +1,22 @@
window.keymap.bindKeys '*',
'ctrl-f': 'move-right'
'ctrl-b': 'move-left'
'ctrl-p': 'move-up'
'ctrl-n': 'move-down'
window.keymap.bindKeys '.editor',
'ctrl-F': 'select-right'
'ctrl-B': 'select-left'
'ctrl-P': 'select-up'
'ctrl-N': 'select-down'
'alt-f': 'move-to-end-of-word'
'alt-F': 'select-to-end-of-word'
'alt-b': 'move-to-beginning-of-word'
'alt-B': 'select-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'
'ctrl-k': 'cut-to-end-of-line'

View File

@@ -0,0 +1,3 @@
keystrokePattern = key:key additionalKeys:additionalKey* { return [key].concat(additionalKeys); }
additionalKey = '-' key:key { return key; }
key = '-' / chars:[^-]+ { return chars.join('') }

View File

@@ -0,0 +1,127 @@
Range = require 'range'
TextMateBundle = require 'text-mate-bundle'
_ = require 'underscore'
require 'underscore-extensions'
module.exports =
class LanguageMode
pairedCharacters:
'(': ')'
'[': ']'
'{': '}'
'"': '"'
"'": "'"
constructor: (@editSession) ->
@buffer = @editSession.buffer
@grammar = TextMateBundle.grammarForFileName(@buffer.getBaseName())
_.adviseBefore @editSession, 'insertText', (text) =>
return true if @editSession.hasMultipleCursors()
cursorBufferPosition = @editSession.getCursorBufferPosition()
nextCharacter = @editSession.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.add([0, 1])])
if @isCloseBracket(text) and text == nextCharacter
@editSession.moveCursorRight()
false
else if /^\s*$/.test(nextCharacter) and pairedCharacter = @pairedCharacters[text]
@editSession.insertText text + pairedCharacter
@editSession.moveCursorLeft()
false
isOpenBracket: (string) ->
@pairedCharacters[string]?
isCloseBracket: (string) ->
@getInvertedPairedCharacters()[string]?
getInvertedPairedCharacters: ->
return @invertedPairedCharacters if @invertedPairedCharacters
@invertedPairedCharacters = {}
for open, close of @pairedCharacters
@invertedPairedCharacters[close] = open
@invertedPairedCharacters
toggleLineCommentsInRange: (range) ->
range = Range.fromObject(range)
scopes = @tokenizedBuffer.scopesForPosition(range.start)
commentString = TextMateBundle.lineCommentStringForScope(scopes[0])
commentRegex = new OnigRegExp("^\s*" + _.escapeRegExp(commentString))
shouldUncomment = commentRegex.test(@editSession.lineForBufferRow(range.start.row))
for row in [range.start.row..range.end.row]
line = @editSession.lineForBufferRow(row)
if shouldUncomment
match = commentRegex.search(line)
@editSession.buffer.change([[row, 0], [row, match[0].length]], "")
else
@editSession.buffer.insert([row, 0], commentString)
doesBufferRowStartFold: (bufferRow) ->
return false if @editSession.isBufferRowBlank(bufferRow)
nextNonEmptyRow = @editSession.nextNonBlankBufferRow(bufferRow)
return false unless nextNonEmptyRow?
@editSession.indentationForBufferRow(nextNonEmptyRow) > @editSession.indentationForBufferRow(bufferRow)
rowRangeForFoldAtBufferRow: (bufferRow) ->
return null unless @doesBufferRowStartFold(bufferRow)
startIndentation = @editSession.indentationForBufferRow(bufferRow)
for row in [(bufferRow + 1)..@editSession.getLastBufferRow()]
continue if @editSession.isBufferRowBlank(row)
indentation = @editSession.indentationForBufferRow(row)
if indentation <= startIndentation
includeRowInFold = indentation == startIndentation and @grammar.foldEndRegex.search(@editSession.lineForBufferRow(row))
foldEndRow = row if includeRowInFold
break
foldEndRow = row
[bufferRow, foldEndRow]
autoIndentBufferRows: (startRow, endRow) ->
@autoIndentBufferRow(row) for row in [startRow..endRow]
autoIndentBufferRow: (bufferRow) ->
@autoIncreaseIndentForBufferRow(bufferRow)
@autoDecreaseIndentForBufferRow(bufferRow)
autoIncreaseIndentForBufferRow: (bufferRow) ->
precedingRow = @buffer.previousNonBlankRow(bufferRow)
return unless precedingRow?
precedingLine = @editSession.lineForBufferRow(precedingRow)
scopes = @tokenizedBuffer.scopesForPosition([precedingRow, Infinity])
increaseIndentPattern = TextMateBundle.indentRegexForScope(scopes[0])
return unless increaseIndentPattern
currentIndentation = @buffer.indentationForRow(bufferRow)
desiredIndentation = @buffer.indentationForRow(precedingRow)
desiredIndentation += @editSession.tabText.length if increaseIndentPattern.test(precedingLine)
if desiredIndentation > currentIndentation
@buffer.setIndentationForRow(bufferRow, desiredIndentation)
autoDecreaseIndentForBufferRow: (bufferRow) ->
scopes = @tokenizedBuffer.scopesForPosition([bufferRow, 0])
increaseIndentPattern = TextMateBundle.indentRegexForScope(scopes[0])
decreaseIndentPattern = TextMateBundle.outdentRegexForScope(scopes[0])
return unless increaseIndentPattern and decreaseIndentPattern
line = @buffer.lineForRow(bufferRow)
return unless decreaseIndentPattern.test(line)
currentIndentation = @buffer.indentationForRow(bufferRow)
precedingRow = @buffer.previousNonBlankRow(bufferRow)
precedingLine = @buffer.lineForRow(precedingRow)
desiredIndentation = @buffer.indentationForRow(precedingRow)
desiredIndentation -= @editSession.tabText.length unless increaseIndentPattern.test(precedingLine)
if desiredIndentation < currentIndentation
@buffer.setIndentationForRow(bufferRow, desiredIndentation)
getLineTokens: (line, stack) ->
{tokens, stack} = @grammar.getLineTokens(line, stack)

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

@@ -0,0 +1,186 @@
_ = 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
maxScreenLineLength: ->
maxLength = 0
@traverseByDelta 'screenDelta', [0, 0], [@lastScreenRow(), 0], ({lineFragment}) ->
length = lineFragment.text.length
maxLength = length if length > maxLength
maxLength
screenPositionForBufferPosition: (bufferPosition, options) ->
@translatePosition('bufferDelta', 'screenDelta', bufferPosition, options)
bufferPositionForScreenPosition: (screenPosition, options) ->
@translatePosition('screenDelta', 'bufferDelta', screenPosition, options)
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
traversedAllFragments = traversalResult.traversedAllFragments
sourceDelta = traversalResult[sourceDeltaType]
targetDelta = traversalResult[targetDeltaType]
return targetDelta unless lastLineFragment
maxSourceColumn = sourceDelta.column + lastLineFragment.textLength()
maxTargetColumn = targetDelta.column + lastLineFragment.textLength()
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 and not traversedAllFragments
targetDelta.row++
targetDelta.column = 0
else
additionalColumns = sourcePosition.column - sourceDelta.column
additionalColumns = lastLineFragment.translateColumn(sourceDeltaType, targetDeltaType, additionalColumns, { skipAtomicTokens })
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
startPosition = Point.fromObject(startPosition)
endPosition = Point.fromObject(endPosition)
for lineFragment, index 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)
lastLineFragment = lineFragment
traversedAllFragments = (index == @lineFragments.length - 1)
{ screenDelta, bufferDelta, lastLineFragment, traversedAllFragments }
logLines: (start=0, end=@screenLineCount() - 1)->
for row in [start..end]
line = @lineForScreenRow(row).text
console.log row, line, line.length

View File

@@ -0,0 +1,32 @@
$ = require 'jquery'
_ = require 'underscore'
PaneGrid = require 'pane-grid'
module.exports =
class PaneColumn extends PaneGrid
@content: ->
@div class: 'column'
className: ->
"PaneColumn"
adjustDimensions: ->
totalUnits = @verticalGridUnits()
unitsSoFar = 0
for child in @children()
child = $(child).view()
childUnits = child.verticalGridUnits()
child.css
width: '100%'
height: "#{childUnits / totalUnits * 100}%"
top: "#{unitsSoFar / totalUnits * 100}%"
left: 0
child.adjustDimensions()
unitsSoFar += childUnits
horizontalGridUnits: ->
Math.max(@horizontalChildUnits()...)
verticalGridUnits: ->
_.sum(@verticalChildUnits())

24
src/app/pane-grid.coffee Normal file
View File

@@ -0,0 +1,24 @@
$ = require 'jquery'
{View} = require 'space-pen'
module.exports =
class PaneGrid extends View
@deserialize: ({children}, rootView) ->
childViews = children.map (child) -> rootView.deserializeView(child)
new this(childViews)
initialize: (children=[]) ->
@append(children...)
serialize: ->
viewClass: @className()
children: @childViewStates()
childViewStates: ->
$(child).view().serialize() for child in @children()
horizontalChildUnits: ->
$(child).view().horizontalGridUnits() for child in @children()
verticalChildUnits: ->
$(child).view().verticalGridUnits() for child in @children()

32
src/app/pane-row.coffee Normal file
View File

@@ -0,0 +1,32 @@
$ = require 'jquery'
_ = require 'underscore'
PaneGrid = require 'pane-grid'
module.exports =
class PaneRow extends PaneGrid
@content: ->
@div class: 'row'
className: ->
"PaneRow"
adjustDimensions: ->
totalUnits = @horizontalGridUnits()
unitsSoFar = 0
for child in @children()
child = $(child).view()
childUnits = child.horizontalGridUnits()
child.css
width: "#{childUnits / totalUnits * 100}%"
height: '100%'
top: 0
left: "#{unitsSoFar / totalUnits * 100}%"
child.adjustDimensions()
unitsSoFar += childUnits
horizontalGridUnits: ->
_.sum(@horizontalChildUnits())
verticalGridUnits: ->
Math.max(@verticalChildUnits()...)

67
src/app/pane.coffee Normal file
View File

@@ -0,0 +1,67 @@
{View} = require 'space-pen'
PaneRow = require 'pane-row'
PaneColumn = require 'pane-column'
module.exports =
class Pane extends View
@content: (wrappedView) ->
@div class: 'pane', =>
@subview 'wrappedView', wrappedView
@deserialize: ({wrappedView}, rootView) ->
new Pane(rootView.deserializeView(wrappedView))
serialize: ->
viewClass: "Pane"
wrappedView: @wrappedView.serialize()
adjustDimensions: -> # do nothing
horizontalGridUnits: ->
1
verticalGridUnits: ->
1
splitUp: (view) ->
@split(view, 'column', 'before')
splitDown: (view) ->
@split(view, 'column', 'after')
splitLeft: (view) ->
@split(view, 'row', 'before')
splitRight: (view) ->
@split(view, 'row', 'after')
split: (view, axis, side) ->
unless @parent().hasClass(axis)
@buildPaneAxis(axis)
.insertBefore(this)
.append(@detach())
pane = new Pane(view)
this[side](pane)
@rootView().adjustPaneDimensions()
view.focus?()
pane
remove: (selector, keepData) ->
return super if keepData
# find parent elements before removing from dom
parentAxis = @parent('.row, .column')
rootView = @rootView()
super
if parentAxis.children().length == 1
sibling = parentAxis.children().detach()
parentAxis.replaceWith(sibling)
rootView.adjustPaneDimensions()
buildPaneAxis: (axis) ->
switch axis
when 'row' then new PaneRow
when 'column' then new PaneColumn
rootView: ->
@parents('#root-view').view()

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

@@ -0,0 +1,87 @@
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) ->
other = Point.fromObject(other)
row = @row + other.row
if other.row == 0
column = @column + other.column
else
column = other.column
new Point(row, column)
subtract: (other) ->
other = Point.fromObject(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) ->
return false unless other
other = Point.fromObject(other)
@row == other.row and @column == other.column
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}"
toArray: ->
[@row, @column]
serialize: ->
@toArray()

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

@@ -0,0 +1,179 @@
fs = require 'fs'
_ = require 'underscore'
$ = require 'jquery'
Range = require 'range'
Buffer = require 'buffer'
EditSession = require 'edit-session'
EventEmitter = require 'event-emitter'
Directory = require 'directory'
ChildProcess = require 'child-process'
module.exports =
class Project
tabText: ' '
autoIndent: true
softTabs: true
softWrap: false
rootDirectory: null
editSessions: null
ignoredPathRegexes: null
constructor: (path) ->
@setPath(path)
@editSessions = []
@buffers = []
@ignoredPathRegexes = [
/\.DS_Store$/
/(^|\/)\.git(\/|$)/
]
destroy: ->
editSession.destroy() for editSession in @getEditSessions()
getPath: ->
@rootDirectory?.path
setPath: (path) ->
@rootDirectory?.off()
if path?
directory = if fs.isDirectory(path) then path else fs.directory(path)
@rootDirectory = new Directory(directory)
else
@rootDirectory = null
@trigger "path-change"
getRootDirectory: ->
@rootDirectory
getFilePaths: ->
deferred = $.Deferred()
filePaths = []
fs.traverseTree @getPath(), (path, prune) =>
if @ignorePath(path)
prune()
else if fs.isFile(path)
filePaths.push @relativize(path)
deferred.resolve filePaths
deferred
ignorePath: (path) ->
_.find @ignoredPathRegexes, (regex) -> path.match(regex)
ignorePathRegex: ->
@ignoredPathRegexes.map((regex) -> "(#{regex.source})").join("|")
resolve: (filePath) ->
filePath = fs.join(@getPath(), filePath) unless filePath[0] == '/'
fs.absolute filePath
relativize: (fullPath) ->
fullPath.replace(@getPath(), "").replace(/^\//, '')
getTabText: -> @tabText
setTabText: (@tabText) ->
getAutoIndent: -> @autoIndent
setAutoIndent: (@autoIndent) ->
getSoftTabs: -> @softTabs
setSoftTabs: (@softTabs) ->
getSoftWrap: -> @softWrap
setSoftWrap: (@softWrap) ->
buildEditSessionForPath: (filePath, editSessionOptions={}) ->
@buildEditSession(@bufferForPath(filePath), editSessionOptions)
buildEditSession: (buffer, editSessionOptions) ->
options = _.extend(@defaultEditSessionOptions(), editSessionOptions)
options.project = this
options.buffer = buffer
editSession = new EditSession(options)
@editSessions.push editSession
@trigger 'new-edit-session', editSession
editSession
defaultEditSessionOptions: ->
tabText: @getTabText()
autoIndent: @getAutoIndent()
softTabs: @getSoftTabs()
softWrap: @getSoftWrap()
getEditSessions: ->
new Array(@editSessions...)
removeEditSession: (editSession) ->
_.remove(@editSessions, editSession)
getBuffers: ->
buffers = []
for editSession in @editSessions when not _.include(buffers, editSession.buffer)
buffers.push editSession.buffer
buffers
bufferForPath: (filePath) ->
if filePath?
filePath = @resolve(filePath)
buffer = _.find @buffers, (buffer) -> buffer.getPath() == filePath
buffer or @buildBuffer(filePath)
else
@buildBuffer()
buildBuffer: (filePath) ->
buffer = new Buffer(filePath, this)
@buffers.push buffer
@trigger 'new-buffer', buffer
buffer
removeBuffer: (buffer) ->
_.remove(@buffers, buffer)
scan: (regex, iterator) ->
regex = new RegExp(regex.source, 'g')
command = "#{require.resolve('ag')} --ackmate \"#{regex.source}\" \"#{@getPath()}\""
bufferedData = ""
state = 'readingPath'
path = null
readPath = (line) ->
if /^[0-9,; ]+:/.test(line)
state = 'readingLines'
else if /^:/.test line
path = line.substr(1)
else
path += ('\n' + line)
readLine = (line) ->
if line.length == 0
state = 'readingPath'
path = null
else
colonIndex = line.indexOf(':')
matchInfo = line.substring(0, colonIndex)
lineText = line.substring(colonIndex + 1)
readMatches(matchInfo, lineText)
readMatches = (matchInfo, lineText) ->
[lineNumber, matchPositionsText] = matchInfo.match(/(\d+);(.+)/)[1..]
row = parseInt(lineNumber) - 1
matchPositions = matchPositionsText.split(',').map (positionText) -> positionText.split(' ').map (pos) -> parseInt(pos)
for [column, length] in matchPositions
range = new Range([row, column], [row, column + length])
match = lineText.substr(column, length)
iterator({path, range, match})
ChildProcess.exec command , bufferLines: true, stdout: (data) ->
lines = data.split('\n')
lines.pop() # the last segment is a spurios '' because data always ends in \n due to bufferLines: true
for line in lines
readPath(line) if state is 'readingPath'
readLine(line) if state is 'readingLines'
_.extend Project.prototype, EventEmitter

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

@@ -0,0 +1,79 @@
Point = require 'point'
_ = require 'underscore'
module.exports =
class Range
@fromObject: (object) ->
if _.isArray(object)
new Range(object...)
else if object instanceof Range
object
else
new Range(object.start, object.end)
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: ->
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()}]"
add: (point) ->
new Range(@start.add(point), @end.add(point))
intersectsWith: (otherRange) ->
if @start.isLessThanOrEqual(otherRange.start)
@end.isGreaterThanOrEqual(otherRange.start)
else
otherRange.intersectsWith(this)
containsPoint: (point, { exclusive } = {}) ->
point = Point.fromObject(point)
if exclusive
point.isGreaterThan(@start) and point.isLessThan(@end)
else
point.isGreaterThanOrEqual(@start) and point.isLessThanOrEqual(@end)
containsRow: (row) ->
@start.row <= row <= @end.row
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)
getRowCount: ->
@end.row - @start.row + 1

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

@@ -0,0 +1,213 @@
$ = require 'jquery'
{$$} = require 'space-pen'
fs = require 'fs'
_ = require 'underscore'
{View} = require 'space-pen'
Buffer = require 'buffer'
Editor = require 'editor'
Project = require 'project'
VimMode = require 'vim-mode'
Pane = require 'pane'
PaneColumn = require 'pane-column'
PaneRow = require 'pane-row'
StatusBar = require 'status-bar'
TextMateTheme = require 'text-mate-theme'
module.exports =
class RootView extends View
@content: ->
@div id: 'root-view', tabindex: -1, =>
@div id: 'horizontal', outlet: 'horizontal', =>
@div id: 'vertical', outlet: 'vertical', =>
@div id: 'panes', outlet: 'panes'
@deserialize: ({ projectPath, panesViewState, extensionStates }) ->
rootView = new RootView(projectPath, extensionStates: extensionStates, suppressOpen: true)
rootView.setRootPane(rootView.deserializeView(panesViewState)) if panesViewState
rootView
extensions: null
extensionStates: null
fontSize: 20
initialize: (pathToOpen, { @extensionStates, suppressOpen } = {}) ->
window.rootView = this
TextMateTheme.activate('IR_Black')
@extensionStates ?= {}
@extensions = {}
@project = new Project(pathToOpen)
@handleEvents()
@setTitle()
@loadUserConfiguration()
@open(pathToOpen) if fs.isFile(pathToOpen) unless suppressOpen
serialize: ->
projectPath: @project?.getPath()
panesViewState: @panes.children().view()?.serialize()
extensionStates: @serializeExtensions()
handleEvents: ->
@on 'toggle-dev-tools', => window.toggleDevTools()
@on 'focus', (e) =>
if @getActiveEditor()
@getActiveEditor().focus()
false
else
@setTitle(@project?.getPath())
@on 'active-editor-path-change', (e, path) =>
@project.setPath(path) unless @project.getRootDirectory()
@setTitle(path)
@on 'increase-font-size', => @setFontSize(@getFontSize() + 1)
@on 'decrease-font-size', => @setFontSize(@getFontSize() - 1)
@on 'focus-next-pane', => @focusNextPane()
afterAttach: (onDom) ->
@focus() if onDom
serializeExtensions: ->
extensionStates = {}
for name, extension of @extensions
try
extensionStates[name] = extension.serialize?()
catch e
console?.error("Exception serializing '#{name}' extension\n", e.stack)
extensionStates
deserializeView: (viewState) ->
switch viewState.viewClass
when 'Pane' then Pane.deserialize(viewState, this)
when 'PaneRow' then PaneRow.deserialize(viewState, this)
when 'PaneColumn' then PaneColumn.deserialize(viewState, this)
when 'Editor' then Editor.deserialize(viewState, this)
activateExtension: (extension) ->
throw new Error("Trying to activate an extension with no name") unless extension.name?
@extensions[extension.name] = extension
extension.activate(this, @extensionStates[extension.name])
deactivateExtension: (extension) ->
extension.deactivate?()
delete @extensions[extension.name]
deactivate: ->
atom.rootViewStates[$windowNumber] = JSON.stringify(@serialize())
@deactivateExtension(extension) for name, extension of @extensions
@remove()
open: (path, options = {}) ->
changeFocus = options.changeFocus ? true
allowActiveEditorChange = options.allowActiveEditorChange ? false
unless editSession = @openInExistingEditor(path, allowActiveEditorChange)
editSession = @project.buildEditSessionForPath(path)
editor = new Editor({editSession})
pane = new Pane(editor)
@panes.append(pane)
if changeFocus
editor.focus()
else
@makeEditorActive(editor)
editSession
openInExistingEditor: (path, allowActiveEditorChange) ->
if activeEditor = @getActiveEditor()
path = @project.resolve(path) if path
if editSession = activeEditor.activateEditSessionForPath(path)
return editSession
if allowActiveEditorChange
for editor in @getEditors()
if editSession = editor.activateEditSessionForPath(path)
editor.focus()
return editSession
editSession = @project.buildEditSessionForPath(path)
activeEditor.edit(editSession)
editSession
editorFocused: (editor) ->
@makeEditorActive(editor) if @panes.containsElement(editor)
makeEditorActive: (editor) ->
previousActiveEditor = @panes.find('.editor.active').view()
previousActiveEditor?.removeClass('active').off('.root-view')
editor.addClass('active')
if not editor.mini
editor.on 'editor-path-change.root-view', =>
@trigger 'active-editor-path-change', editor.getPath()
if not previousActiveEditor or editor.getPath() != previousActiveEditor.getPath()
@trigger 'active-editor-path-change', editor.getPath()
activeKeybindings: ->
keymap.bindingsForElement(document.activeElement)
setTitle: (title='untitled') ->
document.title = title
getEditors: ->
@panes.find('.pane > .editor').map(-> $(this).view()).toArray()
getModifiedBuffers: ->
modifiedBuffers = []
for editor in @getEditors()
for session in editor.editSessions
modifiedBuffers.push session.buffer if session.buffer.isModified()
modifiedBuffers
getOpenBufferPaths: ->
_.uniq(_.flatten(@getEditors().map (editor) -> editor.getOpenBufferPaths()))
getActiveEditor: ->
if (editor = @panes.find('.editor.active')).length
editor.view()
else
@panes.find('.editor:first').view()
getActiveEditSession: ->
@getActiveEditor()?.activeEditSession
focusNextPane: ->
panes = @panes.find('.pane')
currentIndex = panes.toArray().indexOf(@getFocusedPane()[0])
nextIndex = (currentIndex + 1) % panes.length
panes.eq(nextIndex).view().wrappedView.focus()
getFocusedPane: ->
@panes.find('.pane:has(:focus)')
setRootPane: (pane) ->
@panes.empty()
@panes.append(pane)
@adjustPaneDimensions()
adjustPaneDimensions: ->
rootPane = @panes.children().first().view()
rootPane?.css(width: '100%', height: '100%', top: 0, left: 0)
rootPane?.adjustDimensions()
remove: ->
editor.remove() for editor in @getEditors()
@project.destroy()
super
setFontSize: (newFontSize) ->
newFontSize = Math.max(1, newFontSize)
[oldFontSize, @fontSize] = [@fontSize, newFontSize]
@trigger 'font-size-change' if oldFontSize != newFontSize
getFontSize: -> @fontSize
loadUserConfiguration: ->
try
require atom.configFilePath if fs.exists(atom.configFilePath)
catch error
console.error "Failed to load `#{atom.configFilePath}`", error.message, error

View File

@@ -0,0 +1,92 @@
_ = require 'underscore'
Point = require 'point'
module.exports =
class ScreenLine
stack: null
text: null
tokens: null
screenDelta: null
bufferDelta: null
foldable: null
constructor: (@tokens, @text, screenDelta, bufferDelta, extraFields) ->
@screenDelta = Point.fromObject(screenDelta)
@bufferDelta = Point.fromObject(bufferDelta)
_.extend(this, extraFields)
copy: ->
new ScreenLine(@tokens, @text, @screenDelta, @bufferDelta, { @stack, @foldable })
splitAt: (column) ->
return [new ScreenLine([], '', [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] = rightTokens[0].splitAt(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 ScreenLine(leftTokens, leftText, leftScreenDelta, leftBufferDelta, {@stack, @foldable})
rightFragment = new ScreenLine(rightTokens, rightText, rightScreenDelta, rightBufferDelta, {@stack})
[leftFragment, rightFragment]
tokenAtBufferColumn: (bufferColumn) ->
delta = 0
for token in @tokens
delta += token.bufferDelta
return token if delta >= bufferColumn
token
concat: (other) ->
tokens = @tokens.concat(other.tokens)
text = @text + other.text
screenDelta = @screenDelta.add(other.screenDelta)
bufferDelta = @bufferDelta.add(other.bufferDelta)
new ScreenLine(tokens, text, screenDelta, bufferDelta, {stack: other.stack})
translateColumn: (sourceDeltaType, targetDeltaType, sourceColumn, options={}) ->
{ skipAtomicTokens } = options
sourceColumn = Math.min(sourceColumn, @textLength())
currentSourceColumn = 0
currentTargetColumn = 0
for token in @tokens
tokenStartTargetColumn = currentTargetColumn
tokenStartSourceColumn = currentSourceColumn
tokenEndSourceColumn = currentSourceColumn + token[sourceDeltaType]
tokenEndTargetColumn = currentTargetColumn + token[targetDeltaType]
break if tokenEndSourceColumn > sourceColumn
currentSourceColumn = tokenEndSourceColumn
currentTargetColumn = tokenEndTargetColumn
if token?.isAtomic
if skipAtomicTokens and sourceColumn > tokenStartSourceColumn
tokenEndTargetColumn
else
tokenStartTargetColumn
else
remainingColumns = sourceColumn - currentSourceColumn
currentTargetColumn + remainingColumns
textLength: ->
if @fold
textLength = 0
else
textLength = @text.length
isSoftWrapped: ->
@screenDelta.row == 1 and @bufferDelta.row == 0
isEqual: (other) ->
_.isEqual(@tokens, other.tokens) and @screenDelta.isEqual(other.screenDelta) and @bufferDelta.isEqual(other.bufferDelta)

View File

@@ -0,0 +1,61 @@
Anchor = require 'anchor'
Point = require 'point'
Range = require 'range'
{View, $$} = require 'space-pen'
module.exports =
class SelectionView extends View
@content: ->
@div()
regions: null
initialize: ({@editor, @selection} = {}) ->
@regions = []
@selection.on 'change-screen-range', => @updateAppearance()
@selection.on 'destroy', => @remove()
@updateAppearance()
updateAppearance: ->
@clearRegions()
range = @getScreenRange()
@editor.highlightFoldsContainingBufferRange(@getBufferRange())
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: ->
@selection.getScreenRange()
getBufferRange: ->
@selection.getBufferRange()
remove: ->
@editor.removeSelectionView(this)
super

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

@@ -0,0 +1,256 @@
Range = require 'range'
Anchor = require 'anchor'
EventEmitter = require 'event-emitter'
_ = require 'underscore'
module.exports =
class Selection
anchor: null
constructor: ({@cursor, @editSession}) ->
@cursor.selection = this
@cursor.on 'change-screen-position.selection', (e) =>
@trigger 'change-screen-range', @getScreenRange() unless e.bufferChanged
@cursor.on 'destroy.selection', =>
@cursor = null
@destroy()
destroy: ->
if @cursor
@cursor.off('.selection')
@cursor.destroy()
@anchor?.destroy()
@editSession.removeSelection(this)
@trigger 'destroy'
isEmpty: ->
@getBufferRange().isEmpty()
isReversed: ->
not @isEmpty() and @cursor.getBufferPosition().isLessThan(@anchor.getBufferPosition())
getScreenRange: ->
if @anchor
new Range(@anchor.getScreenPosition(), @cursor.getScreenPosition())
else
new Range(@cursor.getScreenPosition(), @cursor.getScreenPosition())
setScreenRange: (screenRange, options={}) ->
screenRange = Range.fromObject(screenRange)
{ start, end } = screenRange
[start, end] = [end, start] if options.reverse
@modifyScreenRange =>
@placeAnchor() unless @anchor
@modifySelection =>
@anchor.setScreenPosition(start)
@cursor.setScreenPosition(end)
setBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
[start, end] = [end, start] if options.reverse
@modifyScreenRange =>
@editSession.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
@placeAnchor() unless @anchor
@modifySelection =>
@anchor.setBufferPosition(start, options)
@cursor.setBufferPosition(end, options)
getBufferRange: ->
if @anchor
new Range(@anchor.getBufferPosition(), @cursor.getBufferPosition())
else
new Range(@cursor.getBufferPosition(), @cursor.getBufferPosition())
getText: ->
@editSession.buffer.getTextInRange(@getBufferRange())
clear: ->
@modifyScreenRange =>
@anchor?.destroy()
@anchor = null
selectWord: ->
@setBufferRange(@cursor.getCurrentWordBufferRange())
expandOverWord: ->
@setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()))
selectLine: (row=@cursor.getBufferPosition().row) ->
@setBufferRange(@editSession.bufferRangeForBufferRow(row))
expandOverLine: ->
@setBufferRange(@getBufferRange().union(@cursor.getCurrentLineBufferRange()))
selectToScreenPosition: (position) ->
@modifySelection => @cursor.setScreenPosition(position)
selectToBufferPosition: (position) ->
@modifySelection => @cursor.setBufferPosition(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()
selectAll: ->
@setBufferRange(@editSession.buffer.getRange())
selectToBeginningOfLine: ->
@modifySelection => @cursor.moveToBeginningOfLine()
selectToEndOfLine: ->
@modifySelection => @cursor.moveToEndOfLine()
selectToBeginningOfWord: ->
@modifySelection => @cursor.moveToBeginningOfWord()
selectToEndOfWord: ->
@modifySelection => @cursor.moveToEndOfWord()
insertText: (text, options={}) ->
oldBufferRange = @getBufferRange()
@editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row)
wasReversed = @isReversed()
@clear()
newBufferRange = @editSession.buffer.change(oldBufferRange, text)
@cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed
autoIndent = options.autoIndent ? true
if @editSession.autoIndent and autoIndent
if /\n/.test(text)
firstLinePrefix = @editSession.getTextInBufferRange([[newBufferRange.start.row, 0], newBufferRange.start])
if /^\s*$/.test(firstLinePrefix)
@editSession.autoIncreaseIndentForBufferRow(newBufferRange.start.row)
if newBufferRange.getRowCount() > 1
@editSession.autoIndentBufferRows(newBufferRange.start.row + 1, newBufferRange.end.row)
else
@editSession.autoDecreaseIndentForRow(newBufferRange.start.row)
backspace: ->
if @isEmpty() and not @editSession.isFoldedAtScreenRow(@cursor.getScreenRow())
if @cursor.isAtBeginningOfLine() and @editSession.isFoldedAtScreenRow(@cursor.getScreenRow() - 1)
@selectToBufferPosition([@cursor.getBufferRow() - 1, Infinity])
else
@selectLeft()
@deleteSelectedText()
backspaceToBeginningOfWord: ->
@selectToBeginningOfWord() if @isEmpty()
@deleteSelectedText()
delete: ->
if @isEmpty()
if @cursor.isAtEndOfLine() and fold = @editSession.largestFoldStartingAtScreenRow(@cursor.getScreenRow() + 1)
@selectToBufferPosition(fold.getBufferRange().end)
else
@selectRight()
@deleteSelectedText()
deleteToEndOfWord: ->
@selectToEndOfWord() if @isEmpty()
@deleteSelectedText()
deleteSelectedText: ->
bufferRange = @getBufferRange()
if fold = @editSession.largestFoldContainingBufferRow(bufferRange.end.row)
includeNewline = bufferRange.start.column == 0 or bufferRange.start.row >= fold.startRow
bufferRange = bufferRange.union(fold.getBufferRange({ includeNewline }))
@modifyScreenRange =>
@editSession.buffer.delete(bufferRange) unless bufferRange.isEmpty()
@cursor?.setBufferPosition(bufferRange.start)
indentSelectedRows: ->
range = @getBufferRange()
for row in [range.start.row..range.end.row]
@editSession.buffer.insert([row, 0], @editSession.tabText) unless @editSession.buffer.lineLengthForRow(row) == 0
outdentSelectedRows: ->
range = @getBufferRange()
buffer = @editSession.buffer
leadingTabRegex = new RegExp("^#{@editSession.tabText}")
for row in [range.start.row..range.end.row]
if leadingTabRegex.test buffer.lineForRow(row)
buffer.delete [[row, 0], [row, @editSession.tabText.length]]
toggleLineComments: ->
@modifySelection =>
@editSession.toggleLineCommentsInRange(@getBufferRange())
cutToEndOfLine: (maintainPasteboard) ->
@selectToEndOfLine() if @isEmpty()
@cut(maintainPasteboard)
cut: (maintainPasteboard=false) ->
@copy(maintainPasteboard)
@delete()
copy: (maintainPasteboard=false) ->
return if @isEmpty()
text = @editSession.buffer.getTextInRange(@getBufferRange())
text = $native.readFromPasteboard() + "\n" + text if maintainPasteboard
$native.writeToPasteboard text
fold: ->
range = @getBufferRange()
@editSession.createFold(range.start.row, range.end.row)
@cursor.setBufferPosition([range.end.row + 1, 0])
autoIndentText: (text) ->
@editSession.autoIndentTextAfterBufferPosition(text, @cursor.getBufferPosition())
autoOutdent: ->
@editSession.autoOutdentBufferRow(@cursor.getBufferRow())
modifySelection: (fn) ->
@retainSelection = true
@view?.retainSelection = true
@placeAnchor() unless @anchor
fn()
@retainSelection = false
@view?.retainSelection = false
modifyScreenRange: (fn) ->
oldScreenRange = @getScreenRange()
fn()
if @cursor
newScreenRange = @getScreenRange()
@trigger 'change-screen-range', newScreenRange unless oldScreenRange.isEqual(newScreenRange)
placeAnchor: ->
@anchor = @editSession.addAnchor(strong: true)
@anchor.setScreenPosition(@cursor.getScreenPosition())
@anchor.on 'change-screen-position.selection', => @trigger 'change-screen-range'
intersectsBufferRange: (bufferRange) ->
@getBufferRange().intersectsWith(bufferRange)
intersectsWith: (otherSelection) ->
@getScreenRange().intersectsWith(otherSelection.getScreenRange())
merge: (otherSelection, options) ->
@setScreenRange(@getScreenRange().union(otherSelection.getScreenRange()), options)
otherSelection.destroy()
_.extend Selection.prototype, EventEmitter

40
src/app/status-bar.coffee Normal file
View File

@@ -0,0 +1,40 @@
{View} = require 'space-pen'
module.exports =
class StatusBar extends View
@activate: (rootView) ->
requireStylesheet 'status-bar.css'
for editor in rootView.getEditors()
@appendToEditorPane(rootView, editor) if rootView.parents('html').length
rootView.on 'editor-open', (e, editor) =>
@appendToEditorPane(rootView, editor)
@appendToEditorPane: (rootView, editor) ->
if pane = editor.pane()
pane.append(new StatusBar(rootView, editor))
@content: ->
@div class: 'status-bar', =>
@div class: 'current-path', outlet: 'currentPath'
@div class: 'cursor-position', outlet: 'cursorPosition'
initialize: (@rootView, @editor) ->
@updatePathText()
@editor.on 'editor-path-change', => @updatePathText()
@updateCursorPositionText()
@editor.on 'cursor-move', => @updateCursorPositionText()
updatePathText: ->
path = @editor.getPath()
if path
@currentPath.text(@rootView.project.relativize(path))
else
@currentPath.text('untitled')
updateCursorPositionText: ->
{ row, column } = @editor.getCursorBufferPosition()
@cursorPosition.text("#{row + 1},#{column + 1}")

View File

@@ -0,0 +1,71 @@
_ = require 'underscore'
fs = require 'fs'
plist = require 'plist'
TextMateGrammar = require 'text-mate-grammar'
module.exports =
class TextMateBundle
@grammarsByFileType: {}
@preferencesByScopeSelector: {}
@bundles: []
@loadAll: ->
for bundlePath in fs.list(require.resolve("bundles"))
@registerBundle(new TextMateBundle(bundlePath))
@registerBundle: (bundle)->
@bundles.push(bundle)
for scopeSelector, preferences of bundle.getPreferencesByScopeSelector()
@preferencesByScopeSelector[scopeSelector] = preferences
for grammar in bundle.grammars
for fileType in grammar.fileTypes
@grammarsByFileType[fileType] = grammar
@grammarForFileName: (fileName) ->
extension = fs.extension(fileName)?[1...]
@grammarsByFileType[extension] or @grammarsByFileType["txt"]
@getPreferenceInScope: (scopeSelector, preferenceName) ->
@preferencesByScopeSelector[scopeSelector]?[preferenceName]
@lineCommentStringForScope: (scope) ->
shellVariables = @getPreferenceInScope(scope, 'shellVariables')
lineComment = (_.find shellVariables, ({name}) -> name == "TM_COMMENT_START")['value']
@indentRegexForScope: (scope) ->
if source = @getPreferenceInScope(scope, 'increaseIndentPattern')
new OnigRegExp(source)
@outdentRegexForScope: (scope) ->
if source = @getPreferenceInScope(scope, 'decreaseIndentPattern')
new OnigRegExp(source)
grammars: null
constructor: (@path) ->
@grammars = []
if fs.exists(@getSyntaxesPath())
for syntaxPath in fs.list(@getSyntaxesPath())
@grammars.push TextMateGrammar.loadFromPath(syntaxPath)
getPreferencesByScopeSelector: ->
return {} unless fs.exists(@getPreferencesPath())
preferencesByScopeSelector = {}
for preferencePath in fs.list(@getPreferencesPath())
plist.parseString fs.read(preferencePath), (e, data) ->
throw new Error(e) if e
{ scope, settings } = data[0]
preferencesByScopeSelector[scope] = _.extend(preferencesByScopeSelector[scope] ? {}, settings)
preferencesByScopeSelector
getSyntaxesPath: ->
fs.join(@path, "Syntaxes")
getPreferencesPath: ->
fs.join(@path, "Preferences")

View File

@@ -0,0 +1,212 @@
_ = require 'underscore'
fs = require 'fs'
plist = require 'plist'
module.exports =
class TextMateGrammar
@loadFromPath: (path) ->
grammar = null
plist.parseString fs.read(path), (e, data) ->
throw new Error(e) if e
grammar = new TextMateGrammar(data[0])
grammar
name: null
fileTypes: null
foldEndRegex: null
repository: null
initialRule: null
constructor: ({ @name, @fileTypes, scopeName, patterns, repository, foldingStopMarker}) ->
@initialRule = new Rule(this, {scopeName, patterns})
@repository = {}
@foldEndRegex = new OnigRegExp(foldingStopMarker) if foldingStopMarker
for name, data of repository
@repository[name] = new Rule(this, data)
for rule in [@initialRule, _.values(@repository)...]
rule.compileRegex()
getLineTokens: (line, stack=[@initialRule]) ->
stack = new Array(stack...)
tokens = []
position = 0
loop
scopes = _.pluck(stack, "scopeName")
if line.length == 0
tokens = [{value: "", scopes: scopes}]
return { tokens, scopes }
break if position == line.length
{ nextTokens, tokensStartPosition, tokensEndPosition} = _.last(stack).getNextTokens(stack, line, position)
if nextTokens
if position < tokensStartPosition # unmatched text preceding next tokens
tokens.push
value: line[position...tokensStartPosition]
scopes: scopes
tokens.push(nextTokens...)
position = tokensEndPosition
else
tokens.push
value: line[position...line.length]
scopes: scopes
break
{ tokens, stack }
ruleForInclude: (name) ->
if name[0] == "#"
@repository[name[1..]]
else if name == "$self"
@initialRule
class Rule
grammar: null
scopeName: null
patterns: null
endPattern: null
constructor: (@grammar, {@scopeName, patterns, @endPattern}) ->
patterns ?= []
@patterns = []
@patterns.push(@endPattern) if @endPattern
@patterns.push((patterns.map (pattern) => new Pattern(grammar, pattern))...)
compileRegex: ->
regexComponents = []
@patternsByCaptureIndex = {}
currentCaptureIndex = 1
for [regex, pattern] in @getRegexPatternPairs()
regexComponents.push(regex.source)
@patternsByCaptureIndex[currentCaptureIndex] = pattern
currentCaptureIndex += 1 + regex.getCaptureCount()
@regex = new OnigRegExp('(' + regexComponents.join(')|(') + ')')
pattern.compileRegex() for pattern in @patterns
getRegexPatternPairs: (included=[]) ->
return [] if _.include(included, this)
included.push(this)
regexPatternPairs = []
regexPatternPairs.push(@endPattern.getRegexPatternPairs()...) if @endPattern
for pattern in @patterns
regexPatternPairs.push(pattern.getRegexPatternPairs(included)...)
regexPatternPairs
getNextTokens: (stack, line, position) ->
captureIndices = @regex.getCaptureIndices(line, position)
return {} unless captureIndices?[2] > 0 # ignore zero-length matches
shiftCapture(captureIndices)
[firstCaptureIndex, firstCaptureStart, firstCaptureEnd] = captureIndices
pattern = @patternsByCaptureIndex[firstCaptureIndex]
nextTokens = pattern.handleMatch(stack, line, captureIndices)
{ nextTokens, tokensStartPosition: firstCaptureStart, tokensEndPosition: firstCaptureEnd }
getNextMatch: (line, position) ->
nextMatch = null
matchedPattern = null
for pattern in @patterns
{ pattern, match } = pattern.getNextMatch(line, position)
if match
if !nextMatch or match.position < nextMatch.position
nextMatch = match
matchedPattern = pattern
{ match: nextMatch, pattern: matchedPattern }
class Pattern
grammar: null
pushRule: null
popRule: false
scopeName: null
regex: null
captures: null
constructor: (@grammar, { name, contentName, @include, match, begin, end, captures, beginCaptures, endCaptures, patterns, @popRule}) ->
@scopeName = name ? contentName # TODO: We need special treatment of contentName
if match
@regex = new OnigRegExp(match)
@captures = captures
else if begin
@regex = new OnigRegExp(begin)
@captures = beginCaptures ? captures
endPattern = new Pattern(@grammar, { match: end, captures: endCaptures ? captures, popRule: true})
@pushRule = new Rule(@grammar, { @scopeName, patterns, endPattern })
getRegexPatternPairs: (included) ->
if @include
rule = @grammar.ruleForInclude(@include)
# console.log "Could not find rule for include #{@include} in #{@grammar.name} grammar" unless rule
rule?.getRegexPatternPairs(included) ? []
else
[[@regex, this]]
compileRegex: ->
@pushRule?.compileRegex()
getNextMatch: (line, position) ->
if @include
rule = @grammar.ruleForInclude(@include)
rule.getNextMatch(line, position)
else
{ match: @regex.getCaptureIndices(line, position), pattern: this }
handleMatch: (stack, line, captureIndices) ->
scopes = _.pluck(stack, "scopeName")
scopes.push(@scopeName) unless @popRule
if @captures
tokens = @getTokensForCaptureIndices(line, captureIndices, scopes)
else
[start, end] = captureIndices[1..2]
tokens = [{ value: line[start...end], scopes: scopes }]
if @pushRule
stack.push(@pushRule)
else if @popRule
stack.pop()
tokens
getTokensForCaptureIndices: (line, captureIndices, scopes, indexOffset=captureIndices[0]) ->
[parentCaptureIndex, parentCaptureStart, parentCaptureEnd] = shiftCapture(captureIndices)
relativeParentCaptureIndex = parentCaptureIndex - indexOffset
tokens = []
if scope = @captures[relativeParentCaptureIndex]?.name
scopes = scopes.concat(scope)
previousChildCaptureEnd = parentCaptureStart
while captureIndices.length and captureIndices[1] < parentCaptureEnd
[childCaptureIndex, childCaptureStart, childCaptureEnd] = captureIndices
if childCaptureStart > previousChildCaptureEnd
tokens.push
value: line[previousChildCaptureEnd...childCaptureStart]
scopes: scopes
captureTokens = @getTokensForCaptureIndices(line, captureIndices, scopes, indexOffset)
tokens.push(captureTokens...)
previousChildCaptureEnd = childCaptureEnd
if parentCaptureEnd > previousChildCaptureEnd
tokens.push
value: line[previousChildCaptureEnd...parentCaptureEnd]
scopes: scopes
tokens
shiftCapture = (captureIndices) ->
[captureIndices.shift(), captureIndices.shift(), captureIndices.shift()]

View File

@@ -0,0 +1,108 @@
_ = require 'underscore'
fs = require 'fs'
plist = require 'plist'
module.exports =
class TextMateTheme
@themesByName: {}
@loadAll: ->
for themePath in fs.list(require.resolve("themes"))
@registerTheme(TextMateTheme.load(themePath))
@load: (path) ->
plistString = fs.read(require.resolve(path))
theme = null
plist.parseString plistString, (err, data) ->
throw new Error("Error loading theme at '#{path}': #{err}") if err
theme = new TextMateTheme(data[0])
theme
@registerTheme: (theme) ->
@themesByName[theme.name] = theme
@getNames: ->
_.keys(@themesByName)
@getTheme: (name) ->
@themesByName[name]
@activate: (name) ->
if theme = @getTheme(name)
theme.activate()
else
throw new Error("No theme with name '#{name}'")
constructor: ({@name, settings}) ->
@rulesets = []
globalSettings = settings[0]
@buildGlobalSettingsRulesets(settings[0])
@buildScopeSelectorRulesets(settings[1..])
activate: ->
applyStylesheet(@name, @getStylesheet())
getStylesheet: ->
lines = []
for {selector, properties} in @getRulesets()
lines.push("#{selector} {")
for name, value of properties
lines.push " #{name}: #{value};"
lines.push("}\n")
lines.join("\n")
getRulesets: -> @rulesets
buildGlobalSettingsRulesets: ({settings}) ->
{ background, foreground, caret, selection } = settings
@rulesets.push
selector: '.editor'
properties:
'background-color': @translateColor(background)
'color': @translateColor(foreground)
@rulesets.push
selector: '.editor.focused .cursor'
properties:
'border-color': @translateColor(caret)
@rulesets.push
selector: '.editor.focused .selection'
properties:
'background-color': @translateColor(selection)
buildScopeSelectorRulesets: (scopeSelectorSettings) ->
for { name, scope, settings } in scopeSelectorSettings
continue unless scope
@rulesets.push
comment: name
selector: @translateScopeSelector(scope)
properties: @translateScopeSelectorSettings(settings)
translateScopeSelector: (textmateScopeSelector) ->
scopes = textmateScopeSelector.replace(/\./g, '-').split(/\s+/).map (scope) -> '.' + scope
scopes.join(' ')
translateScopeSelectorSettings: ({ foreground, background, fontStyle }) ->
properties = {}
if fontStyle
fontStyles = fontStyle.split(/\s+/)
# properties['font-weight'] = 'bold' if _.contains(fontStyles, 'bold')
# properties['font-style'] = 'italic' if _.contains(fontStyles, 'italic')
properties['text-decoration'] = 'underline' if _.contains(fontStyles, 'underline')
properties['color'] = @translateColor(foreground) if foreground
properties['background-color'] = @translateColor(background) if background
properties
translateColor: (textmateColor) ->
if textmateColor.length <= 7
textmateColor
else
r = parseInt(textmateColor[1..2], 16)
g = parseInt(textmateColor[3..4], 16)
b = parseInt(textmateColor[5..6], 16)
a = parseInt(textmateColor[7..8], 16)
"rgba(#{r}, #{g}, #{b}, #{a})"

46
src/app/token.coffee Normal file
View File

@@ -0,0 +1,46 @@
_ = require 'underscore'
module.exports =
class Token
value: null
scopes: null
isAtomic: null
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @fold}) ->
@screenDelta = @value.length
@bufferDelta ?= @screenDelta
isEqual: (other) ->
@value == other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic == !!other.isAtomic
isBracket: ->
/^meta\.brace\b/.test(_.last(@scopes))
splitAt: (splitIndex) ->
value1 = @value.substring(0, splitIndex)
value2 = @value.substring(splitIndex)
[new Token(value: value1, scopes: @scopes), new Token(value: value2, scopes: @scopes)]
breakOutTabCharacters: (tabText) ->
return [this] unless /\t/.test(@value)
for substring in @value.match(/([^\t]+|\t)/g)
if substring == '\t'
@buildTabToken(tabText)
else
new Token(value: substring, scopes: @scopes)
buildTabToken: (tabText) ->
new Token(value: tabText, scopes: @scopes, bufferDelta: 1, isAtomic: true)
getCssClassString: ->
@cssClassString ?= @getCssClasses().join(' ')
getCssClasses: ->
classes = []
for scope in @scopes
scopeComponents = scope.split('.')
for i in [0...scopeComponents.length]
classes.push scopeComponents[0..i].join('-')
classes

View File

@@ -0,0 +1,153 @@
_ = require 'underscore'
ScreenLine = require 'screen-line'
EventEmitter = require 'event-emitter'
Token = require 'token'
Range = require 'range'
Point = require 'point'
module.exports =
class TokenizedBuffer
@idCounter: 1
languageMode: null
buffer: null
aceAdaptor: null
screenLines: null
constructor: (@buffer, { @languageMode, @tabText }) ->
@languageMode.tokenizedBuffer = this
@id = @constructor.idCounter++
@screenLines = @buildScreenLinesForRows(0, @buffer.getLastRow())
@buffer.on "change.tokenized-buffer#{@id}", (e) => @handleBufferChange(e)
handleBufferChange: (e) ->
oldRange = e.oldRange.copy()
newRange = e.newRange.copy()
previousStack = @stackForRow(oldRange.end.row) # used in spill detection below
stack = @stackForRow(newRange.start.row - 1)
@screenLines[oldRange.start.row..oldRange.end.row] =
@buildScreenLinesForRows(newRange.start.row, newRange.end.row, stack)
# 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 _.isEqual(@stackForRow(row), previousStack)
nextRow = row + 1
previousStack = @stackForRow(nextRow)
@screenLines[nextRow] = @buildScreenLineForRow(nextRow, @stackForRow(row))
# 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})
buildScreenLinesForRows: (startRow, endRow, startingStack) ->
stack = startingStack
for row in [startRow..endRow]
screenLine = @buildScreenLineForRow(row, stack)
stack = screenLine.stack
screenLine
buildScreenLineForRow: (row, stack) ->
line = @buffer.lineForRow(row)
{tokens, stack} = @languageMode.getLineTokens(line, stack)
tokenObjects = []
for tokenProperties in tokens
token = new Token(tokenProperties)
tokenObjects.push(token.breakOutTabCharacters(@tabText)...)
text = _.pluck(tokenObjects, 'value').join('')
new ScreenLine(tokenObjects, text, [1, 0], [1, 0], { stack })
lineForScreenRow: (row) ->
@screenLines[row]
linesForScreenRows: (startRow, endRow) ->
@screenLines[startRow..endRow]
stackForRow: (row) ->
@screenLines[row]?.stack
scopesForPosition: (position) ->
position = Point.fromObject(position)
token = @screenLines[position.row].tokenAtBufferColumn(position.column)
token.scopes
destroy: ->
@buffer.off ".tokenized-buffer#{@id}"
iterateTokensInBufferRange: (bufferRange, iterator) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
keepLooping = true
stop = -> keepLooping = false
for bufferRow in [start.row..end.row]
bufferColumn = 0
for token in @screenLines[bufferRow].tokens
startOfToken = new Point(bufferRow, bufferColumn)
iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken)
return unless keepLooping
bufferColumn += token.bufferDelta
backwardsIterateTokensInBufferRange: (bufferRange, iterator) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
keepLooping = true
stop = -> keepLooping = false
for bufferRow in [end.row..start.row]
bufferColumn = @buffer.lineLengthForRow(bufferRow)
for token in new Array(@screenLines[bufferRow].tokens...).reverse()
bufferColumn -= token.bufferDelta
startOfToken = new Point(bufferRow, bufferColumn)
iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken)
return unless keepLooping
findOpeningBracket: (startBufferPosition) ->
range = [[0,0], startBufferPosition]
position = null
depth = 0
@backwardsIterateTokensInBufferRange range, (token, startPosition, { stop }) ->
if token.isBracket()
if token.value == '}'
depth++
else if token.value == '{'
depth--
if depth == 0
position = startPosition
stop()
position
findClosingBracket: (startBufferPosition) ->
range = [startBufferPosition, @buffer.getEofPosition()]
position = null
depth = 0
@iterateTokensInBufferRange range, (token, startPosition, { stop }) ->
if token.isBracket()
if token.value == '{'
depth++
else if token.value == '}'
depth--
if depth == 0
position = startPosition
stop()
position
logLines: (start=0, end=@buffer.getLastRow()) ->
for row in [start..end]
line = @lineForScreenRow(row).text
console.log row, line, line.length
_.extend(TokenizedBuffer.prototype, EventEmitter)

View File

@@ -0,0 +1,46 @@
_ = require 'underscore'
module.exports =
class UndoManager
undoHistory: null
redoHistory: null
currentTransaction: null
constructor: ->
@startBatchCallCount = 0
@undoHistory = []
@redoHistory = []
pushOperation: (operation, editSession) ->
if @currentTransaction
@currentTransaction.push(operation)
else
@undoHistory.push([operation])
@redoHistory = []
operation.do?(editSession)
transact: (fn) ->
if @currentTransaction
fn()
else
@currentTransaction = []
fn()
@undoHistory.push(@currentTransaction) if @currentTransaction.length
@currentTransaction = null
undo: (editSession) ->
if batch = @undoHistory.pop()
opsInReverse = new Array(batch...)
opsInReverse.reverse()
op.undo?(editSession) for op in opsInReverse
@redoHistory.push batch
batch.oldSelectionRanges
redo: (editSession) ->
if batch = @redoHistory.pop()
for op in batch
op.do?(editSession)
op.redo?(editSession)
@undoHistory.push(batch)
batch.newSelectionRanges

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

@@ -0,0 +1,108 @@
# This a weirdo file. We don't create a Window class, we just add stuff to
# the DOM window.
Native = require 'native'
TextMateBundle = require 'text-mate-bundle'
TextMateTheme = require 'text-mate-theme'
fs = require 'fs'
_ = require 'underscore'
$ = require 'jquery'
{CoffeeScript} = require 'coffee-script'
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) ->
TextMateBundle.loadAll()
TextMateTheme.loadAll()
@attachRootView(path)
$(window).on 'close', => @close()
$(window).on 'beforeunload', =>
@shutdown()
false
$(window).focus()
atom.windowOpened this
shutdown: ->
@rootView.deactivate()
$(window).unbind('focus')
$(window).unbind('blur')
$(window).off('before')
atom.windowClosed this
# Note: RootView assigns itself on window on initialization so that
# window.rootView is available when loading user configuration
attachRootView: (pathToOpen) ->
if rootViewState = atom.rootViewStates[$windowNumber]
RootView.deserialize(JSON.parse(rootViewState))
else
new RootView(pathToOpen)
@rootView.open() unless pathToOpen
$(@rootViewParentSelector).append @rootView
requireStylesheet: (path) ->
fullPath = require.resolve(path)
window.applyStylesheet(fullPath, fs.read(fullPath))
applyStylesheet: (id, text) ->
unless $("head style[id='#{id}']").length
$('head').append "<style id='#{id}'>#{text}</style>"
requireExtension: (name) ->
extensionPath = require.resolve name
extension = rootView.activateExtension require(extensionPath)
extensionKeymapPath = fs.join(fs.directory(extensionPath), "keymap.coffee")
require extensionKeymapPath if fs.exists(extensionKeymapPath)
extension
reload: ->
if rootView.getModifiedBuffers().length > 0
message = "There are unsaved buffers, reload anyway?"
detailedMessage = "You will lose all unsaved changes if you reload"
buttons = [
["Reload", -> Native.reload()]
["Cancel", ->]
]
Native.alert(message, detailedMessage, buttons)
else
Native.reload()
toggleDevTools: ->
$native.toggleDevTools()
onerror: ->
$native.showDevTools()
measure: (description, fn) ->
start = new Date().getTime()
fn()
result = new Date().getTime() - start
console.log description, result
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'

View File

@@ -0,0 +1,2 @@
Atom = require 'atom'
window.atom = new Atom(atom.loadPath, $native)

View File

@@ -0,0 +1,216 @@
{View, $$} = require 'space-pen'
$ = require 'jquery'
_ = require 'underscore'
Range = require 'range'
Editor = require 'editor'
fuzzyFilter = require 'fuzzy-filter'
module.exports =
class Autocomplete extends View
@content: ->
@div class: 'autocomplete', tabindex: -1, =>
@ol outlet: 'matchesList'
@subview 'miniEditor', new Editor(mini: true)
editor: null
miniEditor: null
currentBuffer: null
wordList: null
wordRegex: /\w+/g
allMatches: null
filteredMatches: null
currentMatchIndex: null
isAutocompleting: false
originalSelectionBufferRange: null
originalSelectedText: null
@activate: (rootView) ->
new Autocomplete(editor) for editor in rootView.getEditors()
rootView.on 'editor-open', (e, editor) ->
editor.autoComplete = new Autocomplete(editor) unless editor.is('.autocomplete .mini')
initialize: (@editor) ->
requireStylesheet 'autocomplete.css'
@handleEvents()
@setCurrentBuffer(@editor.getBuffer())
handleEvents: ->
@editor.on 'editor-path-change', => @setCurrentBuffer(@editor.getBuffer())
@editor.on 'before-remove', => @currentBuffer?.off '.autocomplete'
@editor.on 'autocomplete:attach', => @attach()
@editor.on 'autocomplete:cancel', => @cancel()
@on 'autocomplete:confirm', => @confirm()
@matchesList.on 'mousedown', (e) =>
index = $(e.target).attr('index')
@selectMatchAtIndex(index) if index?
false
@matchesList.on 'mouseup', =>
if @selectedMatch()
@confirm()
else
@cancel()
@miniEditor.getBuffer().on 'change', (e) =>
@filterMatches() if @hasParent()
@miniEditor.preempt 'move-up', =>
@selectPreviousMatch()
false
@miniEditor.preempt 'move-down', =>
@selectNextMatch()
false
@miniEditor.preempt 'textInput', (e) =>
text = e.originalEvent.data
unless text.match(@wordRegex)
@confirm()
@editor.insertText(text)
false
setCurrentBuffer: (buffer) ->
@currentBuffer?.off '.autocomplete'
@currentBuffer = buffer
@buildWordList()
@currentBuffer.on 'change.autocomplete', (e) =>
@buildWordList() unless @isAutocompleting
buildWordList: () ->
wordHash = {}
matches = @currentBuffer.getText().match(@wordRegex)
wordHash[word] ?= true for word in (matches or [])
@wordList = Object.keys(wordHash)
confirm: ->
@confirmed = true
@editor.getSelection().clear()
@detach()
return unless match = @selectedMatch()
position = @editor.getCursorBufferPosition()
@editor.setCursorBufferPosition([position.row, position.column + match.suffix.length])
cancel: ->
@detach()
@editor.getBuffer().change(@currentMatchBufferRange, @originalSelectedText) if @currentMatchBufferRange
@editor.setSelectedBufferRange(@originalSelectionBufferRange)
attach: ->
@confirmed = false
@miniEditor.on 'focusout', =>
@cancel() unless @confirmed
@originalSelectedText = @editor.getSelectedText()
@originalSelectionBufferRange = @editor.getSelection().getBufferRange()
@currentMatchBufferRange = null
@allMatches = @findMatchesForCurrentSelection()
originalCursorPosition = @editor.getCursorScreenPosition()
@filterMatches()
@editor.append(this)
@setPosition(originalCursorPosition)
@miniEditor.focus()
detach: ->
@miniEditor.off("focusout")
super
@editor.off(".autocomplete")
@editor.focus()
@miniEditor.setText('')
setPosition: (originalCursorPosition) ->
{ left, top } = @editor.pixelPositionForScreenPosition(originalCursorPosition)
top -= @editor.scrollTop()
potentialTop = top + @editor.lineHeight
potentialBottom = potentialTop + @outerHeight()
if potentialBottom > @editor.outerHeight()
@css(left: left, bottom: @editor.outerHeight() - top, top: 'inherit')
else
@css(left: left, top: potentialTop, bottom: 'inherit')
selectPreviousMatch: ->
previousIndex = @currentMatchIndex - 1
previousIndex = @filteredMatches.length - 1 if previousIndex < 0
@selectMatchAtIndex(previousIndex)
selectNextMatch: ->
nextIndex = (@currentMatchIndex + 1) % @filteredMatches.length
@selectMatchAtIndex(nextIndex)
selectMatchAtIndex: (index) ->
@currentMatchIndex = index
@matchesList.find("li").removeClass "selected"
liToSelect = @matchesList.find("li:eq(#{index})")
liToSelect.addClass "selected"
topOfLiToSelect = liToSelect.position().top + @matchesList.scrollTop()
bottomOfLiToSelect = topOfLiToSelect + liToSelect.outerHeight()
if topOfLiToSelect < @matchesList.scrollTop()
@matchesList.scrollTop(topOfLiToSelect)
else if bottomOfLiToSelect > @matchesList.scrollBottom()
@matchesList.scrollBottom(bottomOfLiToSelect)
@replaceSelectedTextWithMatch @selectedMatch()
selectedMatch: ->
@filteredMatches[@currentMatchIndex]
filterMatches: ->
@filteredMatches = fuzzyFilter(@allMatches, @miniEditor.getText(), key: 'word')
@renderMatchList()
renderMatchList: ->
@matchesList.empty()
if @filteredMatches.length > 0
@matchesList.append($$ -> @li match.word, index: index) for match, index in @filteredMatches
else
@matchesList.append($$ -> @li "No matches found")
@selectMatchAtIndex(0) if @filteredMatches.length > 0
findMatchesForCurrentSelection: ->
selection = @editor.getSelection()
{prefix, suffix} = @prefixAndSuffixOfSelection(selection)
if (prefix.length + suffix.length) > 0
regex = new RegExp("^#{prefix}(.+)#{suffix}$", "i")
currentWord = prefix + @editor.getSelectedText() + suffix
for word in @wordList when regex.test(word) and word != currentWord
match = regex.exec(word)
{prefix, suffix, word, infix: match[1]}
else
[]
replaceSelectedTextWithMatch: (match) ->
selection = @editor.getSelection()
startPosition = selection.getBufferRange().start
@isAutocompleting = true
@editor.insertText(match.infix)
@currentMatchBufferRange = [startPosition, [startPosition.row, startPosition.column + match.infix.length]]
@editor.setSelectedBufferRange(@currentMatchBufferRange)
@isAutocompleting = false
prefixAndSuffixOfSelection: (selection) ->
selectionRange = selection.getBufferRange()
lineRange = [[selectionRange.start.row, 0], [selectionRange.end.row, @editor.lineLengthForBufferRow(selectionRange.end.row)]]
[prefix, suffix] = ["", ""]
@currentBuffer.scanInRange @wordRegex, lineRange, (match, range, {stop}) ->
stop() if range.start.isGreaterThan(selectionRange.end)
if range.intersectsWith(selectionRange)
prefixOffset = selectionRange.start.column - range.start.column
suffixOffset = selectionRange.end.column - range.end.column
prefix = match[0][0...prefixOffset] if range.start.isLessThan(selectionRange.start)
suffix = match[0][suffixOffset..] if range.end.isGreaterThan(selectionRange.end)
{prefix, suffix}

View File

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

View File

@@ -0,0 +1,8 @@
window.keymap.bindKeys '.editor',
'escape': 'autocomplete:attach'
'ctrl-space': 'autocomplete:attach'
window.keymap.bindKeys '.autocomplete .editor',
'enter': 'autocomplete:confirm'
'escape': 'autocomplete:cancel'
'ctrl-space': 'autocomplete:cancel'

View File

@@ -0,0 +1,18 @@
fs = require 'fs'
PEG = require 'pegjs'
module.exports =
class CommandInterpreter
constructor: (@project) ->
@parser = PEG.buildParser(fs.read(require.resolve 'command-panel/commands.pegjs'))
eval: (string, activeEditSession) ->
compositeCommand = @parser.parse(string)
@lastRelativeAddress = compositeCommand if compositeCommand.isRelativeAddress()
compositeCommand.execute(@project, activeEditSession)
repeatRelativeAddress: (activeEditSession) ->
@lastRelativeAddress?.execute(@project, activeEditSession)
repeatRelativeAddressInReverse: (activeEditSession) ->
@lastRelativeAddress?.reverse().execute(@project, activeEditSession)

View File

@@ -0,0 +1,138 @@
{View, $$$} = require 'space-pen'
CommandInterpreter = require 'command-panel/command-interpreter'
RegexAddress = require 'command-panel/commands/regex-address'
CompositeCommand = require 'command-panel/commands/composite-command'
PreviewList = require 'command-panel/preview-list'
Editor = require 'editor'
{SyntaxError} = require('pegjs').parser
_ = require 'underscore'
module.exports =
class CommandPanel extends View
@activate: (rootView, state) ->
requireStylesheet 'command-panel.css'
if state?
@instance = CommandPanel.deserialize(state, rootView)
else
@instance = new CommandPanel(rootView)
@deactivate: ->
@instance.destroy()
@serialize: ->
text: @instance.miniEditor.getText()
visible: @instance.hasParent()
miniEditorFocused: @instance.miniEditor.isFocused
@deserialize: (state, rootView) ->
commandPanel = new CommandPanel(rootView)
commandPanel.attach(state.text, focus: false) if state.visible
commandPanel.miniEditor.focus() if state.miniEditorFocused
commandPanel
@content: (rootView) ->
@div class: 'command-panel', =>
@subview 'previewList', new PreviewList(rootView)
@div class: 'prompt-and-editor', =>
@div ':', class: 'prompt', outlet: 'prompt'
@subview 'miniEditor', new Editor(mini: true)
commandInterpreter: null
history: null
historyIndex: 0
initialize: (@rootView)->
@commandInterpreter = new CommandInterpreter(@rootView.project)
@history = []
@on 'command-panel:unfocus', => @rootView.focus()
@on 'command-panel:close', => @detach()
@rootView.on 'command-panel:toggle', => @toggle()
@rootView.on 'command-panel:toggle-preview', => @togglePreview()
@rootView.on 'command-panel:execute', => @execute()
@rootView.on 'command-panel:find-in-file', => @attach("/")
@rootView.on 'command-panel:find-in-project', => @attach("Xx/")
@rootView.on 'command-panel:repeat-relative-address', => @repeatRelativeAddress()
@rootView.on 'command-panel:repeat-relative-address-in-reverse', => @repeatRelativeAddressInReverse()
@rootView.on 'command-panel:set-selection-as-regex-address', => @setSelectionAsLastRelativeAddress()
@miniEditor.off 'move-up move-down'
@miniEditor.on 'move-up', => @navigateBackwardInHistory()
@miniEditor.on 'move-down', => @navigateForwardInHistory()
@previewList.hide()
destroy: ->
@previewList.destroy()
toggle: ->
if @miniEditor.isFocused
@detach()
@rootView.focus()
else
@attach() unless @hasParent()
@miniEditor.focus()
togglePreview: ->
if @previewList.is(':focus')
@previewList.hide()
@detach()
@rootView.focus()
else
@attach() unless @hasParent()
if @previewList.hasOperations()
@previewList.show().focus()
else
@miniEditor.focus()
attach: (text='', options={}) ->
focus = options.focus ? true
@rootView.vertical.append(this)
@miniEditor.focus() if focus
@miniEditor.setText(text)
@miniEditor.setCursorBufferPosition([0, Infinity])
detach: ->
@rootView.focus()
@previewList.hide()
super
execute: (command = @miniEditor.getText()) ->
try
@commandInterpreter.eval(command, @rootView.getActiveEditSession()).done (operationsToPreview) =>
@history.push(command)
@historyIndex = @history.length
if operationsToPreview?.length
@previewList.populate(operationsToPreview)
@previewList.focus()
else
@detach()
catch error
if error.name is "SyntaxError"
@flashError()
return
else
throw error
navigateBackwardInHistory: ->
return if @historyIndex == 0
@historyIndex--
@miniEditor.setText(@history[@historyIndex])
navigateForwardInHistory: ->
return if @historyIndex == @history.length
@historyIndex++
@miniEditor.setText(@history[@historyIndex] or '')
repeatRelativeAddress: ->
@commandInterpreter.repeatRelativeAddress(@rootView.getActiveEditSession())
repeatRelativeAddressInReverse: ->
@commandInterpreter.repeatRelativeAddressInReverse(@rootView.getActiveEditSession())
setSelectionAsLastRelativeAddress: ->
selection = @rootView.getActiveEditor().getSelectedText()
regex = _.escapeRegExp(selection)
@commandInterpreter.lastRelativeAddress = new CompositeCommand([new RegexAddress(regex)])

View File

@@ -0,0 +1,58 @@
{
var CompositeCommand = require('command-panel/commands/composite-command')
var Substitution = require('command-panel/commands/substitution');
var ZeroAddress = require('command-panel/commands/zero-address');
var EofAddress = require('command-panel/commands/eof-address');
var LineAddress = require('command-panel/commands/line-address');
var AddressRange = require('command-panel/commands/address-range');
var CurrentSelectionAddress = require('command-panel/commands/current-selection-address')
var RegexAddress = require('command-panel/commands/regex-address')
var SelectAllMatches = require('command-panel/commands/select-all-matches')
var SelectAllMatchesInProject = require('command-panel/commands/select-all-matches-in-project')
}
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 ZeroAddress()
if (!end) end = new EofAddress()
return new AddressRange(start, end)
}
primitiveAddress
= '0' { return new ZeroAddress() }
/ '$' { return new EofAddress() }
/ '.' { return new CurrentSelectionAddress() }
/ lineNumber:integer { return new LineAddress(lineNumber) }
/ regexAddress
regexAddress
= reverse:'-'? '/' pattern:pattern '/'? { return new RegexAddress(pattern, reverse.length > 0)}
command = substitution / selectAllMatches / selectAllMatchesInProject
substitution
= "s" _ "/" find:pattern "/" replace:pattern "/" _ options:[g]* {
return new Substitution(find, replace, options);
}
selectAllMatches
= 'x' _ '/' pattern:pattern '/'? { return new SelectAllMatches(pattern) }
selectAllMatchesInProject
= 'X' _ 'x' _ '/' pattern:pattern '/'? { return new SelectAllMatchesInProject(pattern) }
pattern
= pattern:[^/]* { return pattern.join('') }
integer
= digits:[0-9]+ { return parseInt(digits.join('')); }
_ = " "*

View File

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

View File

@@ -0,0 +1,17 @@
Command = require 'command-panel/commands/command'
Operation = require 'command-panel/operation'
$ = require 'jquery'
module.exports =
class Address extends Command
compile: (project, buffer, ranges) ->
deferred = $.Deferred()
deferred.resolve ranges.map (range) =>
new Operation
project: project
buffer: buffer
bufferRange: @getRange(buffer, range)
deferred.promise()
isAddress: -> true

View File

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

View File

@@ -0,0 +1,43 @@
_ = require 'underscore'
$ = require 'jquery'
module.exports =
class CompositeCommand
constructor: (@subcommands) ->
execute: (project, editSession) ->
currentRanges = editSession?.getSelectedBufferRanges()
@executeCommands(@subcommands, project, editSession, currentRanges)
executeCommands: (commands, project, editSession, ranges) ->
deferred = $.Deferred()
[currentCommand, remainingCommands...] = commands
currentCommand.compile(project, editSession?.buffer, ranges).done (operations) =>
if remainingCommands.length
nextRanges = operations.map (operation) ->
operation.destroy()
operation.getBufferRange()
@executeCommands(remainingCommands, project, editSession, nextRanges).done ->
deferred.resolve()
else
if currentCommand.previewOperations
deferred.resolve(operations)
else
bufferRanges = []
for operation in operations
bufferRange = operation.execute(editSession)
bufferRanges.push(bufferRange) if bufferRange
operation.destroy()
if bufferRanges.length and not currentCommand.preserveSelections
editSession.setSelectedBufferRanges(bufferRanges)
deferred.resolve()
deferred.promise()
reverse: ->
new CompositeCommand(@subcommands.map (command) -> command.reverse())
isRelativeAddress: ->
_.all(@subcommands, (command) -> command.isAddress() and command.isRelative())

View File

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

View File

@@ -0,0 +1,10 @@
Address = require 'command-panel/commands/address'
Range = require 'range'
module.exports =
class EofAddress extends Address
getRange: (buffer, range) ->
eof = buffer.getEofPosition()
new Range(eof, eof)
isRelative: -> false

View File

@@ -0,0 +1,12 @@
Address = require 'command-panel/commands/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,36 @@
Address = require 'command-panel/commands/address'
Range = require 'range'
module.exports =
class RegexAddress extends Address
regex: null
reverse: null
constructor: (pattern, isReversed) ->
@isReversed = isReversed
@regex = new RegExp(pattern)
getRange: (buffer, range) ->
rangeBefore = new Range([0, 0], range.start)
rangeAfter = new Range(range.end, buffer.getEofPosition())
rangeToSearch = if @isReversed then rangeBefore else rangeAfter
rangeToReturn = null
scanMethodName = if @isReversed then "backwardsScanInRange" else "scanInRange"
buffer[scanMethodName] @regex, rangeToSearch, (match, range) ->
rangeToReturn = range
if rangeToReturn
rangeToReturn
else
rangeToSearch = if @isReversed then rangeAfter else rangeBefore
buffer[scanMethodName] @regex, rangeToSearch, (match, range) ->
rangeToReturn = range
rangeToReturn or range
isRelative: -> true
reverse: ->
new RegexAddress(@regex, !@isReversed)

View File

@@ -0,0 +1,24 @@
Command = require 'command-panel/commands/command'
Operation = require 'command-panel/operation'
$ = require 'jquery'
module.exports =
class SelectAllMatchesInProject extends Command
regex: null
previewOperations: true
constructor: (pattern) ->
@regex = new RegExp(pattern, 'g')
compile: (project, buffer, range) ->
deferred = $.Deferred()
operations = []
promise = project.scan @regex, ({path, range}) ->
operations.push(new Operation(
project: project
buffer: project.bufferForPath(path)
bufferRange: range
))
promise.done -> deferred.resolve(operations)
deferred.promise()

View File

@@ -0,0 +1,23 @@
Command = require 'command-panel/commands/command'
Operation = require 'command-panel/operation'
$ = require 'jquery'
module.exports =
class SelectAllMatches extends Command
regex: null
constructor: (pattern) ->
@regex = new RegExp(pattern, 'g')
compile: (project, buffer, ranges) ->
deferred = $.Deferred()
operations = []
for range in ranges
buffer.scanInRange @regex, range, (match, matchRange) ->
operations.push(new Operation(
project: project
buffer: buffer
bufferRange: matchRange
))
deferred.resolve(operations)
deferred.promise()

View File

@@ -0,0 +1,28 @@
Command = require 'command-panel/commands/command'
Operation = require 'command-panel/operation'
$ = require 'jquery'
module.exports =
class Substitution extends Command
regex: null
replacementText: null
preserveSelections: true
constructor: (pattern, replacementText, options) ->
@replacementText = replacementText
@regex = new RegExp(pattern, options.join(''))
compile: (project, buffer, ranges) ->
deferred = $.Deferred()
operations = []
for range in ranges
buffer.scanInRange @regex, range, (match, matchRange, { replace }) =>
operations.push(new Operation(
project: project
buffer: buffer
bufferRange: matchRange
newText: @replacementText
preserveSelection: true
))
deferred.resolve(operations)
deferred.promise()

View File

@@ -0,0 +1,9 @@
Address = require 'command-panel/commands/address'
Range = require 'range'
module.exports =
class ZeroAddress extends Address
getRange: ->
new Range([0, 0], [0, 0])
isRelative: -> false

View File

@@ -0,0 +1 @@
module.exports = require 'command-panel/command-panel'

View File

@@ -0,0 +1,17 @@
window.keymap.bindKeys '*'
'ctrl-0': 'command-panel:toggle'
'ctrl-2': 'command-panel:toggle-preview'
'meta-:': 'command-panel:toggle'
'meta-F': 'command-panel:find-in-project'
window.keymap.bindKeys '.command-panel .preview-list, .command-panel .editor input',
'meta-w': 'command-panel:close'
escape: 'command-panel:unfocus'
enter: 'command-panel:execute'
window.keymap.bindKeys '.editor',
'meta-g': 'command-panel:repeat-relative-address'
'meta-G': 'command-panel:repeat-relative-address-in-reverse'
'meta-e': 'command-panel:set-selection-as-regex-address'
'meta-f': 'command-panel:find-in-file'
'meta-F': 'command-panel:find-in-project'

View File

@@ -0,0 +1,30 @@
{$$$} = require 'space-pen'
module.exports =
class Operation
constructor: ({@project, @buffer, bufferRange, @newText, @preserveSelection}) ->
@buffer.retain()
@anchorRange = @buffer.addAnchorRange(bufferRange)
getPath: ->
@project.relativize(@buffer.getPath())
getBufferRange: ->
@anchorRange.getBufferRange()
execute: (editSession) ->
@buffer.change(@getBufferRange(), @newText) if @newText
@getBufferRange() unless @preserveSelection
preview: ->
range = @anchorRange.getBufferRange()
line = @buffer.lineForRow(range.start.row)
prefix = line[0...range.start.column]
match = line[range.start.column...range.end.column]
suffix = line[range.end.column..]
{prefix, suffix, match}
destroy: ->
@buffer.release()
@anchorRange.destroy()

View File

@@ -0,0 +1,83 @@
$ = require 'jquery'
{$$$, View} = require 'space-pen'
module.exports =
class PreviewList extends View
@content: ->
@ol class: 'preview-list', tabindex: -1, ->
selectedOperationIndex: 0
operations: null
initialize: (@rootView) ->
@on 'move-down', => @selectNextOperation()
@on 'move-up', => @selectPreviousOperation()
@on 'command-panel:execute', => @executeSelectedOperation()
@on 'mousedown', 'li', (e) =>
@setSelectedOperationIndex(parseInt($(e.target).closest('li').data('index')))
@executeSelectedOperation()
destroy: ->
@destroyOperations() if @operations
hasOperations: -> @operations?
populate: (operations) ->
@destroyOperations() if @operations
@operations = operations
@empty()
@html $$$ ->
for operation, index in operations
{prefix, suffix, match} = operation.preview()
@li 'data-index': index, =>
@span operation.getPath(), outlet: "path", class: "path"
@span outlet: "preview", class: "preview", =>
@span prefix
@span match, class: 'match'
@span suffix
@setSelectedOperationIndex(0)
@show()
selectNextOperation: ->
@setSelectedOperationIndex(@selectedOperationIndex + 1)
selectPreviousOperation: ->
@setSelectedOperationIndex(@selectedOperationIndex - 1)
setSelectedOperationIndex: (index) ->
index = Math.max(0, index)
index = Math.min(@operations.length - 1, index)
@children(".selected").removeClass('selected')
element = @children("li:eq(#{index})")
element.addClass('selected')
@scrollToElement(element)
@selectedOperationIndex = index
executeSelectedOperation: ->
operation = @getSelectedOperation()
editSession = @rootView.open(operation.getPath())
bufferRange = operation.execute(editSession)
editSession.setSelectedBufferRange(bufferRange) if bufferRange
@rootView.focus()
false
getOperations: ->
new Array(@operations...)
destroyOperations: ->
operation.destroy() for operation in @getOperations()
@operations = null
getSelectedOperation: ->
@operations[@selectedOperationIndex]
scrollToElement: (element) ->
top = @scrollTop() + element.position().top
bottom = top + element.outerHeight()
if bottom > @scrollBottom()
@scrollBottom(bottom)
if top < @scrollTop()
@scrollTop(top)

View File

@@ -0,0 +1,110 @@
{View, $$} = require 'space-pen'
stringScore = require 'stringscore'
fuzzyFilter = require 'fuzzy-filter'
$ = require 'jquery'
Editor = require 'editor'
module.exports =
class FuzzyFinder extends View
@activate: (rootView) ->
@instance = new FuzzyFinder(rootView)
@content: ->
@div class: 'fuzzy-finder', =>
@ol outlet: 'pathList'
@subview 'miniEditor', new Editor(mini: true)
paths: null
allowActiveEditorChange: null
maxResults: null
initialize: (@rootView) ->
requireStylesheet 'fuzzy-finder.css'
@maxResults = 10
@rootView.on 'fuzzy-finder:toggle-file-finder', => @toggleFileFinder()
@rootView.on 'fuzzy-finder:toggle-buffer-finder', => @toggleBufferFinder()
@on 'fuzzy-finder:cancel', => @detach()
@on 'move-up', => @moveUp()
@on 'move-down', => @moveDown()
@on 'fuzzy-finder:select-path', => @select()
@on 'mousedown', 'li', (e) => @entryClicked(e)
@miniEditor.getBuffer().on 'change', => @populatePathList() if @hasParent()
@miniEditor.off 'move-up move-down'
toggleFileFinder: ->
if @hasParent()
@detach()
else
return unless @rootView.project.getPath()?
@allowActiveEditorChange = false
@populateProjectPaths()
@attach()
toggleBufferFinder: ->
if @hasParent()
@detach()
else
@allowActiveEditorChange = true
@populateOpenBufferPaths()
@attach() if @paths?.length
populateProjectPaths: ->
@rootView.project.getFilePaths().done (@paths) => @populatePathList()
populateOpenBufferPaths: ->
@paths = @rootView.getOpenBufferPaths().map (path) =>
@rootView.project.relativize(path)
@populatePathList() if @paths?.length
attach: ->
@rootView.append(this)
@miniEditor.focus()
@miniEditor.on 'focusout', => @detach()
detach: ->
@miniEditor.off 'focusout'
@rootView.focus()
super
@miniEditor.setText('')
populatePathList: ->
@pathList.empty()
for path in @findMatches(@miniEditor.getText())
@pathList.append $$ -> @li path
@pathList.children('li:first').addClass 'selected'
findSelectedLi: ->
@pathList.children('li.selected')
open : (path) ->
return unless path.length
@rootView.open(path, {@allowActiveEditorChange})
@detach()
select: ->
@open(@findSelectedLi().text())
entryClicked: (e) ->
@open($(e.currentTarget).text())
false
moveUp: ->
@findSelectedLi()
.filter(':not(:first-child)')
.removeClass('selected')
.prev()
.addClass('selected')
moveDown: ->
@findSelectedLi()
.filter(':not(:last-child)')
.removeClass('selected')
.next()
.addClass('selected')
findMatches: (query) ->
fuzzyFilter(@paths, query, maxResults: @maxResults)

View File

@@ -0,0 +1 @@
module.exports = require 'fuzzy-finder/fuzzy-finder'

View File

@@ -0,0 +1,8 @@
window.keymap.bindKeys '*'
'meta-t': 'fuzzy-finder:toggle-file-finder'
'meta-b': 'fuzzy-finder:toggle-buffer-finder'
window.keymap.bindKeys ".fuzzy-finder .editor input",
'enter': 'fuzzy-finder:select-path',
'escape': 'fuzzy-finder:cancel'
'meta-w': 'fuzzy-finder:cancel'

View File

@@ -0,0 +1 @@
module.exports = require 'keybindings-view/keybindings-view'

View File

@@ -0,0 +1,31 @@
{View, $$} = require 'space-pen'
module.exports =
class KeybindingsView extends View
@activate: (rootView, state) ->
requireStylesheet 'keybinding-view.css'
@instance = new this(rootView)
@content: (rootView) ->
@div class: 'keybindings-view', tabindex: -1, =>
@ul outlet: 'keybindingList'
initialize: (@rootView) ->
@rootView.on 'keybindings-view:attach', => @attach()
@on 'keybindings-view:detach', => @detach()
attach: ->
@keybindingList.empty()
@keybindingList.append $$ ->
for keystroke, command of rootView.activeKeybindings()
@li =>
@span class: 'keystroke', "#{keystroke}"
@span ":"
@span "#{command}"
@rootView.append(this)
@focus()
detach: ->
super()
@rootView.focus()

View File

@@ -0,0 +1,5 @@
window.keymap.bindKeys '*',
'ctrl-?': 'keybindings-view:attach'
window.keymap.bindKeys ".keybindings-view",
'escape': 'keybindings-view:detach'

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
module.exports =
class SnippetExpansion
tabStopAnchorRanges: null
constructor: (snippet, @editSession) ->
@editSession.selectToBeginningOfWord()
startPosition = @editSession.getCursorBufferPosition()
@editSession.transact =>
@editSession.insertText(snippet.body, autoIndent: false)
if snippet.tabStops.length
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
if snippet.lineCount > 1
@indentSubsequentLines(startPosition.row, snippet)
placeTabStopAnchorRanges: (startPosition, tabStopRanges) ->
@tabStopAnchorRanges = tabStopRanges.map ({start, end}) =>
@editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)])
@setTabStopIndex(0)
indentSubsequentLines: (startRow, snippet) ->
initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0]
for row in [startRow + 1...startRow + snippet.lineCount]
@editSession.buffer.insert([row, 0], initialIndent)
goToNextTabStop: ->
nextIndex = @tabStopIndex + 1
if @cursorIsInsideTabStops() and nextIndex < @tabStopAnchorRanges.length
@setTabStopIndex(nextIndex)
true
else
@destroy()
false
goToPreviousTabStop: ->
if @cursorIsInsideTabStops()
@setTabStopIndex(@tabStopIndex - 1) if @tabStopIndex > 0
true
else
@destroy()
false
ensureValidTabStops: ->
@tabStopAnchorRanges? and @destroyIfCursorIsOutsideTabStops()
setTabStopIndex: (@tabStopIndex) ->
@editSession.setSelectedBufferRange(@tabStopAnchorRanges[@tabStopIndex].getBufferRange())
cursorIsInsideTabStops: ->
position = @editSession.getCursorBufferPosition()
for anchorRange in @tabStopAnchorRanges
return true if anchorRange.containsBufferPosition(position)
false
destroy: ->
anchorRange.destroy() for anchorRange in @tabStopAnchorRanges
@editSession.snippetExpansion = null
restore: (@editSession) ->
@editSession.snippetExpansion = this
@tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) =>
@editSession.addAnchorRange(anchorRange.getBufferRange())

View File

@@ -0,0 +1,36 @@
_ = require 'underscore'
Range = require 'range'
module.exports =
class Snippet
body: null
lineCount: null
tabStops: null
constructor: ({@bodyPosition, @prefix, @description, body}) ->
@body = @extractTabStops(body)
extractTabStops: (bodyLines) ->
tabStopsByIndex = {}
bodyText = []
[row, column] = [0, 0]
for bodyLine, i in bodyLines
lineText = []
for segment in bodyLine
if segment.index
{ index, placeholderText } = segment
tabStopsByIndex[index] = new Range([row, column], [row, column + placeholderText.length])
lineText.push(placeholderText)
else
lineText.push(segment)
column += segment.length
bodyText.push(lineText.join(''))
row++; column = 0
@lineCount = row
@tabStops = []
for index in _.keys(tabStopsByIndex).sort()
@tabStops.push tabStopsByIndex[index]
bodyText.join('\n')

View File

@@ -0,0 +1,46 @@
fs = require 'fs'
PEG = require 'pegjs'
_ = require 'underscore'
SnippetExpansion = require 'snippets/snippet-expansion'
module.exports =
name: 'Snippets'
snippetsByExtension: {}
snippetsParser: PEG.buildParser(fs.read(require.resolve 'extensions/snippets/snippets.pegjs'), trackLineAndColumn: true)
activate: (@rootView) ->
@loadSnippets()
@rootView.on 'editor-open', (e, editor) => @enableSnippetsInEditor(editor)
loadSnippets: ->
snippetsDir = fs.join(atom.configDirPath, 'snippets')
if fs.exists(snippetsDir)
@loadSnippetsFile(path) for path in fs.list(snippetsDir) when fs.extension(path) == '.snippets'
loadSnippetsFile: (path) ->
@evalSnippets(fs.base(path, '.snippets'), fs.read(path))
evalSnippets: (extension, text) ->
@snippetsByExtension[extension] = @snippetsParser.parse(text)
enableSnippetsInEditor: (editor) ->
editor.on 'snippets:expand', (e) =>
editSession = editor.activeEditSession
prefix = editSession.getLastCursor().getCurrentWordPrefix()
if snippet = @snippetsByExtension[editSession.getFileExtension()]?[prefix]
editSession.transact ->
snippetExpansion = new SnippetExpansion(snippet, editSession)
editSession.snippetExpansion = snippetExpansion
editSession.pushOperation
undo: -> snippetExpansion.destroy()
redo: (editSession) -> snippetExpansion.restore(editSession)
else
e.abortKeyBinding()
editor.on 'snippets:next-tab-stop', (e) ->
unless editor.activeEditSession.snippetExpansion?.goToNextTabStop()
e.abortKeyBinding()
editor.on 'snippets:previous-tab-stop', (e) ->
unless editor.activeEditSession.snippetExpansion?.goToPreviousTabStop()
e.abortKeyBinding()

View File

@@ -0,0 +1,39 @@
{
var Snippet = require('extensions/snippets/snippet');
var Point = require('point');
}
snippets = snippets:snippet+ ws? {
var snippetsByPrefix = {};
snippets.forEach(function(snippet) {
snippetsByPrefix[snippet.prefix] = snippet
});
return snippetsByPrefix;
}
snippet = ws? start ws prefix:prefix ws description:string bodyPosition:beforeBody body:body end {
return new Snippet({ bodyPosition: bodyPosition, prefix: prefix, description: description, body: body });
}
start = 'snippet'
prefix = prefix:[A-Za-z0-9_]+ { return prefix.join(''); }
string = ['] body:[^']* ['] { return body.join(''); }
/ ["] body:[^"]* ["] { return body.join(''); }
beforeBody = [ ]* '\n' { return new Point(line, 0); } // return start position of body: body begins on next line, so don't subtract 1 from line
body = bodyLine+
bodyLine = content:(tabStop / bodyText)* '\n' { return content; }
bodyText = text:bodyChar+ { return text.join(''); }
bodyChar = !(end / tabStop) char:[^\n] { return char; }
tabStop = simpleTabStop / tabStopWithPlaceholder
simpleTabStop = '$' index:[0-9]+ {
return { index: parseInt(index), placeholderText: '' };
}
tabStopWithPlaceholder = '${' index:[0-9]+ ':' placeholderText:[^}]* '}' {
return { index: parseInt(index), placeholderText: placeholderText.join('') };
}
end = 'endsnippet'
ws = ([ \n] / comment)+
comment = '#' [^\n]*

View File

@@ -0,0 +1,14 @@
module.exports =
name: "strip trailing whitespace"
activate: (rootView) ->
for buffer in rootView.project.getBuffers()
@stripTrailingWhitespaceBeforeSave(buffer)
rootView.project.on 'new-buffer', (buffer) =>
@stripTrailingWhitespaceBeforeSave(buffer)
stripTrailingWhitespaceBeforeSave: (buffer) ->
buffer.on 'before-save', ->
buffer.scan /[ \t]+$/g, (match, range, { replace }) ->
replace('')

View File

@@ -0,0 +1,38 @@
{View, $$} = require 'space-pen'
Editor = require 'editor'
fs = require 'fs'
$ = require 'jquery'
module.exports =
class Dialog extends View
@content: ({prompt} = {}) ->
@div class: 'tree-view-dialog', =>
@div prompt, outlet: 'prompt'
@subview 'miniEditor', new Editor(mini: true)
initialize: ({path, @onConfirm, select} = {}) ->
@miniEditor.focus()
@on 'tree-view:confirm', => @confirm()
@on 'tree-view:cancel', => @cancel()
@miniEditor.on 'focusout', => @remove()
@miniEditor.setText(path)
if select
extension = fs.extension(path)
baseName = fs.base(path)
range = [[0, path.length - baseName.length], [0, path.length - extension.length]]
@miniEditor.setSelectedBufferRange(range)
confirm: ->
return if @onConfirm(@miniEditor.getText()) is false
@remove()
$('#root-view').focus()
cancel: ->
@remove()
$('.tree-view').focus()
showError: (message) ->
@prompt.text(message)
@prompt.flashError()

View File

@@ -0,0 +1,84 @@
{View, $$} = require 'space-pen'
FileView = require 'tree-view/file-view'
Directory = require 'directory'
$ = require 'jquery'
module.exports =
class DirectoryView extends View
@content: ({directory, isExpanded} = {}) ->
@li class: 'directory entry', =>
@div outlet: 'header', class: 'header', =>
@span '', class: 'disclosure-arrow', outlet: 'disclosureArrow'
@span directory.getBaseName(), class: 'name'
directory: null
entries: null
header: null
initialize: ({@directory, isExpanded} = {}) ->
@expand() if isExpanded
@disclosureArrow.on 'click', => @toggleExpansion()
getPath: ->
@directory.path
buildEntries: ->
@unwatchDescendantEntries()
@entries?.remove()
@entries = $$ -> @ol class: 'entries'
for entry in @directory.getEntries()
if entry instanceof Directory
@entries.append(new DirectoryView(directory: entry, isExpanded: false))
else
@entries.append(new FileView(entry))
@append(@entries)
toggleExpansion: ->
if @isExpanded then @collapse() else @expand()
expand: ->
return if @isExpanded
@addClass('expanded')
@disclosureArrow.text('')
@buildEntries()
@watchEntries()
@deserializeEntryExpansionStates(@entryStates) if @entryStates?
@isExpanded = true
false
collapse: ->
@entryStates = @serializeEntryExpansionStates()
@removeClass('expanded')
@disclosureArrow.text('')
@unwatchEntries()
@entries.remove()
@entries = null
@isExpanded = false
watchEntries: ->
@directory.on "contents-change.#{@directory.path}", =>
@buildEntries()
@trigger "tree-view:directory-modified"
unwatchEntries: ->
@unwatchDescendantEntries()
@directory.off ".#{@directory.path}"
unwatchDescendantEntries: ->
@find('.expanded.directory').each ->
$(this).view().unwatchEntries()
serializeEntryExpansionStates: ->
entryStates = {}
@entries?.find('> .directory.expanded').each ->
view = $(this).view()
entryStates[view.directory.getBaseName()] = view.serializeEntryExpansionStates()
entryStates
deserializeEntryExpansionStates: (entryStates) ->
for directoryName, childEntryStates of entryStates
@entries.find("> .directory:contains('#{directoryName}')").each ->
view = $(this).view()
view.entryStates = childEntryStates
view.expand()

View File

@@ -0,0 +1,14 @@
{View, $$} = require 'space-pen'
$ = require 'jquery'
module.exports =
class FileView extends View
@content: (file) ->
@li file.getBaseName(), class: 'file entry'
file: null
initialize: (@file) ->
getPath: ->
@file.path

View File

@@ -0,0 +1 @@
module.exports = require 'tree-view/tree-view'

View File

@@ -0,0 +1,18 @@
window.keymap.bindKeys '#root-view'
'ctrl-1': 'tree-view:toggle'
'ctrl-meta-1': 'tree-view:reveal-active-file'
window.keymap.bindKeys '.tree-view'
'escape': 'tree-view:unfocus'
'meta-w': 'tree-view:toggle'
'right': 'tree-view:expand-directory'
'left': 'tree-view:collapse-directory'
'enter': 'tree-view:open-selected-entry'
'm': 'tree-view:move'
'a': 'tree-view:add'
'delete': 'tree-view:remove'
'backspace': 'tree-view:remove'
window.keymap.bindKeys '.tree-view-dialog .mini.editor'
'enter': 'tree-view:confirm'
'escape': 'tree-view:cancel'

View File

@@ -0,0 +1,265 @@
{View, $$} = require 'space-pen'
Directory = require 'directory'
DirectoryView = require 'tree-view/directory-view'
FileView = require 'tree-view/file-view'
Dialog = require 'tree-view/dialog'
Native = require 'native'
fs = require 'fs'
$ = require 'jquery'
_ = require 'underscore'
module.exports =
class TreeView extends View
@activate: (rootView, state) ->
requireStylesheet 'tree-view.css'
if state
@instance = TreeView.deserialize(state, rootView)
else
@instance = new TreeView(rootView)
@instance.attach()
@deactivate: () ->
@instance.deactivate()
@serialize: ->
@instance.serialize()
@content: (rootView) ->
@div class: 'tree-view', tabindex: -1, =>
if rootView.project.getRootDirectory()
@subview 'root', new DirectoryView(directory: rootView.project.getRootDirectory(), isExpanded: true)
@deserialize: (state, rootView) ->
treeView = new TreeView(rootView)
treeView.root.deserializeEntryExpansionStates(state.directoryExpansionStates)
treeView.selectEntryForPath(state.selectedPath)
treeView.focusAfterAttach = state.hasFocus
treeView.attach() if state.attached
treeView
root: null
focusAfterAttach: false
initialize: (@rootView) ->
@on 'click', '.entry', (e) => @entryClicked(e)
@on 'move-up', => @moveUp()
@on 'move-down', => @moveDown()
@on 'tree-view:expand-directory', => @expandDirectory()
@on 'tree-view:collapse-directory', => @collapseDirectory()
@on 'tree-view:open-selected-entry', => @openSelectedEntry()
@on 'tree-view:move', => @moveSelectedEntry()
@on 'tree-view:add', => @add()
@on 'tree-view:remove', => @removeSelectedEntry()
@on 'tree-view:directory-modified', => @selectActiveFile()
@on 'tree-view:unfocus', => @rootView.focus()
@rootView.on 'tree-view:toggle', => @toggle()
@rootView.on 'tree-view:reveal-active-file', => @revealActiveFile()
@rootView.on 'active-editor-path-change', => @selectActiveFile()
@rootView.project.on 'path-change', => @updateRoot()
@selectEntry(@root) if @root
afterAttach: (onDom) ->
@focus() if @focusAfterAttach
serialize: ->
directoryExpansionStates: @root?.serializeEntryExpansionStates()
selectedPath: @selectedEntry()?.getPath()
hasFocus: @is(':focus')
attached: @hasParent()
deactivate: ->
@root?.unwatchEntries()
toggle: ->
if @is(':focus')
@detach()
@rootView.focus()
else
@attach() unless @hasParent()
@focus()
attach: ->
@rootView.horizontal.prepend(this)
entryClicked: (e) ->
entry = $(e.currentTarget).view()
switch e.originalEvent?.detail ? 1
when 1
@selectEntry(entry)
@openSelectedEntry() if (entry instanceof FileView)
when 2
if entry.is('.selected.file')
@rootView.getActiveEditor().focus()
else if entry.is('.selected.directory')
entry.toggleExpansion()
false
updateRoot: ->
@root?.remove()
@root = new DirectoryView(directory: @rootView.project.getRootDirectory(), isExpanded: true)
@append(@root)
selectActiveFile: ->
activeFilePath = @rootView.getActiveEditor()?.getPath()
@selectEntryForPath(activeFilePath)
revealActiveFile: ->
@attach()
@focus()
return unless activeFilePath = @rootView.getActiveEditor()?.getPath()
project = @rootView.project
activePathComponents = project.relativize(activeFilePath).split('/')
currentPath = project.getPath().replace(/\/$/, '')
for pathComponent in activePathComponents
currentPath += '/' + pathComponent
entry = @entryForPath(currentPath)
if entry.hasClass('directory')
entry.expand()
else
@selectEntry(entry)
entryForPath: (path) ->
fn = (bestMatchEntry, element) ->
entry = $(element).view()
regex = new RegExp("^" + _.escapeRegExp(entry.getPath()))
if regex.test(path) and entry.getPath().length > bestMatchEntry.getPath().length
entry
else
bestMatchEntry
@find(".entry").toArray().reduce(fn, @root)
selectEntryForPath: (path) ->
@selectEntry(@entryForPath(path))
moveDown: ->
selectedEntry = @selectedEntry()
if selectedEntry
if selectedEntry.is('.expanded.directory')
return if @selectEntry(selectedEntry.find('.entry:first'))
until @selectEntry(selectedEntry.next())
selectedEntry = selectedEntry.parents('.entry:first')
break unless selectedEntry.length
else
@selectEntry(@root)
@scrollToEntry(@selectedEntry())
moveUp: ->
selectedEntry = @selectedEntry()
if selectedEntry
if previousEntry = @selectEntry(selectedEntry.prev())
if previousEntry.is('.expanded.directory')
@selectEntry(previousEntry.find('.entry:last'))
else
@selectEntry(selectedEntry.parents('.directory').first())
else
@selectEntry(@find('.entry').last())
@scrollToEntry(@selectedEntry())
expandDirectory: ->
selectedEntry = @selectedEntry()
selectedEntry.view().expand() if (selectedEntry instanceof DirectoryView)
collapseDirectory: ->
selectedEntry = @selectedEntry()
if directory = selectedEntry.closest('.expanded.directory').view()
directory.collapse()
@selectEntry(directory)
openSelectedEntry: ->
selectedEntry = @selectedEntry()
if (selectedEntry instanceof DirectoryView)
selectedEntry.view().toggleExpansion()
else if (selectedEntry instanceof FileView)
@rootView.open(selectedEntry.getPath(), changeFocus: false)
moveSelectedEntry: ->
entry = @selectedEntry()
return unless entry
oldPath = @selectedEntry().getPath()
dialog = new Dialog
prompt: "Enter the new path for the file:"
path: @rootView.project.relativize(oldPath)
select: true
onConfirm: (newPath) =>
newPath = @rootView.project.resolve(newPath)
directoryPath = fs.directory(newPath)
try
fs.makeTree(directoryPath) unless fs.exists(directoryPath)
fs.move(oldPath, newPath)
catch e
dialog.showError("Error: " + e.message + " Try a different path:")
return false
@rootView.append(dialog)
removeSelectedEntry: ->
entry = @selectedEntry()
return unless entry
entryType = if entry instanceof DirectoryView then "directory" else "file"
message = "Are you sure you would like to delete the selected #{entryType}?"
detailedMessage = "You are deleting #{entry.getPath()}"
buttons = [
["Move to Trash", => Native.moveToTrash(entry.getPath())]
["Cancel", => ] # Do Nothing
["Delete", => fs.remove(entry.getPath())]
]
Native.alert message, detailedMessage, buttons
add: ->
selectedPath = @selectedEntry().getPath()
directoryPath = if fs.isFile(selectedPath) then fs.directory(selectedPath) else selectedPath
relativeDirectoryPath = @rootView.project.relativize(directoryPath)
relativeDirectoryPath += '/' if relativeDirectoryPath.length > 0
dialog = new Dialog
prompt: "Enter the path for the new file/directory. Directories end with '/':"
path: relativeDirectoryPath
select: false
onConfirm: (relativePath) =>
endsWithDirectorySeperator = /\/$/.test(relativePath)
path = @rootView.project.resolve(relativePath)
try
if fs.exists(path)
pathType = if fs.isFile(path) then "file" else "directory"
dialog.showError("Error: A #{pathType} already exists at path '#{path}'. Try a different path:")
false
else if endsWithDirectorySeperator
fs.makeTree(path)
else
fs.write(path, "")
@rootView.open(path)
catch e
dialog.showError("Error: " + e.message + " Try a different path:")
return false
@rootView.append(dialog)
selectedEntry: ->
@find('.selected')?.view()
selectEntry: (entry) ->
return false unless entry.get(0)
@find('.selected').removeClass('selected')
entry.addClass('selected')
scrollToEntry: (entry) ->
displayElement = if (entry instanceof DirectoryView) then entry.header else entry
top = @scrollTop() + displayElement.position().top
bottom = top + displayElement.outerHeight()
if bottom > @scrollBottom()
@scrollBottom(bottom)
if top < @scrollTop()
@scrollTop(top)

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,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,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.getBuffer().getLineCount() - 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.lineForBufferRow(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.getBuffer().getLineCount()
column = @editor.lineForBufferRow(row).length
else
nextLineMatch = regex.exec(@editor.lineForBufferRow(++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.getLastBufferRow()]
if @editor.lineForBufferRow(r).length == 0
row = r
break
if not row
row = @editor.getLastBufferRow()
column = @editor.getBuffer().getLastLine().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.getBuffer().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 }

View File

@@ -0,0 +1,3 @@
# Bootstrap the app in a single window mode
require 'atom-bootstrap.coffee'
require 'window-bootstrap.coffee'

View File

@@ -0,0 +1,35 @@
# node.js child-process
# http://nodejs.org/docs/v0.6.3/api/child_processes.html
$ = require 'jquery'
_ = require 'underscore'
module.exports =
class ChildProccess
@exec: (command, options={}) ->
deferred = $.Deferred()
if options.bufferLines
options.stdout = @bufferLines(options.stdout) if options.stdout
options.stderr = @bufferLines(options.stderr) if options.stderr
$native.exec command, options, (exitStatus, stdout, stderr) ->
try
if exitStatus != 0
deferred.reject({command, exitStatus, stderr})
else
deferred.resolve(stdout, stderr)
catch e
console.error "In ChildProccess termination callback: ", e.message
console.error e.stack
deferred
@bufferLines: (callback) ->
buffered = ""
(data) ->
buffered += data
lastNewlineIndex = buffered.lastIndexOf('\n')
if lastNewlineIndex >= 0
callback(buffered.substring(0, lastNewlineIndex + 1))
buffered = buffered.substring(lastNewlineIndex + 1)

20
src/stdlib/event.coffee Normal file
View File

@@ -0,0 +1,20 @@
_ = require 'underscore'
module.exports =
class Event
events: {}
on: (name, callback) ->
@events[name] ?= []
@events[name].push callback
off: (name, callback) ->
delete @events[name][_.indexOf callback] if @events[name]
trigger: (name, data...) ->
if name.match /^app:/
OSX.NSApp.triggerGlobalAtomEvent_data name, data
return
_.each @events[name], (callback) => callback data...
null

131
src/stdlib/fs.coffee Normal file
View File

@@ -0,0 +1,131 @@
# commonjs fs module
# http://ringojs.org/api/v0.8/fs/
_ = require 'underscore'
$ = require 'jquery'
module.exports =
# Make the given path absolute by resolving it against the
# current working directory.
absolute: (path) ->
$native.absolute(path)
# Return the basename of the given path. That is the path with
# any leading directory components removed. If specified, also
# remove a trailing extension.
base: (path, ext) ->
base = path.replace(/\/$/, '').split("/").pop()
if ext then base.replace(RegExp(ext + "$"), "") else base
# Returns the path of a file's containing directory, albeit the
# parent directory if the file is a directory. A terminal directory
# separator is ignored.
directory: (path) ->
parentPath = path.replace(new RegExp("/#{@base(path)}\/?$"), '')
return "" if path == parentPath
parentPath
# Returns true if the file specified by path exists
exists: (path) ->
return false unless path?
$native.exists(path)
# Returns the extension of a file. The extension of a file is the
# last dot (excluding any number of initial dots) followed by one or
# more non-dot characters. Returns an empty string if no valid
# extension exists.
extension: (path) ->
return '' unless typeof path is 'string'
match = @base(path).match(/\.[^\.]+$/)
if match
match[0]
else
""
join: (paths...) ->
return paths[0] if paths.length == 1
[first, rest...] = paths
first.replace(/\/?$/, "/") + @join(rest...)
# Returns true if the file specified by path exists and is a
# directory.
isDirectory: (path) ->
$native.isDirectory path
# Returns true if the file specified by path exists and is a
# regular file.
isFile: (path) ->
return false unless path?
$native.isFile(path)
# Returns an array with all the names of files contained
# in the directory path.
list: (path) ->
$native.list(path, false)
listTree: (path) ->
$native.list(path, true)
move: (source, target) ->
$native.move(source, target)
# Remove a file at the given path. Throws an error if path is not a
# file or a symbolic link to a file.
remove: (path) ->
$native.remove path
# Open, read, and close a file, returning the file's contents.
read: (path) ->
$native.read(path)
# Returns an array of path components. If the path is absolute, the first
# component will be an indicator of the root of the file system; for file
# systems with drives (such as Windows), this is the drive identifier with a
# colon, like "c:"; on Unix, this is an empty string "". The intent is that
# calling "join.apply" with the result of "split" as arguments will
# reconstruct the path.
split: (path) ->
path.split("/")
# Open, write, flush, and close a file, writing the given content.
write: (path, content) ->
$native.write(path, content)
makeDirectory: (path) ->
$native.makeDirectory(path)
# Creates the directory specified by "path" including any missing parent
# directories.
makeTree: (path) ->
return unless path
if not @exists(path)
@makeTree(@directory(path))
@makeDirectory(path)
traverseTree: (rootPath, fn) ->
recurse = null
prune = -> recurse = false
for path in @list(rootPath)
recurse = true
fn(path, prune)
@traverseTree(path, fn) if @isDirectory(path) and recurse
lastModified: (path) ->
$native.lastModified(path)
md5ForPath: (path) ->
$native.md5ForPath(path)
async:
list: (path) ->
deferred = $.Deferred()
$native.asyncList path, false, (subpaths) ->
deferred.resolve subpaths
deferred
listTree: (path) ->
deferred = $.Deferred()
$native.asyncList path, true, (subpaths) ->
deferred.resolve subpaths
deferred

View File

@@ -0,0 +1,16 @@
stringScore = require 'stringscore'
module.exports = (candidates, query, options) ->
if query
scoredCandidates = candidates.map (candidate) ->
string = if options.key? then candidate[options.key] else candidate
{ candidate, score: stringScore(string, query) }
scoredCandidates.sort (a, b) ->
if a.score > b.score then -1
else if a.score < b.score then 1
else 0
candidates = (scoredCandidate.candidate for scoredCandidate in scoredCandidates when scoredCandidate.score > 0)
candidates = candidates[0...options.maxResults] if options.maxResults?
candidates

View File

@@ -0,0 +1,32 @@
$ = require 'jquery'
$.fn.scrollBottom = (newValue) ->
if newValue?
@scrollTop(newValue - @height())
else
@scrollTop() + @height()
$.fn.scrollRight = (newValue) ->
if newValue?
@scrollLeft(newValue - @width())
else
@scrollLeft() + @width()
$.fn.containsElement = (element) ->
(element[0].compareDocumentPosition(this[0]) & 8) == 8
$.fn.preempt = (eventName, handler) ->
@on eventName, (e, args...) ->
if handler(e, args...) == false then e.stopImmediatePropagation()
eventNameWithoutNamespace = eventName.split('.')[0]
handlers = @data('events')[eventNameWithoutNamespace]
handlers.unshift(handlers.pop())
$.fn.hasParent = ->
@parent()[0]?
$.fn.flashError = ->
@addClass 'error'
removeErrorClass = => @removeClass 'error'
window.setTimeout(removeErrorClass, 200)

9
src/stdlib/native.coffee Normal file
View File

@@ -0,0 +1,9 @@
module.exports =
class Native
@alert: (args...) -> $native.alert(args...)
@saveDialog: (args...) -> $native.saveDialog(args...)
@reload: -> $native.reload()
@moveToTrash: (args...) -> $native.moveToTrash(args...)

Some files were not shown because too many files have changed in this diff Show More