mirror of
https://github.com/atom/atom.git
synced 2026-02-09 06:05:11 -05:00
WIP: Use rake to start compiling resources (like require.coffee)
This commit is contained in:
28
src/app/ace-adaptor.coffee
Normal file
28
src/app/ace-adaptor.coffee
Normal 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])
|
||||
28
src/app/anchor-range.coffee
Normal file
28
src/app/anchor-range.coffee
Normal 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
81
src/app/anchor.coffee
Normal 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
39
src/app/atom.coffee
Normal 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)
|
||||
52
src/app/binding-set.coffee
Normal file
52
src/app/binding-set.coffee
Normal 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('-')
|
||||
57
src/app/buffer-change-operation.coffee
Normal file
57
src/app/buffer-change-operation.coffee
Normal 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
357
src/app/buffer.coffee
Normal 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)
|
||||
62
src/app/cursor-view.coffee
Normal file
62
src/app/cursor-view.coffee
Normal 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 ' '
|
||||
|
||||
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
162
src/app/cursor.coffee
Normal 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
38
src/app/directory.coffee
Normal 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
|
||||
281
src/app/display-buffer.coffee
Normal file
281
src/app/display-buffer.coffee
Normal 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
545
src/app/edit-session.coffee
Normal 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
869
src/app/editor.coffee
Normal 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 ' ' 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()
|
||||
62
src/app/event-emitter.coffee
Normal file
62
src/app/event-emitter.coffee
Normal 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
50
src/app/file.coffee
Normal 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
74
src/app/fold.coffee
Normal 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
27
src/app/gutter.coffee
Normal 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
122
src/app/keymap.coffee
Normal 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)
|
||||
15
src/app/keymaps/apple.coffee
Normal file
15
src/app/keymaps/apple.coffee
Normal 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'
|
||||
7
src/app/keymaps/atom.coffee
Normal file
7
src/app/keymaps/atom.coffee
Normal 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'
|
||||
38
src/app/keymaps/editor.coffee
Normal file
38
src/app/keymaps/editor.coffee
Normal 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'
|
||||
22
src/app/keymaps/emacs.coffee
Normal file
22
src/app/keymaps/emacs.coffee
Normal 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'
|
||||
3
src/app/keystroke-pattern.pegjs
Normal file
3
src/app/keystroke-pattern.pegjs
Normal file
@@ -0,0 +1,3 @@
|
||||
keystrokePattern = key:key additionalKeys:additionalKey* { return [key].concat(additionalKeys); }
|
||||
additionalKey = '-' key:key { return key; }
|
||||
key = '-' / chars:[^-]+ { return chars.join('') }
|
||||
127
src/app/language-mode.coffee
Normal file
127
src/app/language-mode.coffee
Normal 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
186
src/app/line-map.coffee
Normal 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
|
||||
|
||||
32
src/app/pane-column.coffee
Normal file
32
src/app/pane-column.coffee
Normal 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
24
src/app/pane-grid.coffee
Normal 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
32
src/app/pane-row.coffee
Normal 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
67
src/app/pane.coffee
Normal 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
87
src/app/point.coffee
Normal 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
179
src/app/project.coffee
Normal 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
79
src/app/range.coffee
Normal 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
213
src/app/root-view.coffee
Normal 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
|
||||
|
||||
92
src/app/screen-line.coffee
Normal file
92
src/app/screen-line.coffee
Normal 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)
|
||||
61
src/app/selection-view.coffee
Normal file
61
src/app/selection-view.coffee
Normal 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
256
src/app/selection.coffee
Normal 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
40
src/app/status-bar.coffee
Normal 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}")
|
||||
|
||||
71
src/app/text-mate-bundle.coffee
Normal file
71
src/app/text-mate-bundle.coffee
Normal 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")
|
||||
|
||||
212
src/app/text-mate-grammar.coffee
Normal file
212
src/app/text-mate-grammar.coffee
Normal 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()]
|
||||
|
||||
108
src/app/text-mate-theme.coffee
Normal file
108
src/app/text-mate-theme.coffee
Normal 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
46
src/app/token.coffee
Normal 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
|
||||
|
||||
153
src/app/tokenized-buffer.coffee
Normal file
153
src/app/tokenized-buffer.coffee
Normal 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)
|
||||
46
src/app/undo-manager.coffee
Normal file
46
src/app/undo-manager.coffee
Normal 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
108
src/app/window.coffee
Normal 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'
|
||||
Reference in New Issue
Block a user