Merge branch 'summit'

Conflicts:
	src/app/config.coffee
	src/atom-application.coffee
	src/main.coffee
This commit is contained in:
Kevin Sawicki & Nathan Sobo
2013-08-19 14:01:24 -07:00
70 changed files with 1503 additions and 2226 deletions

View File

@@ -7,6 +7,7 @@ remote = require 'remote'
crypto = require 'crypto'
path = require 'path'
dialog = remote.require 'dialog'
app = remote.require 'app'
telepath = require 'telepath'
ThemeManager = require 'theme-manager'
@@ -116,7 +117,7 @@ window.atom =
_.uniq(packagePaths)
getAvailablePackageNames: ->
path.basename(packagePath) for packagePath in @getAvailablePackagePaths()
_.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath)
getAvailablePackageMetadata: ->
packages = []
@@ -184,7 +185,7 @@ window.atom =
remote.getCurrentWindow().hide()
exit: (status) ->
remote.require('app').exit(status)
app.exit(status)
toggleFullScreen: ->
@setFullScreen(!@isFullScreen())
@@ -195,6 +196,9 @@ window.atom =
isFullScreen: ->
remote.getCurrentWindow().isFullScreen()
getHomeDirPath: ->
app.getHomeDir()
getWindowStatePath: ->
switch @windowMode
when 'spec'
@@ -231,12 +235,12 @@ window.atom =
console.warn "Error parsing window state: #{windowStatePath}", error.stack, error
windowState ?= {}
telepath.Document.create(windowState, site: telepath.createSite(1))
site.deserializeDocument(windowState) ? site.createDocument({})
saveWindowState: ->
windowStateJson = JSON.stringify(@getWindowState().toObject())
windowStateJson = JSON.stringify(@getWindowState().serialize(), null, 2)
if windowStatePath = @getWindowStatePath()
fsUtils.writeSync(windowStatePath, windowStateJson)
fsUtils.writeSync(windowStatePath, "#{windowStateJson}\n")
else
@getLoadSettings().windowState = windowStateJson

View File

@@ -1,4 +1,4 @@
Range = require 'range'
{Range} = require 'telepath'
_ = require 'underscore'
### Internal ###
@@ -19,16 +19,16 @@ class BufferChangeOperation
do: ->
@buffer.pauseEvents()
@pauseMarkerObservation()
@markersToRestoreOnUndo = @invalidateMarkers(@oldRange)
# @markersToRestoreOnUndo = @invalidateMarkers(@oldRange)
if @oldRange?
@oldText = @buffer.getTextInRange(@oldRange)
@newRange = @calculateNewRange(@oldRange, @newText)
@newRange = Range.fromText(@oldRange.start, @newText)
newRange = @changeBuffer
oldRange: @oldRange
newRange: @newRange
oldText: @oldText
newText: @newText
@restoreMarkers(@markersToRestoreOnRedo) if @markersToRestoreOnRedo
# @restoreMarkers(@markersToRestoreOnRedo) if @markersToRestoreOnRedo
@buffer.resumeEvents()
@resumeMarkerObservation()
newRange
@@ -36,67 +36,19 @@ class BufferChangeOperation
undo: ->
@buffer.pauseEvents()
@pauseMarkerObservation()
@markersToRestoreOnRedo = @invalidateMarkers(@newRange)
# @markersToRestoreOnRedo = @invalidateMarkers(@newRange)
if @oldRange?
@changeBuffer
oldRange: @newRange
newRange: @oldRange
oldText: @newText
newText: @oldText
@restoreMarkers(@markersToRestoreOnUndo)
# @restoreMarkers(@markersToRestoreOnUndo)
@buffer.resumeEvents()
@resumeMarkerObservation()
splitLines: (text) ->
lines = text.split('\n')
lineEndings = []
for line, index in lines
if _.endsWith(line, '\r')
lines[index] = line[...-1]
lineEndings[index] = '\r\n'
else
lineEndings[index] = '\n'
{lines, lineEndings}
changeBuffer: ({ oldRange, newRange, newText, oldText }) ->
{ prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange)
{lines, lineEndings} = @splitLines(newText)
lastLineIndex = lines.length - 1
if lines.length == 1
lines = [prefix + newText + suffix]
else
lines[0] = prefix + lines[0]
lines[lastLineIndex] += suffix
startRow = oldRange.start.row
endRow = oldRange.end.row
normalizeLineEndings = @options.normalizeLineEndings ? true
if normalizeLineEndings and suggestedLineEnding = @buffer.suggestedLineEndingForRow(startRow)
lineEndings[index] = suggestedLineEnding for index in [0..lastLineIndex]
_.spliceWithArray(@buffer.lines, startRow, endRow - startRow + 1, lines)
_.spliceWithArray(@buffer.lineEndings, startRow, endRow - startRow + 1, lineEndings)
@buffer.cachedMemoryContents = null
@buffer.conflict = false if @buffer.conflict and !@buffer.isModified()
event = { oldRange, newRange, oldText, newText }
@updateMarkers(event)
@buffer.trigger 'changed', event
@buffer.scheduleModifiedEvents()
newRange
calculateNewRange: (oldRange, newText) ->
newRange = new Range(oldRange.start.copy(), oldRange.start.copy())
{lines} = @splitLines(newText)
if lines.length == 1
newRange.end.column += newText.length
else
lastLineIndex = lines.length - 1
newRange.end.row += lastLineIndex
newRange.end.column = lines[lastLineIndex].length
@buffer.text.change(oldRange, newText)
newRange
invalidateMarkers: (oldRange) ->
@@ -109,9 +61,6 @@ class BufferChangeOperation
marker.resumeEvents() for marker in @buffer.getMarkers(includeInvalid: true)
@buffer.trigger 'markers-updated' if @oldRange?
updateMarkers: (bufferChange) ->
marker.handleBufferChange(bufferChange) for marker in @buffer.getMarkers()
restoreMarkers: (markersToRestore) ->
for [id, previousRange] in markersToRestore
if validMarker = @buffer.validMarkers[id]

View File

@@ -1,256 +0,0 @@
_ = require 'underscore'
Point = require 'point'
Range = require 'range'
EventEmitter = require 'event-emitter'
module.exports =
class BufferMarker
headPosition: null
tailPosition: null
suppressObserverNotification: false
invalidationStrategy: null
### Internal ###
constructor: ({@id, @buffer, range, @invalidationStrategy, @attributes, noTail, reverse}) ->
@invalidationStrategy ?= 'contains'
@setRange(range, {noTail, reverse})
### Public ###
# Sets the marker's range, potentialy modifying both its head and tail.
#
# range - The new {Range} the marker should cover
# options - A hash of options with the following keys:
# reverse: if `true`, the marker is reversed; that is, its tail is "above" the head
# noTail: if `true`, the marker doesn't have a tail
setRange: (range, options={}) ->
@consolidateObserverNotifications false, =>
range = Range.fromObject(range)
if options.reverse
@setTailPosition(range.end) unless options.noTail
@setHeadPosition(range.start)
else
@setTailPosition(range.start) unless options.noTail
@setHeadPosition(range.end)
# Identifies if the ending position of a marker is greater than the starting position.
#
# This can happen when, for example, you highlight text "up" in a {Buffer}.
#
# Returns a {Boolean}.
isReversed: ->
@tailPosition? and @headPosition.isLessThan(@tailPosition)
# Checks that the marker's attributes match the given attributes
#
# Returns a {Boolean}.
matchesAttributes: (queryAttributes) ->
for key, value of queryAttributes
switch key
when 'startRow'
return false unless @getRange().start.row == value
when 'endRow'
return false unless @getRange().end.row == value
when 'containsRange'
return false unless @getRange().containsRange(value, exclusive: true)
when 'containsRow'
return false unless @getRange().containsRow(value)
else
return false unless _.isEqual(@attributes[key], value)
true
# Identifies if the marker's head position is equal to its tail.
#
# Returns a {Boolean}.
isRangeEmpty: ->
@getHeadPosition().isEqual(@getTailPosition())
# Retrieves the {Range} between a marker's head and its tail.
#
# Returns a {Range}.
getRange: ->
if @tailPosition
new Range(@getTailPosition(), @getHeadPosition())
else
new Range(@getHeadPosition(), @getHeadPosition())
# Retrieves the position of the marker's head.
#
# Returns a {Point}.
getHeadPosition: -> @headPosition?.copy()
# Retrieves the position of the marker's tail.
#
# Returns a {Point}.
getTailPosition: -> @tailPosition?.copy() ? @getHeadPosition()
# Sets the position of the marker's head.
#
# newHeadPosition - The new {Point} to place the head
# options - A hash with the following keys:
# clip: if `true`, the point is [clipped]{Buffer.clipPosition}
# bufferChanged: if `true`, indicates that the {Buffer} should trigger an event that it's changed
#
# Returns a {Point} representing the new head position.
setHeadPosition: (newHeadPosition, options={}) ->
oldHeadPosition = @getHeadPosition()
newHeadPosition = Point.fromObject(newHeadPosition)
newHeadPosition = @buffer.clipPosition(newHeadPosition) if options.clip ? true
return if newHeadPosition.isEqual(@headPosition)
@headPosition = newHeadPosition
bufferChanged = !!options.bufferChanged
@notifyObservers({oldHeadPosition, newHeadPosition, bufferChanged})
@headPosition
# Sets the position of the marker's tail.
#
# newHeadPosition - The new {Point} to place the tail
# options - A hash with the following keys:
# clip: if `true`, the point is [clipped]{Buffer.clipPosition}
# bufferChanged: if `true`, indicates that the {Buffer} should trigger an event that it's changed
#
# Returns a {Point} representing the new tail position.
setTailPosition: (newTailPosition, options={}) ->
oldTailPosition = @getTailPosition()
newTailPosition = Point.fromObject(newTailPosition)
newTailPosition = @buffer.clipPosition(newTailPosition) if options.clip ? true
return if newTailPosition.isEqual(@tailPosition)
@tailPosition = newTailPosition
bufferChanged = !!options.bufferChanged
@notifyObservers({oldTailPosition, newTailPosition, bufferChanged})
@tailPosition
# Retrieves the starting position of the marker.
#
# Returns a {Point}.
getStartPosition: ->
@getRange().start
# Retrieves the ending position of the marker.
#
# Returns a {Point}.
getEndPosition: ->
@getRange().end
# Sets the marker's tail to the same position as the marker's head.
#
# This only works if there isn't already a tail position.
#
# Returns a {Point} representing the new tail position.
placeTail: ->
@setTailPosition(@getHeadPosition()) unless @tailPosition
# Removes the tail from the marker.
clearTail: ->
oldTailPosition = @getTailPosition()
@tailPosition = null
newTailPosition = @getTailPosition()
@notifyObservers({oldTailPosition, newTailPosition, bufferChanged: false})
# Identifies if a {Point} is within the marker.
#
# Returns a {Boolean}.
containsPoint: (point) ->
@getRange().containsPoint(point)
# Destroys the marker
destroy: ->
@buffer.destroyMarker(@id)
@trigger 'destroyed'
# Returns a {Boolean} indicating whether the marker is valid. Markers can be
# invalidated when a region surrounding them in the buffer is changed.
isValid: ->
@buffer.getMarker(@id)?
# Returns a {Boolean} indicating whether the marker has been destroyed. A marker
# can be invalid without being destroyed, in which case undoing the invalidating
# operation would restore the marker. Once a marker is destroyed by calling
# {BufferMarker.destroy}, no undo/redo operation can ever bring it back.
isDestroyed: ->
not (@buffer.validMarkers[@id]? or @buffer.invalidMarkers[@id]?)
### Internal ###
tryToInvalidate: (changedRange) ->
previousRange = @getRange()
if changedRange
betweenStartAndEnd = @getRange().containsRange(changedRange, exclusive: false)
containsStart = changedRange.containsPoint(@getStartPosition(), exclusive: true)
containsEnd = changedRange.containsPoint(@getEndPosition(), exclusive: true)
switch @invalidationStrategy
when 'between'
@invalidate() if betweenStartAndEnd or containsStart or containsEnd
when 'contains'
@invalidate() if containsStart or containsEnd
when 'never'
if containsStart or containsEnd
if containsStart and containsEnd
@setRange([changedRange.end, changedRange.end])
else if containsStart
@setRange([changedRange.end, @getEndPosition()])
else
@setRange([@getStartPosition(), changedRange.start])
[@id, previousRange]
handleBufferChange: (bufferChange) ->
@consolidateObserverNotifications true, =>
@setHeadPosition(@updatePosition(@headPosition, bufferChange, true), clip: false, bufferChanged: true)
@setTailPosition(@updatePosition(@tailPosition, bufferChange, false), clip: false, bufferChanged: true) if @tailPosition
updatePosition: (position, bufferChange, isHead) ->
{ oldRange, newRange } = bufferChange
return position if not isHead and oldRange.start.isEqual(position)
return position 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
[newRow, newColumn]
notifyObservers: ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged} = {}) ->
return if @suppressObserverNotification
if newHeadPosition? and newTailPosition?
return if _.isEqual(newHeadPosition, oldHeadPosition) and _.isEqual(newTailPosition, oldTailPosition)
else if newHeadPosition?
return if _.isEqual(newHeadPosition, oldHeadPosition)
else if newTailPosition?
return if _.isEqual(newTailPosition, oldTailPosition)
oldHeadPosition ?= @getHeadPosition()
newHeadPosition ?= @getHeadPosition()
oldTailPosition ?= @getTailPosition()
newTailPosition ?= @getTailPosition()
valid = @isValid()
@trigger 'changed', {oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged, valid}
consolidateObserverNotifications: (bufferChanged, fn) ->
@suppressObserverNotification = true
oldHeadPosition = @getHeadPosition()
oldTailPosition = @getTailPosition()
fn()
newHeadPosition = @getHeadPosition()
newTailPosition = @getTailPosition()
@suppressObserverNotification = false
@notifyObservers({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged})
invalidate: ->
delete @buffer.validMarkers[@id]
@buffer.invalidMarkers[@id] = this
@notifyObservers(bufferChanged: true)
revalidate: ->
delete @buffer.invalidMarkers[@id]
@buffer.validMarkers[@id] = this
@notifyObservers(bufferChanged: true)
_.extend BufferMarker.prototype, EventEmitter

View File

@@ -12,9 +12,9 @@ nodeModulesDirPath = path.join(resourcePath, "node_modules")
bundledThemesDirPath = path.join(resourcePath, "themes")
userThemesDirPath = path.join(configDirPath, "themes")
userPackagesDirPath = path.join(configDirPath, "packages")
userStoragePath = path.join(configDirPath, ".storage")
packageDirPaths = [userPackagesDirPath]
packageDirPaths.unshift(path.join(configDirPath, "dev", "packages")) if atom.getLoadSettings().devMode
userPackageDirPaths = [userPackagesDirPath]
userPackageDirPaths.unshift(path.join(configDirPath, "dev", "packages")) if atom.getLoadSettings().devMode
userStoragePath = path.join(configDirPath, "storage")
# Public: Handles all of Atom's configuration details.
#
@@ -26,8 +26,8 @@ class Config
themeDirPaths: [userThemesDirPath, bundledThemesDirPath]
bundledPackageDirPaths: [nodeModulesDirPath]
nodeModulesDirPath: nodeModulesDirPath
packageDirPaths: packageDirPaths
userPackagesDirPath: userPackagesDirPath
packageDirPaths: _.clone(userPackageDirPaths)
userPackageDirPaths: userPackageDirPaths
userStoragePath: userStoragePath
lessSearchPaths: [path.join(resourcePath, 'static'), path.join(resourcePath, 'vendor')]
defaultSettings: null

View File

@@ -1,6 +1,5 @@
{View} = require 'space-pen'
Point = require 'point'
Range = require 'range'
{Point, Range} = require 'telepath'
_ = require 'underscore'
### Internal ###
@@ -31,6 +30,9 @@ class CursorView extends View
@cursor.on 'destroyed.cursor-view', =>
@needsRemoval = true
if @cursor.marker.isRemote()
@addClass("site-#{@cursor.marker.getOriginSiteId()}")
remove: ->
@editor.removeCursorView(this)
@cursor.off('.cursor-view')

View File

@@ -1,5 +1,4 @@
Point = require 'point'
Range = require 'range'
{Point, Range} = require 'telepath'
EventEmitter = require 'event-emitter'
_ = require 'underscore'
@@ -22,27 +21,28 @@ class Cursor
@updateVisibility()
{oldHeadScreenPosition, newHeadScreenPosition} = e
{oldHeadBufferPosition, newHeadBufferPosition} = e
{bufferChanged} = e
{textChanged} = e
return if oldHeadScreenPosition.isEqual(newHeadScreenPosition)
@needsAutoscroll ?= @isLastCursor() and !bufferChanged
@needsAutoscroll ?= @isLastCursor() and !textChanged
movedEvent =
oldBufferPosition: oldHeadBufferPosition
oldScreenPosition: oldHeadScreenPosition
newBufferPosition: newHeadBufferPosition
newScreenPosition: newHeadScreenPosition
bufferChanged: bufferChanged
textChanged: textChanged
@trigger 'moved', movedEvent
@editSession.trigger 'cursor-moved', movedEvent
@marker.on 'destroyed', =>
@destroyed = true
@editSession.removeCursor(this)
@trigger 'destroyed'
@needsAutoscroll = true
destroy: ->
@destroyed = true
@marker.destroy()
@editSession.removeCursor(this)
@trigger 'destroyed'
changePosition: (options, fn) ->
@goalColumn = null
@@ -139,9 +139,7 @@ class Cursor
# Deselects whatever the cursor is selecting.
clearSelection: ->
if @selection
@selection.goalBufferRange = null
@selection.clear() unless @selection.retainSelection
@selection?.clear()
# Retrieves the cursor's screen row.
#
@@ -427,7 +425,7 @@ class Cursor
#
# Returns a {Number}.
getIndentLevel: ->
if @editSession.softTabs
if @editSession.getSoftTabs()
@getBufferColumn() / @editSession.getTabLength()
else
@getBufferColumn()

View File

@@ -49,7 +49,9 @@ class Directory
# pathToCheck - the {String} path to check.
#
# Returns a {Boolean}.
contains: (pathToCheck='') ->
contains: (pathToCheck) ->
return false unless pathToCheck
if pathToCheck.indexOf(path.join(@getPath(), path.sep)) is 0
true
else if pathToCheck.indexOf(path.join(@getRealPath(), path.sep)) is 0
@@ -62,7 +64,9 @@ class Directory
# fullPath - The {String} path to convert.
#
# Returns a {String}.
relativize: (fullPath='') ->
relativize: (fullPath) ->
return fullPath unless fullPath
if fullPath is @getPath()
''
else if fullPath.indexOf(path.join(@getPath(), path.sep)) is 0

View File

@@ -1,4 +1,4 @@
Range = require 'range'
{Range} = require 'telepath'
_ = require 'underscore'
EventEmitter = require 'event-emitter'
Subscriber = require 'subscriber'
@@ -6,18 +6,30 @@ Subscriber = require 'subscriber'
module.exports =
class DisplayBufferMarker
bufferMarkerSubscription: null
headScreenPosition: null
tailScreenPosition: null
valid: true
oldHeadBufferPosition: null
oldHeadScreenPosition: null
oldTailBufferPosition: null
oldTailScreenPosition: null
wasValid: true
### Internal ###
constructor: ({@bufferMarker, @displayBuffer}) ->
@id = @bufferMarker.id
@observeBufferMarker()
@oldHeadBufferPosition = @getHeadBufferPosition()
@oldHeadScreenPosition = @getHeadScreenPosition()
@oldTailBufferPosition = @getTailBufferPosition()
@oldTailScreenPosition = @getTailScreenPosition()
@wasValid = @isValid()
@subscribe @bufferMarker, 'destroyed', => @destroyed()
@subscribe @bufferMarker, 'changed', (event) => @notifyObservers(event)
### Public ###
copy: (attributes) ->
@displayBuffer.getMarker(@bufferMarker.copy(attributes).id)
# Gets the screen range of the display marker.
#
# Returns a {Range}.
@@ -48,7 +60,7 @@ class DisplayBufferMarker
#
# Returns a {Point}.
getHeadScreenPosition: ->
@headScreenPosition ?= @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true)
@displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true)
# Sets the screen position of the marker's head.
#
@@ -75,7 +87,7 @@ class DisplayBufferMarker
#
# Returns a {Point}.
getTailScreenPosition: ->
@tailScreenPosition ?= @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true)
@displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true)
# Sets the screen position of the marker's tail.
#
@@ -103,13 +115,16 @@ class DisplayBufferMarker
# This only works if there isn't already a tail position.
#
# Returns a {Point} representing the new tail position.
placeTail: ->
@bufferMarker.placeTail()
plantTail: ->
@bufferMarker.plantTail()
# Removes the tail from the marker.
clearTail: ->
@bufferMarker.clearTail()
hasTail: ->
@bufferMarker.hasTail()
# Returns whether the head precedes the tail in the buffer
isReversed: ->
@bufferMarker.isReversed()
@@ -126,14 +141,50 @@ class DisplayBufferMarker
isDestroyed: ->
@bufferMarker.isDestroyed()
getOriginSiteId: ->
@bufferMarker.getOriginSiteId()
isLocal: ->
@bufferMarker.isLocal()
isRemote: ->
@bufferMarker.isRemote()
getAttributes: ->
@bufferMarker.getAttributes()
setAttributes: (attributes) ->
@bufferMarker.setAttributes(attributes)
matchesAttributes: (attributes) ->
@bufferMarker.matchesAttributes(attributes)
for key, value of attributes
return false unless @matchesAttribute(key, value)
true
matchesAttribute: (key, value) ->
switch key
when 'startBufferRow'
key = 'startRow'
when 'endBufferRow'
key = 'endRow'
when 'containsBufferRange'
key = 'containsRange'
when 'containsBufferPosition'
key = 'containsPosition'
@bufferMarker.matchesAttribute(key, value)
# Destroys the marker
destroy: ->
@bufferMarker.destroy()
@unsubscribe()
isEqual: (other) ->
return false unless other instanceof @constructor
@bufferMarker.isEqual(other.bufferMarker)
compare: (other) ->
@bufferMarker.compare(other.bufferMarker)
# Returns a {String} representation of the marker
inspect: ->
"DisplayBufferMarker(id: #{@id}, bufferRange: #{@getBufferRange().inspect()})"
@@ -144,54 +195,37 @@ class DisplayBufferMarker
delete @displayBuffer.markers[@id]
@trigger 'destroyed'
observeBufferMarker: ->
@subscribe @bufferMarker, 'destroyed', => @destroyed()
notifyObservers: ({textChanged}) ->
textChanged ?= false
@getHeadScreenPosition() # memoize current value
@getTailScreenPosition() # memoize current value
@subscribe @bufferMarker, 'changed', ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged, valid}) =>
@notifyObservers
oldHeadBufferPosition: oldHeadPosition
newHeadBufferPosition: newHeadPosition
oldTailBufferPosition: oldTailPosition
newTailBufferPosition: newTailPosition
bufferChanged: bufferChanged
valid: valid
newHeadBufferPosition = @getHeadBufferPosition()
newHeadScreenPosition = @getHeadScreenPosition()
newTailBufferPosition = @getTailBufferPosition()
newTailScreenPosition = @getTailScreenPosition()
isValid = @isValid()
notifyObservers: ({oldHeadBufferPosition, oldTailBufferPosition, bufferChanged, valid} = {}) ->
return unless @valid or @isValid()
oldHeadScreenPosition = @getHeadScreenPosition()
newHeadScreenPosition = oldHeadScreenPosition
oldTailScreenPosition = @getTailScreenPosition()
newTailScreenPosition = oldTailScreenPosition
valid ?= true
if valid
@headScreenPosition = null
newHeadScreenPosition = @getHeadScreenPosition()
@tailScreenPosition = null
newTailScreenPosition = @getTailScreenPosition()
validChanged = valid isnt @valid
headScreenPositionChanged = not _.isEqual(newHeadScreenPosition, oldHeadScreenPosition)
tailScreenPositionChanged = not _.isEqual(newTailScreenPosition, oldTailScreenPosition)
return unless validChanged or headScreenPositionChanged or tailScreenPositionChanged
oldHeadBufferPosition ?= @getHeadBufferPosition()
newHeadBufferPosition = @getHeadBufferPosition() ? oldHeadBufferPosition
oldTailBufferPosition ?= @getTailBufferPosition()
newTailBufferPosition = @getTailBufferPosition() ? oldTailBufferPosition
@valid = valid
changed = false
changed = true unless _.isEqual(newHeadBufferPosition, @oldHeadBufferPosition)
changed = true unless _.isEqual(newHeadScreenPosition, @oldHeadScreenPosition)
changed = true unless _.isEqual(newTailBufferPosition, @oldTailBufferPosition)
changed = true unless _.isEqual(newTailScreenPosition, @oldTailScreenPosition)
changed = true unless _.isEqual(isValid, @wasValid)
return unless changed
@trigger 'changed', {
oldHeadScreenPosition, newHeadScreenPosition,
oldTailScreenPosition, newTailScreenPosition,
oldHeadBufferPosition, newHeadBufferPosition,
oldTailBufferPosition, newTailBufferPosition,
bufferChanged
valid
@oldHeadScreenPosition, newHeadScreenPosition,
@oldTailScreenPosition, newTailScreenPosition,
@oldHeadBufferPosition, newHeadBufferPosition,
@oldTailBufferPosition, newTailBufferPosition,
textChanged,
isValid
}
@oldHeadBufferPosition = newHeadBufferPosition
@oldHeadScreenPosition = newHeadScreenPosition
@oldTailBufferPosition = newTailBufferPosition
@oldTailScreenPosition = newTailScreenPosition
@wasValid = isValid
_.extend DisplayBufferMarker.prototype, EventEmitter
_.extend DisplayBufferMarker.prototype, Subscriber

View File

@@ -1,37 +1,64 @@
_ = require 'underscore'
guid = require 'guid'
telepath = require 'telepath'
{Point, Range} = telepath
TokenizedBuffer = require 'tokenized-buffer'
RowMap = require 'row-map'
Point = require 'point'
EventEmitter = require 'event-emitter'
Range = require 'range'
Fold = require 'fold'
Token = require 'token'
DisplayBufferMarker = require 'display-buffer-marker'
Subscriber = require 'subscriber'
DefaultSoftWrapColumn = 1000000
module.exports =
class DisplayBuffer
@idCounter: 1
screenLines: null
rowMap: null
tokenizedBuffer: null
markers: null
foldsByMarkerId: null
### Internal ###
@acceptsDocuments: true
registerDeserializer(this)
@deserialize: (state) ->
new DisplayBuffer(state)
constructor: (optionsOrState) ->
if optionsOrState instanceof telepath.Document
@state = optionsOrState
@id = @state.get('id')
@tokenizedBuffer = deserialize(@state.get('tokenizedBuffer'))
@buffer = @tokenizedBuffer.buffer
else
{@buffer, softWrapColumn} = optionsOrState
@id = guid.create().toString()
@tokenizedBuffer = new TokenizedBuffer(optionsOrState)
@state = site.createDocument
deserializer: @constructor.name
id: @id
tokenizedBuffer: @tokenizedBuffer.getState()
softWrapColumn: softWrapColumn ? DefaultSoftWrapColumn
constructor: (@buffer, options={}) ->
@id = @constructor.idCounter++
@tokenizedBuffer = new TokenizedBuffer(@buffer, options)
@softWrapColumn = options.softWrapColumn ? Infinity
@markers = {}
@foldsByMarkerId = {}
@updateAllScreenLines()
@createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes())
@tokenizedBuffer.on 'grammar-changed', (grammar) => @trigger 'grammar-changed', grammar
@tokenizedBuffer.on 'changed', @handleTokenizedBufferChange
@subscribe @buffer, 'markers-updated', @handleMarkersUpdated
@subscribe @buffer, 'marker-created', @handleMarkerCreated
@subscribe @buffer, 'markers-updated', @handleBufferMarkersUpdated
@subscribe @buffer, 'marker-created', @handleBufferMarkerCreated
serialize: -> @state.clone()
getState: -> @state
copy: ->
newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength()})
for marker in @findMarkers(displayBufferId: @id)
marker.copy(displayBufferId: newDisplayBuffer.id)
newDisplayBuffer
updateAllScreenLines: ->
@maxLineLength = 0
@@ -56,7 +83,8 @@ class DisplayBuffer
# Defines the limit at which the buffer begins to soft wrap text.
#
# softWrapColumn - A {Number} defining the soft wrap limit.
setSoftWrapColumn: (@softWrapColumn) ->
setSoftWrapColumn: (softWrapColumn) ->
@state.set('softWrapColumn', softWrapColumn)
start = 0
end = @getLastRow()
@updateAllScreenLines()
@@ -64,6 +92,9 @@ class DisplayBuffer
bufferDelta = 0
@triggerChanged({ start, end, screenDelta, bufferDelta })
getSoftWrapColumn: ->
@state.get('softWrapColumn')
# Gets the screen line for the given screen row.
#
# screenRow - A {Number} indicating the screen row.
@@ -107,9 +138,15 @@ class DisplayBuffer
createFold: (startRow, endRow) ->
foldMarker =
@findFoldMarker({startRow, endRow}) ?
@buffer.markRange([[startRow, 0], [endRow, Infinity]], @foldMarkerAttributes())
@buffer.markRange([[startRow, 0], [endRow, Infinity]], @getFoldMarkerAttributes())
@foldForMarker(foldMarker)
isFoldedAtBufferRow: (bufferRow) ->
@largestFoldContainingBufferRow(bufferRow)?
isFoldedAtScreenRow: (screenRow) ->
@largestFoldContainingBufferRow(@bufferRowForScreenRow(screenRow))?
# Destroys the fold with the given id
destroyFoldWithId: (id) ->
@foldsByMarkerId[id]?.destroy()
@@ -370,7 +407,7 @@ class DisplayBuffer
#
# Returns a {Number} representing the `line` position where the wrap would take place.
# Returns `null` if a wrap wouldn't occur.
findWrapColumn: (line, softWrapColumn=@softWrapColumn) ->
findWrapColumn: (line, softWrapColumn=@getSoftWrapColumn()) ->
return unless line.length > softWrapColumn
if /\s/.test(line[softWrapColumn])
@@ -396,8 +433,7 @@ class DisplayBuffer
#
# Returns the {DisplayBufferMarker} (if it exists).
getMarker: (id) ->
marker = @markers[id]
unless marker?
unless marker = @markers[id]
if bufferMarker = @buffer.getMarker(id)
marker = new DisplayBufferMarker({bufferMarker, displayBuffer: this})
@markers[id] = marker
@@ -407,7 +443,10 @@ class DisplayBuffer
#
# Returns an {Array} of existing {DisplayBufferMarker}s.
getMarkers: ->
_.values(@markers)
@buffer.getMarkers().map ({id}) => @getMarker(id)
getMarkerCount: ->
@buffer.getMarkerCount()
# Constructs a new marker at the given screen range.
#
@@ -470,21 +509,16 @@ class DisplayBuffer
#
# Returns an {Array} of {DisplayBufferMarker}s
findMarkers: (attributes) ->
{ startBufferRow, endBufferRow, containsBufferRange, containsBufferRow } = attributes
attributes.startRow = startBufferRow if startBufferRow?
attributes.endRow = endBufferRow if endBufferRow?
attributes.containsRange = containsBufferRange if containsBufferRange?
attributes.containsRow = containsBufferRow if containsBufferRow?
attributes = _.omit(attributes, ['startBufferRow', 'endBufferRow', 'containsBufferRange', 'containsBufferRow'])
@buffer.findMarkers(attributes).map ({id}) => @getMarker(id)
markers = @getMarkers().filter (marker) -> marker.matchesAttributes(attributes)
markers.sort (a, b) -> a.compare(b)
findFoldMarker: (attributes) ->
@findFoldMarkers(attributes)[0]
findFoldMarkers: (attributes) ->
@buffer.findMarkers(@foldMarkerAttributes(attributes))
@buffer.findMarkers(@getFoldMarkerAttributes(attributes))
foldMarkerAttributes: (attributes={}) ->
getFoldMarkerAttributes: (attributes={}) ->
_.extend(attributes, class: 'fold', displayBufferId: @id)
pauseMarkerObservers: ->
@@ -492,10 +526,11 @@ class DisplayBuffer
resumeMarkerObservers: ->
marker.resumeEvents() for marker in @getMarkers()
@trigger 'markers-updated'
refreshMarkerScreenPositions: ->
for marker in @getMarkers()
marker.notifyObservers(bufferChanged: false)
marker.notifyObservers(textChanged: false)
destroy: ->
marker.unsubscribe() for marker in @getMarkers()
@@ -608,15 +643,18 @@ class DisplayBuffer
@longestScreenRow = maxLengthCandidatesStartRow + screenRow
@maxLineLength = length
handleMarkersUpdated: =>
event = @pendingChangeEvent
@pendingChangeEvent = null
@triggerChanged(event, false)
handleBufferMarkersUpdated: =>
if event = @pendingChangeEvent
@pendingChangeEvent = null
@triggerChanged(event, false)
handleMarkerCreated: (marker) =>
new Fold(this, marker) if marker.matchesAttributes(@foldMarkerAttributes())
handleBufferMarkerCreated: (marker) =>
@createFoldForMarker(marker) if marker.matchesAttributes(@getFoldMarkerAttributes())
@trigger 'marker-created', @getMarker(marker.id)
createFoldForMarker: (marker) ->
new Fold(this, marker)
foldForMarker: (marker) ->
@foldsByMarkerId[marker.id]

View File

@@ -2,7 +2,8 @@ _ = require 'underscore'
fsUtils = require 'fs-utils'
path = require 'path'
telepath = require 'telepath'
Point = require 'point'
guid = require 'guid'
{Point, Range} = telepath
Buffer = require 'text-buffer'
LanguageMode = require 'language-mode'
DisplayBuffer = require 'display-buffer'
@@ -10,7 +11,6 @@ Cursor = require 'cursor'
Selection = require 'selection'
EventEmitter = require 'event-emitter'
Subscriber = require 'subscriber'
Range = require 'range'
TextMateScopeSelector = require('first-mate').ScopeSelector
# An `EditSession` manages the states between {Editor}s, {Buffer}s, and the project as a whole.
@@ -22,60 +22,60 @@ class EditSession
### Internal ###
@version: 1
@version: 4
@deserialize: (state) ->
new EditSession(state)
id: null
languageMode: null
displayBuffer: null
cursors: null
remoteCursors: null
selections: null
softTabs: true
softWrap: false
remoteSelections: null
suppressSelectionMerging: false
constructor: (optionsOrState) ->
@cursors = []
@remoteCursors = []
@selections = []
@remoteSelections = []
if optionsOrState instanceof telepath.Document
@state = optionsOrState
{tabLength, softTabs, @softWrap} = @state.toObject()
@buffer = deserialize(@state.get('buffer'))
@id = @state.get('id')
displayBuffer = deserialize(@state.get('displayBuffer'))
@setBuffer(displayBuffer.buffer)
@setDisplayBuffer(displayBuffer)
for marker in @findMarkers(@getSelectionMarkerAttributes())
marker.setAttributes(preserveFolds: true)
@addSelection(marker)
@setScrollTop(@state.get('scrollTop'))
@setScrollLeft(@state.get('scrollLeft'))
cursorScreenPosition = @state.getObject('cursorScreenPosition')
registerEditSession = true
else
{@buffer, tabLength, softTabs, @softWrap} = optionsOrState
@state = telepath.Document.create
{buffer, displayBuffer, tabLength, softTabs, softWrap, suppressCursorCreation} = optionsOrState
@id = guid.create().toString()
displayBuffer ?= new DisplayBuffer({buffer, tabLength})
@state = site.createDocument
deserializer: 'EditSession'
version: @constructor.version
id: @id
bufferId: buffer.id
displayBuffer: displayBuffer.getState()
softWrap: softWrap ? false
softTabs: buffer.usesSoftTabs() ? softTabs ? true
scrollTop: 0
scrollLeft: 0
cursorScreenPosition = [0, 0]
@setBuffer(buffer)
@setDisplayBuffer(displayBuffer)
if @getCursors().length is 0 and not suppressCursorCreation
position = _.last(@getRemoteCursors())?.getBufferPosition() ? [0, 0]
@addCursorAtBufferPosition(position)
@softTabs = @buffer.usesSoftTabs() ? softTabs ? true
@languageMode = new LanguageMode(this, @buffer.getExtension())
@displayBuffer = new DisplayBuffer(@buffer, { @languageMode, tabLength })
@cursors = []
@selections = []
@addCursorAtScreenPosition(cursorScreenPosition)
@buffer.retain()
@subscribe @buffer, "path-changed", =>
project.setPath(path.dirname(@getPath())) unless project.getPath()?
@trigger "title-changed"
@trigger "path-changed"
@subscribe @buffer, "contents-conflicted", => @trigger "contents-conflicted"
@subscribe @buffer, "markers-updated", => @mergeCursors()
@subscribe @buffer, "modified-status-changed", => @trigger "modified-status-changed"
@preserveCursorPositionOnBufferReload()
@subscribe @displayBuffer, "changed", (e) =>
@trigger 'screen-lines-changed', e
@displayBuffer.on 'grammar-changed', => @handleGrammarChange()
@state.observe ({key, newValue}) =>
@state.on 'changed', ({key, newValue}) =>
switch key
when 'scrollTop'
@trigger 'scroll-top-changed', newValue
@@ -84,6 +84,22 @@ class EditSession
project.addEditSession(this) if registerEditSession
setBuffer: (@buffer) ->
@buffer.retain()
@subscribe @buffer, "path-changed", =>
project.setPath(path.dirname(@getPath())) unless project.getPath()?
@trigger "title-changed"
@trigger "path-changed"
@subscribe @buffer, "contents-conflicted", => @trigger "contents-conflicted"
@subscribe @buffer, "modified-status-changed", => @trigger "modified-status-changed"
@preserveCursorPositionOnBufferReload()
setDisplayBuffer: (@displayBuffer) ->
@subscribe @displayBuffer, 'marker-created', @handleMarkerCreated
@subscribe @displayBuffer, "changed", (e) => @trigger 'screen-lines-changed', e
@subscribe @displayBuffer, "markers-updated", => @mergeIntersectingSelections()
@subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange()
getViewClass: ->
require 'editor'
@@ -99,22 +115,21 @@ class EditSession
@trigger 'destroyed'
@off()
serialize: ->
@state.set
buffer: @buffer.serialize()
scrollTop: @getScrollTop()
scrollLeft: @getScrollLeft()
tabLength: @getTabLength()
softTabs: @softTabs
softWrap: @softWrap
cursorScreenPosition: @getCursorScreenPosition().serialize()
@state
serialize: -> @state.clone()
getState: -> @state
getState: -> @serialize()
# Creates a copy of the current {EditSession}.Returns an identical `EditSession`.
# Creates an {EditSession} with the same initial state
copy: ->
EditSession.deserialize(@serialize())
tabLength = @getTabLength()
displayBuffer = @displayBuffer.copy()
softTabs = @getSoftTabs()
softWrap = @getSoftWrap()
newEditSession = new EditSession({@buffer, displayBuffer, tabLength, softTabs, softWrap, suppressCursorCreation: true})
newEditSession.setScrollTop(@getScrollTop())
newEditSession.setScrollLeft(@getScrollLeft())
for marker in @findMarkers(editSessionId: @id)
marker.copy(editSessionId: newEditSession.id, preserveFolds: true)
newEditSession
### Public ###
@@ -186,20 +201,26 @@ class EditSession
# softWrapColumn - A {Number} defining the soft wrap limit
setSoftWrapColumn: (@softWrapColumn) -> @displayBuffer.setSoftWrapColumn(@softWrapColumn)
getSoftTabs: ->
@state.get('softTabs')
# Defines whether to use soft tabs.
#
# softTabs - A {Boolean} which, if `true`, indicates that you want soft tabs.
setSoftTabs: (@softTabs) ->
setSoftTabs: (softTabs) ->
@state.set('softTabs', softTabs)
# Retrieves whether soft tabs are enabled.
#
# Returns a {Boolean}.
getSoftWrap: -> @softWrap
getSoftWrap: ->
@state.get('softWrap')
# Defines whether to use soft wrapping of text.
#
# softTabs - A {Boolean} which, if `true`, indicates that you want soft wraps.
setSoftWrap: (@softWrap) ->
setSoftWrap: (softWrap) ->
@state.set('softWrap', softWrap)
# Retrieves that character used to indicate a tab.
#
@@ -275,7 +296,7 @@ class EditSession
# Constructs the string used for tabs.
buildIndentString: (number) ->
if @softTabs
if @getSoftTabs()
_.multiplyString(" ", number * @getTabLength())
else
_.multiplyString("\t", Math.floor(number))
@@ -306,7 +327,7 @@ class EditSession
# Retrieves the current buffer's URI.
#
# Returns a {String}.
getUri: -> @getPath()
getUri: -> @buffer.getUri()
# {Delegates to: Buffer.isRowBlank}
isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow)
@@ -483,7 +504,7 @@ class EditSession
#
# bufferRange - The {Range} to perform the replace in
normalizeTabsInBufferRange: (bufferRange) ->
return unless @softTabs
return unless @getSoftTabs()
@scanInBufferRange /\t/, bufferRange, ({replace}) => replace(@getTabText())
# Performs a cut to the end of the current line.
@@ -596,8 +617,7 @@ class EditSession
#
# Returns `true` if the buffer row is folded, `false` otherwise.
isFoldedAtBufferRow: (bufferRow) ->
screenRow = @screenPositionForBufferPosition([bufferRow]).row
@isFoldedAtScreenRow(screenRow)
@displayBuffer.isFoldedAtBufferRow(bufferRow)
# Determines if the given screen row is folded.
#
@@ -605,7 +625,7 @@ class EditSession
#
# Returns `true` if the screen row is folded, `false` otherwise.
isFoldedAtScreenRow: (screenRow) ->
@lineForScreenRow(screenRow)?.fold?
@displayBuffer.isFoldedAtScreenRow(screenRow)
# {Delegates to: DisplayBuffer.largestFoldContainingBufferRow}
largestFoldContainingBufferRow: (bufferRow) ->
@@ -769,15 +789,18 @@ class EditSession
selection.insertText(fn(text))
selection.setBufferRange(range)
pushOperation: (operation) ->
@buffer.pushOperation(operation, this)
### Public ###
# Returns a valid {DisplayBufferMarker} object for the given id if one exists.
getMarker: (id) ->
@displayBuffer.getMarker(id)
getMarkers: ->
@displayBuffer.getMarkers()
findMarkers: (attributes) ->
@displayBuffer.findMarkers(attributes)
# {Delegates to: DisplayBuffer.markScreenRange}
markScreenRange: (args...) ->
@displayBuffer.markScreenRange(args...)
@@ -808,6 +831,9 @@ class EditSession
hasMultipleCursors: ->
@getCursors().length > 1
getAllCursors: ->
@getCursors().concat(@getRemoteCursors())
# Retrieves all the cursors.
#
# Returns an {Array} of {Cursor}s.
@@ -819,14 +845,16 @@ class EditSession
getCursor: ->
_.last(@cursors)
getRemoteCursors: -> new Array(@remoteCursors...)
# Adds a cursor at the provided `screenPosition`.
#
# screenPosition - An {Array} of two numbers: the screen row, and the screen column.
#
# Returns the new {Cursor}.
addCursorAtScreenPosition: (screenPosition) ->
marker = @markScreenPosition(screenPosition, invalidationStrategy: 'never')
@addSelection(marker).cursor
@markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
@getLastSelection().cursor
# Adds a cursor at the provided `bufferPosition`.
#
@@ -834,8 +862,8 @@ class EditSession
#
# Returns the new {Cursor}.
addCursorAtBufferPosition: (bufferPosition) ->
marker = @markBufferPosition(bufferPosition, invalidationStrategy: 'never')
@addSelection(marker).cursor
@markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
@getLastSelection().cursor
# Adds a cursor to the `EditSession`.
#
@@ -844,7 +872,10 @@ class EditSession
# Returns the new {Cursor}.
addCursor: (marker) ->
cursor = new Cursor(editSession: this, marker: marker)
@cursors.push(cursor)
if marker.isLocal()
@cursors.push(cursor)
else
@remoteCursors.push(cursor)
@trigger 'cursor-added', cursor
cursor
@@ -863,13 +894,18 @@ class EditSession
#
# Returns the new {Selection}.
addSelection: (marker, options={}) ->
unless options.preserveFolds
unless marker.getAttributes().preserveFolds
@destroyFoldsIntersectingBufferRange(marker.getBufferRange())
cursor = @addCursor(marker)
selection = new Selection(_.extend({editSession: this, marker, cursor}, options))
@selections.push(selection)
if marker.isLocal()
@selections.push(selection)
else
@remoteSelections.push(selection)
selectionBufferRange = selection.getBufferRange()
@mergeIntersectingSelections() unless options.suppressMerge
@mergeIntersectingSelections()
if selection.destroyed
for selection in @getSelections()
if selection.intersectsBufferRange(selectionBufferRange)
@@ -885,9 +921,8 @@ class EditSession
#
# Returns the new {Selection}.
addSelectionForBufferRange: (bufferRange, options={}) ->
options = _.defaults({invalidationStrategy: 'never'}, options)
marker = @markBufferRange(bufferRange, options)
@addSelection(marker, options)
@markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
@getLastSelection()
# Given a buffer range, this removes all previous selections and creates a new selection for it.
#
@@ -906,19 +941,22 @@ class EditSession
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)
@mergeIntersectingSelections options, =>
for bufferRange, i in bufferRanges
bufferRange = Range.fromObject(bufferRange)
if selections[i]
selections[i].setBufferRange(bufferRange, options)
else
@addSelectionForBufferRange(bufferRange, options)
# Unselects a given selection.
#
# selection - The {Selection} to remove.
removeSelection: (selection) ->
_.remove(@selections, selection)
if selection.isLocal()
_.remove(@selections, selection)
else
_.remove(@remoteSelections, selection)
# Clears every selection. TODO
clearSelections: ->
@@ -933,6 +971,9 @@ class EditSession
else
false
getAllSelections: ->
@getSelections().concat(@getRemoteSelections())
# Gets all the selections.
#
# Returns an {Array} of {Selection}s.
@@ -953,14 +994,16 @@ class EditSession
getLastSelection: ->
_.last(@selections)
getRemoteSelections: -> new Array(@remoteSelections...)
# Gets all selections, ordered by their position in the buffer.
#
# Returns an {Array} of {Selection}s.
getSelectionsOrderedByBufferPosition: ->
@getSelections().sort (a, b) ->
aRange = a.getBufferRange()
bRange = b.getBufferRange()
aRange.end.compare(bRange.end)
@getSelections().sort (a, b) -> a.compare(b)
getRemoteSelectionsOrderedByBufferPosition: ->
@getRemoteSelections().sort (a, b) -> a.compare(b)
# Gets the very last selection, as it's ordered in the buffer.
#
@@ -1031,6 +1074,9 @@ class EditSession
getSelectedBufferRanges: ->
selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition()
getRemoteSelectedBufferRanges: ->
selection.getBufferRange() for selection in @getRemoteSelectionsOrderedByBufferPosition()
# Gets the selected text of the most recently added {Selection}.
#
# Returns a {String}.
@@ -1128,7 +1174,7 @@ class EditSession
selectToScreenPosition: (position) ->
lastSelection = @getLastSelection()
lastSelection.selectToScreenPosition(position)
@mergeIntersectingSelections(reverse: lastSelection.isReversed())
@mergeIntersectingSelections(isReversed: lastSelection.isReversed())
# Selects the text one position right of the cursor.
selectRight: ->
@@ -1250,14 +1296,6 @@ class EditSession
@setSelectedBufferRange(range)
range
# Given a buffer position, this finds all markers that contain the position.
#
# bufferPosition - A {Point} to check
#
# Returns an {Array} of {Numbers}, representing marker IDs containing `bufferPosition`.
markersForBufferPosition: (bufferPosition) ->
@buffer.markersForPosition(bufferPosition)
mergeCursors: ->
positions = []
for cursor in @getCursors()
@@ -1268,25 +1306,38 @@ class EditSession
positions.push(position)
expandSelectionsForward: (fn) ->
fn(selection) for selection in @getSelections()
@mergeIntersectingSelections()
@mergeIntersectingSelections =>
fn(selection) for selection in @getSelections()
expandSelectionsBackward: (fn) ->
fn(selection) for selection in @getSelections()
@mergeIntersectingSelections(reverse: true)
@mergeIntersectingSelections isReversed: true, =>
fn(selection) for selection in @getSelections()
finalizeSelections: ->
selection.finalize() for selection in @getSelections()
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
# Merges intersecting selections. If passed a function, it executes the function
# with merging suppressed, then merges intersecting selections afterward.
mergeIntersectingSelections: (args...) ->
fn = args.pop() if _.isFunction(_.last(args))
options = args.pop() ? {}
return fn?() if @suppressSelectionMerging
if fn?
@suppressSelectionMerging = true
result = fn()
@suppressSelectionMerging = false
reducer = (disjointSelections, selection) ->
intersectingSelection = _.find(disjointSelections, (s) -> s.intersectsWith(selection))
if intersectingSelection?
intersectingSelection.merge(selection, options)
disjointSelections
else
disjointSelections.concat([selection])
_.reduce(@getSelections(), reducer, [])
preserveCursorPositionOnBufferReload: ->
cursorPosition = null
@@ -1315,9 +1366,11 @@ class EditSession
transact: (fn) -> @buffer.transact(fn)
commit: -> @buffer.commit()
beginTransaction: -> @buffer.beginTransaction()
abort: -> @buffer.abort()
commitTransaction: -> @buffer.commitTransaction()
abortTransaction: -> @buffer.abortTransaction()
inspect: ->
JSON.stringify @state.toObject()
@@ -1328,6 +1381,13 @@ class EditSession
@unfoldAll()
@trigger 'grammar-changed'
handleMarkerCreated: (marker) =>
if marker.matchesAttributes(@getSelectionMarkerAttributes())
@addSelection(marker)
getSelectionMarkerAttributes: ->
type: 'selection', editSessionId: @id, invalidation: 'never'
getDebugSnapshot: ->
[
@displayBuffer.getDebugSnapshot()

View File

@@ -1,8 +1,7 @@
{View, $$} = require 'space-pen'
Buffer = require 'text-buffer'
TextBuffer = require 'text-buffer'
Gutter = require 'gutter'
Point = require 'point'
Range = require 'range'
{Point, Range} = require 'telepath'
EditSession = require 'edit-session'
CursorView = require 'cursor-view'
SelectionView = require 'selection-view'
@@ -97,7 +96,7 @@ class Editor extends View
@edit(editSession)
else if @mini
@edit(new EditSession
buffer: new Buffer()
buffer: new TextBuffer
softWrap: false
tabLength: 2
softTabs: true
@@ -586,6 +585,9 @@ class Editor extends View
# {Delegates to: EditSession.getPath}
getPath: -> @activeEditSession?.getPath()
# {Delegates to: EditSession.getRelativePath}
getRelativePath: -> @activeEditSession?.getRelativePath()
# {Delegates to: Buffer.getLineCount}
getLineCount: -> @getBuffer().getLineCount()
@@ -704,8 +706,7 @@ class Editor extends View
$(document).one "mouseup.editor-#{@id}", =>
clearInterval(interval)
$(document).off 'mousemove', moveHandler
reverse = @activeEditSession.getLastSelection().isReversed()
@activeEditSession.mergeIntersectingSelections({reverse})
@activeEditSession.mergeIntersectingSelections(isReversed: @activeEditSession.getLastSelection().isReversed())
@activeEditSession.finalizeSelections()
@syncCursorAnimations()
@@ -1133,8 +1134,8 @@ class Editor extends View
@updateLayerDimensions()
@scrollTop(editSessionScrollTop)
@scrollLeft(editSessionScrollLeft)
@newCursors = @activeEditSession.getCursors()
@newSelections = @activeEditSession.getSelections()
@newCursors = @activeEditSession.getAllCursors()
@newSelections = @activeEditSession.getAllSelections()
@updateDisplay(suppressAutoScroll: true)
requestDisplayUpdate: ->
@@ -1278,6 +1279,7 @@ class Editor extends View
)
intactRanges = newIntactRanges
@pendingChanges = []
intactRanges
truncateIntactRanges: (intactRanges, renderFrom, renderTo) ->
@@ -1670,8 +1672,9 @@ class Editor extends View
console.log @activeEditSession.getCursorScopes()
transact: (fn) -> @activeEditSession.transact(fn)
commit: -> @activeEditSession.commit()
abort: -> @activeEditSession.abort()
beginTransaction: -> @activeEditSession.beginTransaction()
commitTransaction: -> @activeEditSession.commitTransaction()
abortTransaction: -> @activeEditSession.abortTransaction()
saveDebugSnapshot: ->
atom.showSaveDialog (path) =>

View File

@@ -1,5 +1,4 @@
Range = require 'range'
Point = require 'point'
{Point, Range} = require 'telepath'
# Public: Represents a fold that collapses multiple buffer lines into a single
# line on the screen.
@@ -18,10 +17,14 @@ class Fold
@displayBuffer.foldsByMarkerId[@marker.id] = this
@updateDisplayBuffer()
@marker.on 'destroyed', => @destroyed()
@marker.on 'changed', ({isValid}) => @destroy() unless isValid
# Returns whether this fold is contained within another fold
isInsideLargerFold: ->
@displayBuffer.findMarker(class: 'fold', containsBufferRange: @getBufferRange())?
if largestContainingFoldMarker = @displayBuffer.findMarker(class: 'fold', containsBufferRange: @getBufferRange())
not largestContainingFoldMarker.getBufferRange().isEqual(@getBufferRange())
else
false
# Destroys this fold
destroy: ->

View File

@@ -10,6 +10,28 @@ GitUtils = require 'git-utils'
# Ultimately, this is an overlay to the native [git-utils](https://github.com/atom/node-git) module.
module.exports =
class Git
### Public ###
# Creates a new `Git` instance.
#
# path - The git repository to open
# options - A hash with one key:
# refreshOnWindowFocus: A {Boolean} that identifies if the windows should refresh
#
# Returns a new {Git} object.
@open: (path, options) ->
return null unless path
try
new Git(path, options)
catch e
null
@exists: (path) ->
if git = @open(path)
git.destroy()
true
else
false
path: null
statuses: null
upstream: null
@@ -57,20 +79,6 @@ class Git
### Public ###
# Creates a new `Git` instance.
#
# path - The git repository to open
# options - A hash with one key:
# refreshOnWindowFocus: A {Boolean} that identifies if the windows should refresh
#
# Returns a new {Git} object.
@open: (path, options) ->
return null unless path
try
new Git(path, options)
catch e
null
# Retrieves the git repository.
#
# Returns a new `Repository`.
@@ -213,6 +221,16 @@ class Git
# Returns an object with two keys, `ahead` and `behind`. These will always be greater than zero.
getLineDiffs: (path, text) -> @getRepo().getLineDiffs(@relativize(path), text)
getConfigValue: (key) -> @getRepo().getConfigValue(key)
getOriginUrl: -> @getConfigValue('remote.origin.url')
getReferenceTarget: (reference) -> @getRepo().getReferenceTarget(reference)
getAheadBehindCount: (reference) -> @getRepo().getAheadBehindCount(reference)
hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")?
### Internal ###
refreshStatus: ->

View File

@@ -1,5 +1,5 @@
{View, $$, $$$} = require 'space-pen'
Range = require 'range'
{Range} = require 'telepath'
$ = require 'jquery'
_ = require 'underscore'

View File

@@ -1,4 +1,4 @@
Range = require 'range'
{Range} = require 'telepath'
_ = require 'underscore'
require 'underscore-extensions'
{OnigRegExp} = require 'oniguruma'

View File

@@ -15,10 +15,10 @@ class PaneAxis extends View
@state = args[0]
@state.get('children').each (child, index) => @addChild(deserialize(child), index, updateState: false)
else
@state = telepath.Document.create(deserializer: @className(), children: [])
@state = site.createDocument(deserializer: @className(), children: [])
@addChild(child) for child in args
@state.get('children').observe ({index, inserted, removed, site}) =>
@state.get('children').on 'changed', ({index, inserted, removed, site}) =>
return if site is @state.site.id
for childState in removed
@removeChild(@children(":eq(#{index})").view(), updateState: false)
@@ -27,7 +27,7 @@ class PaneAxis extends View
addChild: (child, index=@children().length, options={}) ->
@insertAt(index, child)
@state.get('children').insert(index, child.serialize()) if options.updateState ? true
@state.get('children').insert(index, child.getState()) if options.updateState ? true
@getContainer()?.adjustPaneDimensions()
removeChild: (child, options={}) ->
@@ -83,8 +83,9 @@ class PaneAxis extends View
children.insert(childIndex + 1, newChild.getState())
serialize: ->
child.serialize() for child in @children().views()
@state
state = @state.clone()
state.set('children', child.serialize() for child in @children().views())
state
getState: -> @state

View File

@@ -18,22 +18,27 @@ class PaneContainer extends View
@content: ->
@div id: 'panes'
initialize: (@state) ->
if @state?
@setRoot(deserialize(@state.get('root')), updateState: false)
initialize: (state) ->
if state instanceof telepath.Document
@state = state
@setRoot(deserialize(@state.get('root')))
else
@state = telepath.Document.create(deserializer: 'PaneContainer')
@state = site.createDocument(deserializer: 'PaneContainer')
@state.observe ({key, newValue, site}) =>
@state.on 'changed', ({key, newValue, site}) =>
return if site is @state.site.id
if key is 'root'
@setRoot(deserialize(newValue), updateState: false)
if newValue?
@setRoot(deserialize(newValue))
else
@setRoot(null)
@destroyedItemStates = []
serialize: ->
@getRoot()?.serialize()
@state
state = @state.clone()
state.set('root', @getRoot()?.serialize())
state
getState: -> @state
@@ -89,10 +94,10 @@ class PaneContainer extends View
getRoot: ->
@children().first().view()
setRoot: (root, options={}) ->
setRoot: (root) ->
@empty()
@append(root) if root?
@state.set(root: root?.getState()) if options.updateState ? true
@state.set(root: root?.getState())
removeChild: (child) ->
throw new Error("Removing non-existant child") unless @getRoot() is child

View File

@@ -29,29 +29,28 @@ class Pane extends View
initialize: (args...) ->
if args[0] instanceof telepath.Document
@state = args[0]
@items = @state.get('items').map (item) -> deserialize(item)
@items = _.compact(@state.get('items').map (item) -> deserialize(item))
else
@items = args
@state = telepath.Document.create
@state = site.createDocument
deserializer: 'Pane'
items: @items.map (item) -> item.getState?() ? item.serialize()
@state.get('items').observe ({index, removed, inserted, site}) =>
@state.get('items').on 'changed', ({index, removed, inserted, site}) =>
return if site is @state.site.id
for itemState in removed
@removeItemAtIndex(index, updateState: false)
for itemState, i in inserted
@addItem(deserialize(itemState), index + i, updateState: false)
@state.observe ({key, newValue, site}) =>
@state.on 'changed', ({key, newValue, site}) =>
return if site is @state.site.id
@showItemForUri(newValue) if key is 'activeItemUri'
@viewsByClassName = {}
@viewsByItem = new WeakMap()
if activeItemUri = @state.get('activeItemUri')
@showItemForUri(activeItemUri)
else
activeItemUri = @state.get('activeItemUri')
unless activeItemUri? and @showItemForUri(activeItemUri)
@showItem(@items[0]) if @items.length > 0
@command 'core:close', @destroyActiveItem
@@ -226,7 +225,7 @@ class Pane extends View
saveItemAs: (item, nextAction) ->
return unless item.saveAs?
itemPath = item.getUri?()
itemPath = item.getPath?()
itemPath = dirname(itemPath) if itemPath
path = atom.showSaveDialogSync(itemPath)
if path
@@ -272,7 +271,11 @@ class Pane extends View
_.detect @items, (item) -> item.getUri?() is uri
showItemForUri: (uri) ->
@showItem(@itemForUri(uri))
if item = @itemForUri(uri)
@showItem(item)
true
else
false
cleanupItemView: (item) ->
if item instanceof $
@@ -318,9 +321,10 @@ class Pane extends View
@viewForItem(@activeItem)
serialize: ->
@state.get('items').set(index, item.serialize()) for item, index in @items
@state.set focused: @is(':has(:focus)')
@state
state = @state.clone()
state.set('items', item.serialize() for item, index in @items)
state.set('focused', @is(':has(:focus)'))
state
getState: -> @state
@@ -377,7 +381,7 @@ class Pane extends View
@closest('#panes').view()
copyActiveItem: ->
deserialize(@activeItem.serialize())
@activeItem.copy?() ? deserialize(@activeItem.serialize())
remove: (selector, keepData) ->
return super if keepData

View File

@@ -1,182 +0,0 @@
_ = require 'underscore'
# Public: Represents a coordinate in the editor.
#
# Each `Point` is actually an object with two properties: `row` and `column`.
module.exports =
class Point
# Constructs a `Point` from a given object.
#
# object - This can be an {Array} (`[startRow, startColumn, endRow, endColumn]`) or an object `{row, column}`
#
# Returns the new {Point}.
@fromObject: (object) ->
if object instanceof Point
object
else
if _.isArray(object)
[row, column] = object
else
{ row, column } = object
new Point(row, column)
# Identifies which `Point` is smaller.
#
# "Smaller" means that both the `row` and `column` values of one `Point` are less than or equal
# to the other.
#
# point1 - The first {Point} to check
# point2 - The second {Point} to check
#
# Returns the smaller {Point}.
@min: (point1, point2) ->
point1 = @fromObject(point1)
point2 = @fromObject(point2)
if point1.isLessThanOrEqual(point2)
point1
else
point2
# Creates a new `Point` object.
#
# row - A {Number} indicating the row (default: 0)
# column - A {Number} indicating the column (default: 0)
#
# Returns a {Point},
constructor: (@row=0, @column=0) ->
# Creates an identical copy of the `Point`.
#
# Returns a duplicate {Point}.
copy: ->
new Point(@row, @column)
# Adds the `column`s of two `Point`s together.
#
# other - The {Point} to add with
#
# Returns the new {Point}.
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)
# Moves a `Point`.
#
# In other words, the `row` values and `column` values are added to each other.
#
# other - The {Point} to add with
#
# Returns the new {Point}.
translate: (other) ->
other = Point.fromObject(other)
new Point(@row + other.row, @column + other.column)
# Creates two new `Point`s, split down a `column` value.
#
# In other words, given a point, this creates `Point(0, column)` and `Point(row, column)`.
#
# column - The {Number} to split at
#
# Returns an {Array} of two {Point}s.
splitAt: (column) ->
if @row == 0
rightColumn = @column - column
else
rightColumn = @column
[new Point(0, column), new Point(@row, rightColumn)]
# Compares two `Point`s.
#
# other - The {Point} to compare against
#
# Returns a {Number} matching the following rules:
# * If the first `row` is greater than `other.row`, returns `1`.
# * If the first `row` is less than `other.row`, returns `-1`.
# * If the first `column` is greater than `other.column`, returns `1`.
# * If the first `column` is less than `other.column`, returns `-1`.
#
# Otherwise, returns `0`.
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
# Identifies if two `Point`s are equal.
#
# other - The {Point} to compare against
#
# Returns a {Boolean}.
isEqual: (other) ->
return false unless other
other = Point.fromObject(other)
@row == other.row and @column == other.column
# Identifies if one `Point` is less than another.
#
# other - The {Point} to compare against
#
# Returns a {Boolean}.
isLessThan: (other) ->
@compare(other) < 0
# Identifies if one `Point` is less than or equal to another.
#
# other - The {Point} to compare against
#
# Returns a {Boolean}.
isLessThanOrEqual: (other) ->
@compare(other) <= 0
# Identifies if one `Point` is greater than another.
#
# other - The {Point} to compare against
#
# Returns a {Boolean}.
isGreaterThan: (other) ->
@compare(other) > 0
# Identifies if one `Point` is greater than or equal to another.
#
# other - The {Point} to compare against
#
# Returns a {Boolean}.
isGreaterThanOrEqual: (other) ->
@compare(other) >= 0
# Converts the {Point} to a String.
#
# Returns a {String}.
toString: ->
"#{@row},#{@column}"
# Converts the {Point} to an Array.
#
# Returns an {Array}.
toArray: ->
[@row, @column]
### Internal ###
inspect: ->
"(#{@row}, #{@column})"
# Internal:
serialize: ->
@toArray()

View File

@@ -1,13 +1,17 @@
fsUtils = require 'fs-utils'
path = require 'path'
url = require 'url'
_ = require 'underscore'
$ = require 'jquery'
Range = require 'range'
Buffer = require 'text-buffer'
telepath = require 'telepath'
{Range} = telepath
TextBuffer = require 'text-buffer'
EditSession = require 'edit-session'
EventEmitter = require 'event-emitter'
Directory = require 'directory'
BufferedNodeProcess = require 'buffered-node-process'
Git = require 'git'
# Public: Represents a project that's opened in Atom.
#
@@ -15,9 +19,12 @@ BufferedNodeProcess = require 'buffered-node-process'
# of directories and files that you can operate on.
module.exports =
class Project
@acceptsDocuments: true
@version: 1
registerDeserializer(this)
@deserialize: (state) -> new Project(state.path)
@deserialize: (state) -> new Project(state)
@openers: []
@@ -27,6 +34,11 @@ class Project
@unregisterOpener: (opener) ->
_.remove(@openers, opener)
@pathForRepositoryUrl: (repoUrl) ->
[repoName] = url.parse(repoUrl).path.split('/')[-1..]
repoName = repoName.replace(/\.git$/, '')
path.join(config.get('core.projectHome'), repoName)
tabLength: 2
softTabs: true
softWrap: false
@@ -39,20 +51,50 @@ class Project
destroy: ->
editSession.destroy() for editSession in @getEditSessions()
buffer.release() for buffer in @getBuffers()
if @repo?
@repo.destroy()
@repo = null
### Public ###
# Establishes a new project at a given path.
#
# path - The {String} name of the path
constructor: (path) ->
@setPath(path)
constructor: (pathOrState) ->
@editSessions = []
@buffers = []
if pathOrState instanceof telepath.Document
@state = pathOrState
if projectPath = @state.remove('path')
@setPath(projectPath)
else
@setPath(@constructor.pathForRepositoryUrl(@state.get('repoUrl')))
@state.get('buffers').each (bufferState) =>
if buffer = deserialize(bufferState, project: this)
@addBuffer(buffer, updateState: false)
else
@state = site.createDocument(deserializer: @constructor.name, version: @constructor.version, buffers: [])
@setPath(pathOrState)
@state.get('buffers').on 'changed', ({inserted, removed, index, site}) =>
return if site is @state.site.id
for removedBuffer in removed
@removeBufferAtIndex(index, updateState: false)
for insertedBuffer, i in inserted
@addBufferAtIndex(deserialize(insertedBuffer, project: this), index + i, updateState: false)
serialize: ->
deserializer: 'Project'
path: @getPath()
state = @state.clone()
state.set('path', @getPath())
state.set('buffers', buffer.serialize() for buffer in @getBuffers())
state
getState: -> @state
getRepo: -> @repo
# Retrieves the project path.
#
@@ -69,8 +111,15 @@ class Project
if projectPath?
directory = if fsUtils.isDirectorySync(projectPath) then projectPath else path.dirname(projectPath)
@rootDirectory = new Directory(directory)
@repo = Git.open(projectPath)
else
@rootDirectory = null
if @repo?
@repo.destroy()
@repo = null
if originUrl = @repo?.getOriginUrl()
@state.set('repoUrl', originUrl)
@trigger "path-changed"
@@ -110,7 +159,7 @@ class Project
#
# Returns a {Boolean}.
ignoreRepositoryPath: (repositoryPath) ->
config.get("core.hideGitIgnoredFiles") and git?.isPathIgnored(path.join(@getPath(), repositoryPath))
config.get("core.hideGitIgnoredFiles") and @repo?.isPathIgnored(path.join(@getPath(), repositoryPath))
# Given a uri, this resolves it relative to the project directory. If the path
# is already absolute or if it is prefixed with a scheme, it is returned unchanged.
@@ -168,6 +217,7 @@ class Project
#
# Returns either an {EditSession} (for text) or {ImageEditSession} (for images).
open: (filePath, options={}) ->
filePath = @resolve(filePath) if filePath?
for opener in @constructor.openers
return resource if resource = opener(filePath, options)
@@ -215,23 +265,40 @@ class Project
else
@buildBuffer(null, text)
bufferForId: (id) ->
_.find @buffers, (buffer) -> buffer.id is id
# Given a file path, this sets its {Buffer}.
#
# filePath - A {String} representing a path
# text - The {String} text to use as a buffer
#
# Returns the {Buffer}.
buildBuffer: (filePath, text) ->
buffer = new Buffer(filePath, text)
@buffers.push buffer
buildBuffer: (filePath, initialText) ->
filePath = @resolve(filePath) if filePath?
buffer = new TextBuffer({project: this, filePath, initialText})
@addBuffer(buffer)
@trigger 'buffer-created', buffer
buffer
addBuffer: (buffer, options={}) ->
@addBufferAtIndex(buffer, @buffers.length, options)
addBufferAtIndex: (buffer, index, options={}) ->
@buffers[index] = buffer
@state.get('buffers').insert(index, buffer.getState()) if options.updateState ? true
# Removes a {Buffer} association from the project.
#
# Returns the removed {Buffer}.
removeBuffer: (buffer) ->
_.remove(@buffers, buffer)
index = @buffers.indexOf(buffer)
@removeBufferAtIndex(index) unless index is -1
removeBufferAtIndex: (index, options={}) ->
[buffer] = @buffers.splice(index, 1)
@state.get('buffers').remove(index) if options.updateState ? true
buffer?.destroy()
# Performs a search across all the files in the project.
#

View File

@@ -1,216 +0,0 @@
Point = require 'point'
_ = require 'underscore'
# Public: Indicates a region within the editor.
#
# To better visualize how this works, imagine a rectangle.
# Each quadrant of the rectangle is analogus to a range, as ranges contain a
# starting row and a starting column, as well as an ending row, and an ending column.
#
# Each `Range` is actually constructed of two `Point` objects, labelled `start` and `end`.
module.exports =
class Range
# Constructs a `Range` from a given object.
#
# object - This can be an {Array} (`[startRow, startColumn, endRow, endColumn]`) or an object `{start: Point, end: Point}`
#
# Returns the new {Range}.
@fromObject: (object) ->
if _.isArray(object)
new Range(object...)
else if object instanceof Range
object
else
new Range(object.start, object.end)
# Constructs a `Range` from a {Point}, and the delta values beyond that point.
#
# point - A {Point} to start with
# rowDelta - A {Number} indicating how far from the starting {Point} the range's row should be
# columnDelta - A {Number} indicating how far from the starting {Point} the range's column should be
#
# Returns the new {Range}.
@fromPointWithDelta: (pointA, rowDelta, columnDelta) ->
pointA = Point.fromObject(pointA)
pointB = new Point(pointA.row + rowDelta, pointA.column + columnDelta)
new Range(pointA, pointB)
# Creates a new `Range` object based on two {Point}s.
#
# pointA - The first {Point} (default: `0, 0`)
# pointB - The second {Point} (default: `0, 0`)
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
# Creates an identical copy of the `Range`.
#
# Returns a duplicate {Range}.
copy: ->
new Range(@start.copy(), @end.copy())
# Identifies if two `Range`s are equal.
#
# All four points (`start.row`, `start.column`, `end.row`, `end.column`) must be
# equal for this method to return `true`.
#
# other - A different {Range} to check against
#
# Returns a {Boolean}.
isEqual: (other) ->
if _.isArray(other) and other.length == 2
other = new Range(other...)
other.start.isEqual(@start) and other.end.isEqual(@end)
# Returns an integer (-1, 0, 1) indicating whether this range is less than, equal,
# or greater than the given range when sorting.
#
# Ranges that start earlier are considered "less than" ranges that start later.
# If ranges start at the same location, the larger range sorts before the smaller
# range.
#
# other - A {Range} to compare against.
#
# Returns a {Number}, either -1, 0, or 1.
compare: (other) ->
other = Range.fromObject(other)
if value = @start.compare(other.start)
value
else
other.end.compare(@end)
# Identifies if the `Range` is on the same line.
#
# In other words, if `start.row` is equal to `end.row`.
#
# Returns a {Boolean}.
isSingleLine: ->
@start.row == @end.row
# Identifies if two `Range`s are on the same line.
#
# other - A different {Range} to check against
#
# Returns a {Boolean}.
coversSameRows: (other) ->
@start.row == other.start.row && @end.row == other.end.row
# Adds a new point to the `Range`s `start` and `end`.
#
# point - A new {Point} to add
#
# Returns the new {Range}.
add: (point) ->
new Range(@start.add(point), @end.add(point))
# Moves a `Range`.
#
# In other words, the starting and ending `row` values, and the starting and ending
# `column` values, are added to each other.
#
# startPoint - The {Point} to move the `Range`s `start` by
# endPoint - The {Point} to move the `Range`s `end` by
#
# Returns the new {Range}.
translate: (startPoint, endPoint=startPoint) ->
new Range(@start.translate(startPoint), @end.translate(endPoint))
# Identifies if two `Range`s intersect each other.
#
# otherRange - A different {Range} to check against
#
# Returns a {Boolean}.
intersectsWith: (otherRange) ->
if @start.isLessThanOrEqual(otherRange.start)
@end.isGreaterThanOrEqual(otherRange.start)
else
otherRange.intersectsWith(this)
# Identifies if a second `Range` is contained within a first.
#
# otherRange - A different {Range} to check against
# options - A hash with a single option:
# exclusive: A {Boolean} which, if `true`, indicates that no {Point}s in the `Range` can be equal
#
# Returns a {Boolean}.
containsRange: (otherRange, {exclusive} = {}) ->
{ start, end } = Range.fromObject(otherRange)
@containsPoint(start, {exclusive}) and @containsPoint(end, {exclusive})
# Identifies if a `Range` contains a {Point}.
#
# point - A {Point} to check against
# options - A hash with a single option:
# exclusive: A {Boolean} which, if `true`, indicates that no {Point}s in the `Range` can be equal
#
# Returns a {Boolean}.
containsPoint: (point, {exclusive} = {}) ->
point = Point.fromObject(point)
if exclusive
point.isGreaterThan(@start) and point.isLessThan(@end)
else
point.isGreaterThanOrEqual(@start) and point.isLessThanOrEqual(@end)
# Identifies if a `Range` contains a row.
#
# row - A row {Number} to check against
# options - A hash with a single option:
#
# Returns a {Boolean}.
containsRow: (row) ->
@start.row <= row <= @end.row
# Constructs a union between two `Range`s.
#
# otherRange - A different {Range} to unionize with
#
# Returns the new {Range}.
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)
# Identifies if a `Range` is empty.
#
# A `Range` is empty if its start {Point} matches its end.
#
# Returns a {Boolean}.
isEmpty: ->
@start.isEqual(@end)
# Calculates the difference between a `Range`s `start` and `end` points.
#
# Returns a {Point}.
toDelta: ->
rows = @end.row - @start.row
if rows == 0
columns = @end.column - @start.column
else
columns = @end.column
new Point(rows, columns)
# Calculates the number of rows a `Range`s contains.
#
# Returns a {Number}.
getRowCount: ->
@end.row - @start.row + 1
# Returns an array of all rows in a `Range`
#
# Returns an {Array}
getRows: ->
[@start.row..@end.row]
### Internal ###
inspect: ->
"[#{@start.inspect()} - #{@end.inspect()}]"

View File

@@ -1,3 +1,4 @@
path = require 'path'
$ = require 'jquery'
{$$} = require 'space-pen'
fsUtils = require 'fs-utils'
@@ -26,6 +27,7 @@ class RootView extends View
excludeVcsIgnoredPaths: false
disabledPackages: []
themes: ['atom-dark-ui', 'atom-dark-syntax']
projectHome: path.join(atom.getHomeDirPath(), 'github')
### Internal ###
@acceptsDocuments: true
@@ -34,7 +36,7 @@ class RootView extends View
@div id: 'root-view', =>
@div id: 'horizontal', outlet: 'horizontal', =>
@div id: 'vertical', outlet: 'vertical', =>
@subview 'panes', deserialize(state?.get?('panes')) ? new PaneContainer
@div outlet: 'panes'
@deserialize: (state) ->
new RootView(state)
@@ -42,8 +44,16 @@ class RootView extends View
initialize: (state={}) ->
if state instanceof telepath.Document
@state = state
panes = deserialize(state.get('panes'))
else
@state = telepath.Document.create(_.extend({version: RootView.version, deserializer: 'RootView', panes: @panes.serialize()}, state))
panes = new PaneContainer
@state = site.createDocument
deserializer: @constructor.name
version: @constructor.version
panes: panes.getState()
@panes.replaceWith(panes)
@panes = panes
@on 'focus', (e) => @handleFocus(e)
@subscribe $(window), 'focus', (e) =>
@@ -82,9 +92,12 @@ class RootView extends View
_.nextTick => atom.setFullScreen(@state.get('fullScreen'))
serialize: ->
@panes.serialize()
@state.set('fullScreen', atom.isFullScreen())
@state
state = @state.clone()
state.set('panes', @panes.serialize())
state.set('fullScreen', atom.isFullScreen())
state
getState: -> @state
handleFocus: (e) ->
if @getActivePane()
@@ -113,7 +126,7 @@ class RootView extends View
# Returns the `EditSession` for the file URI.
open: (path, options = {}) ->
changeFocus = options.changeFocus ? true
path = project.resolve(path) if path?
path = project.relativize(path)
if activePane = @getActivePane()
editSession = activePane.itemForUri(path) ? project.open(path)
activePane.showItem(editSession)

View File

@@ -1,5 +1,4 @@
Point = require 'point'
Range = require 'range'
{Point, Range} = require 'telepath'
{View, $$} = require 'space-pen'
# Internal:
@@ -19,6 +18,9 @@ class SelectionView extends View
@needsRemoval = true
@editor.requestDisplayUpdate()
if @selection.marker.isRemote()
@addClass("site-#{@selection.marker.getOriginSiteId()}")
updateDisplay: ->
@clearRegions()
range = @getScreenRange()

View File

@@ -1,4 +1,4 @@
Range = require 'range'
{Range} = require 'telepath'
EventEmitter = require 'event-emitter'
_ = require 'underscore'
@@ -9,25 +9,21 @@ class Selection
marker: null
editSession: null
initialScreenRange: null
goalBufferRange: null
wordwise: false
needsAutoscroll: null
### Internal ###
constructor: ({@cursor, @marker, @editSession, @goalBufferRange}) ->
constructor: ({@cursor, @marker, @editSession}) ->
@cursor.selection = this
@marker.on 'changed', => @screenRangeChanged()
@cursor.on 'destroyed.selection', =>
@cursor = null
@destroy()
@marker.on 'destroyed', =>
@destroyed = true
@editSession.removeSelection(this)
@trigger 'destroyed' unless @editSession.destroyed
destroy: ->
return if @destroyed
@destroyed = true
@editSession.removeSelection(this)
@trigger 'destroyed' unless @editSession.destroyed
@cursor?.destroy()
@marker.destroy()
finalize: ->
@initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange())
@@ -88,7 +84,7 @@ class Selection
setBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
@needsAutoscroll = options.autoscroll
options.reverse ?= @isReversed()
options.isReversed ?= @isReversed()
@editSession.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
@modifySelection =>
@cursor.needsAutoscroll = false if options.autoscroll?
@@ -112,7 +108,8 @@ class Selection
# Clears the selection, moving the marker to move to the head.
clear: ->
@marker.clearTail()
@marker.setAttributes(goalBufferRange: null)
@marker.clearTail() unless @retainSelection
# Modifies the selection to mark the current word.
#
@@ -149,7 +146,7 @@ class Selection
@modifySelection =>
if @initialScreenRange
if position.isLessThan(@initialScreenRange.start)
@marker.setScreenRange([position, @initialScreenRange.end], reverse: true)
@marker.setScreenRange([position, @initialScreenRange.end], isReversed: true)
else
@marker.setScreenRange([@initialScreenRange.start, position])
else
@@ -228,7 +225,7 @@ class Selection
# Moves the selection down one row.
addSelectionBelow: ->
range = (@goalBufferRange ? @getBufferRange()).copy()
range = (@getGoalBufferRange() ? @getBufferRange()).copy()
nextRow = range.end.row + 1
for row in [nextRow..@editSession.getLastBufferRow()]
@@ -241,12 +238,15 @@ class Selection
else
continue if clippedRange.isEmpty()
@editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true)
@editSession.addSelectionForBufferRange(range, goalBufferRange: range)
break
getGoalBufferRange: ->
@marker.getAttributes().goalBufferRange
# Moves the selection up one row.
addSelectionAbove: ->
range = (@goalBufferRange ? @getBufferRange()).copy()
range = (@getGoalBufferRange() ? @getBufferRange()).copy()
previousRow = range.end.row - 1
for row in [previousRow..0]
@@ -259,7 +259,7 @@ class Selection
else
continue if clippedRange.isEmpty()
@editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true)
@editSession.addSelectionForBufferRange(range, goalBufferRange: range)
break
# Replaces text at the current selection.
@@ -283,7 +283,7 @@ class Selection
newBufferRange = @editSession.buffer.change(oldBufferRange, text)
if options.select
@setBufferRange(newBufferRange, reverse: wasReversed)
@setBufferRange(newBufferRange, isReversed: wasReversed)
else
@cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed
@@ -490,7 +490,7 @@ class Selection
modifySelection: (fn) ->
@retainSelection = true
@placeTail()
@plantTail()
fn()
@retainSelection = false
@@ -499,8 +499,8 @@ class Selection
# This only works if there isn't already a tail position.
#
# Returns a {Point} representing the new tail position.
placeTail: ->
@marker.placeTail()
plantTail: ->
@marker.plantTail()
# Identifies if a selection intersects with a given buffer range.
#
@@ -523,13 +523,24 @@ class Selection
# otherSelection - A `Selection` to merge with
# options - A hash of options matching those found in {.setBufferRange}
merge: (otherSelection, options) ->
myGoalBufferRange = @getGoalBufferRange()
otherGoalBufferRange = otherSelection.getGoalBufferRange()
if myGoalBufferRange? and otherGoalBufferRange?
options.goalBufferRange = myGoalBufferRange.union(otherGoalBufferRange)
else
options.goalBufferRange = myGoalBufferRange ? otherGoalBufferRange
@setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options)
if @goalBufferRange and otherSelection.goalBufferRange
@goalBufferRange = @goalBufferRange.union(otherSelection.goalBufferRange)
else if otherSelection.goalBufferRange
@goalBufferRange = otherSelection.goalBufferRange
otherSelection.destroy()
compare: (other) ->
@getBufferRange().compare(other.getBufferRange())
isLocal: ->
@marker.isLocal()
isRemote: ->
@marker.isRemote()
### Internal ###
screenRangeChanged: ->

View File

@@ -1,12 +1,10 @@
_ = require 'underscore'
telepath = require 'telepath'
{Point, Range} = telepath
fsUtils = require 'fs-utils'
File = require 'file'
Point = require 'point'
Range = require 'range'
EventEmitter = require 'event-emitter'
UndoManager = require 'undo-manager'
BufferChangeOperation = require 'buffer-change-operation'
BufferMarker = require 'buffer-marker'
guid = require 'guid'
# Public: Represents the contents of a file.
#
@@ -14,54 +12,69 @@ BufferMarker = require 'buffer-marker'
# the case, as a `Buffer` could be an unsaved chunk of text.
module.exports =
class TextBuffer
@idCounter = 1
@acceptsDocuments: true
@version: 2
registerDeserializer(this)
@deserialize: (state, params) ->
new this(state, params)
stoppedChangingDelay: 300
stoppedChangingTimeout: null
undoManager: null
cachedDiskContents: null
cachedMemoryContents: null
conflict: false
lines: null
lineEndings: null
file: null
validMarkers: null
invalidMarkers: null
refcount: 0
# Creates a new buffer.
#
# path - A {String} representing the file path
# initialText - A {String} setting the starting text
constructor: (path, initialText) ->
@id = @constructor.idCounter++
@nextMarkerId = 1
@validMarkers = {}
@invalidMarkers = {}
@lines = ['']
@lineEndings = []
if path
@setPath(path)
if initialText?
@setText(initialText)
@updateCachedDiskContents()
else if fsUtils.exists(path)
@reload()
else
@setText('')
constructor: (optionsOrState={}, params={}) ->
if optionsOrState instanceof telepath.Document
{@project} = params
@state = optionsOrState
@id = @state.get('id')
filePath = @state.get('relativePath')
@text = @state.get('text')
reloadFromDisk = @state.get('isModified') is false
else
@setText(initialText ? '')
{@project, filePath, initialText} = optionsOrState
@text = site.createDocument(initialText ? '', shareStrings: true)
reloadFromDisk = true
@id = guid.create().toString()
@state = site.createDocument
id: @id
deserializer: @constructor.name
version: @constructor.version
text: @text
@undoManager = new UndoManager(this)
@text.on 'changed', @handleTextChange
@text.on 'marker-created', (marker) => @trigger 'marker-created', marker
@text.on 'markers-updated', => @trigger 'markers-updated'
if filePath
@setPath(@project.resolve(filePath))
if fsUtils.exists(@getPath())
@updateCachedDiskContents()
@reload() if reloadFromDisk and @isModified()
@text.clearUndoStack()
### Internal ###
handleTextChange: (event) =>
@cachedMemoryContents = null
@conflict = false if @conflict and !@isModified()
bufferChangeEvent = _.pick(event, 'oldRange', 'newRange', 'oldText', 'newText')
@trigger 'changed', bufferChangeEvent
@scheduleModifiedEvents()
destroy: ->
throw new Error("Destroying buffer twice with path '#{@getPath()}'") if @destroyed
@file?.off()
@destroyed = true
project?.removeBuffer(this)
unless @destroyed
@file?.off()
@destroyed = true
@project?.removeBuffer(this)
retain: ->
@refcount++
@@ -73,12 +86,13 @@ class TextBuffer
this
serialize: ->
deserializer: 'TextBuffer'
path: @getPath()
text: @getText() if @isModified()
state = @state.clone()
state.set('isModified', @isModified())
for marker in state.get('text').getMarkers() when marker.isRemote()
marker.destroy()
state
@deserialize: ({path, text}) ->
project.bufferForPath(path, text)
getState: -> @state
subscribeToFile: ->
@file.on "contents-changed", =>
@@ -133,6 +147,15 @@ class TextBuffer
getPath: ->
@file?.getPath()
getUri: ->
@getRelativePath()
getRelativePath: ->
@state.get('relativePath')
setRelativePath: (relativePath) ->
@setPath(@project.resolve(relativePath))
# Sets the path for the file.
#
# path - A {String} representing the new file path
@@ -143,7 +166,7 @@ class TextBuffer
@file = new File(path)
@file.read() if @file.exists()
@subscribeToFile()
@state.set('relativePath', @project.relativize(path))
@trigger "path-changed", this
# Retrieves the current buffer's file extension.
@@ -171,7 +194,8 @@ class TextBuffer
#
# Returns a new {Range}, from `[0, 0]` to the end of the buffer.
getRange: ->
new Range([0, 0], [@getLastRow(), @getLastLine().length])
lastRow = @getLastRow()
new Range([0, 0], [lastRow, @lineLengthForRow(lastRow)])
# Given a range, returns the lines of text within it.
#
@@ -179,25 +203,13 @@ class TextBuffer
#
# Returns a {String} of the combined lines.
getTextInRange: (range) ->
range = @clipRange(range)
if range.start.row == range.end.row
return @lineForRow(range.start.row)[range.start.column...range.end.column]
multipleLines = []
multipleLines.push @lineForRow(range.start.row)[range.start.column..] # first line
multipleLines.push @lineEndingForRow(range.start.row)
for row in [range.start.row + 1...range.end.row]
multipleLines.push @lineForRow(row) # middle lines
multipleLines.push @lineEndingForRow(row)
multipleLines.push @lineForRow(range.end.row)[0...range.end.column] # last line
return multipleLines.join ''
@text.getTextInRange(@clipRange(range))
# Gets all the lines in a file.
#
# Returns an {Array} of {String}s.
getLines: ->
@lines
@text.getLines()
# Given a row, returns the line of text.
#
@@ -205,7 +217,7 @@ class TextBuffer
#
# Returns a {String}.
lineForRow: (row) ->
@lines[row]
@text.lineForRow(row)
# Given a row, returns its line ending.
#
@@ -213,10 +225,13 @@ class TextBuffer
#
# Returns a {String}, or `undefined` if `row` is the final row.
lineEndingForRow: (row) ->
@lineEndings[row] unless row is @getLastRow()
@text.lineEndingForRow(row)
suggestedLineEndingForRow: (row) ->
@lineEndingForRow(row) ? @lineEndingForRow(row - 1)
if row is @getLastRow()
@lineEndingForRow(row - 1)
else
@lineEndingForRow(row)
# Given a row, returns the length of the line of text.
#
@@ -224,7 +239,7 @@ class TextBuffer
#
# Returns a {Number}.
lineLengthForRow: (row) ->
@lines[row].length
@text.lineLengthForRow(row)
# Given a row, returns the length of the line ending
#
@@ -251,13 +266,13 @@ class TextBuffer
#
# Returns a {Number}.
getLineCount: ->
@getLines().length
@text.getLineCount()
# Gets the row number of the last line.
#
# Returns a {Number}.
getLastRow: ->
@getLines().length - 1
@getLineCount() - 1
# Finds the last line in the current buffer.
#
@@ -273,20 +288,10 @@ class TextBuffer
new Point(lastRow, @lineLengthForRow(lastRow))
characterIndexForPosition: (position) ->
position = @clipPosition(position)
index = 0
for row in [0...position.row]
index += @lineLengthForRow(row) + Math.max(@lineEndingLengthForRow(row), 1)
index + position.column
@text.indexForPoint(@clipPosition(position))
positionForCharacterIndex: (index) ->
row = 0
while index >= (lineLength = @lineLengthForRow(row) + Math.max(@lineEndingLengthForRow(row), 1))
index -= lineLength
row++
new Point(row, index)
@text.pointForIndex(index)
# Given a row, this deletes it from the buffer.
#
@@ -310,7 +315,6 @@ class TextBuffer
else
startPoint = [start, 0]
endPoint = [end + 1, 0]
@delete(new Range(startPoint, endPoint))
# Adds text to the end of the buffer.
@@ -321,10 +325,10 @@ class TextBuffer
# Adds text to a specific point in the buffer
#
# point - A {Point} in the buffer to insert into
# position - A {Point} in the buffer to insert into
# text - A {String} of text to add
insert: (point, text) ->
@change(new Range(point, point), text)
insert: (position, text) ->
@change(new Range(position, position), text)
# Deletes text from the buffer
#
@@ -340,15 +344,7 @@ class TextBuffer
#
# Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed.
clipPosition: (position) ->
position = Point.fromObject(position)
eofPosition = @getEofPosition()
if position.isGreaterThan(eofPosition)
eofPosition
else
row = Math.max(position.row, 0)
column = Math.max(position.column, 0)
column = Math.min(@lineLengthForRow(row), column)
new Point(row, column)
@text.clipPosition(position)
# Given a range, this clips it to a real range.
#
@@ -363,19 +359,11 @@ class TextBuffer
range = Range.fromObject(range)
new Range(@clipPosition(range.start), @clipPosition(range.end))
prefixAndSuffixForRange: (range) ->
prefix: @lines[range.start.row][0...range.start.column]
suffix: @lines[range.end.row][range.end.column..]
undo: ->
@text.undo()
# Undos the last operation.
#
# editSession - The {EditSession} associated with the buffer.
undo: (editSession) -> @undoManager.undo(editSession)
# Redos the last operation.
#
# editSession - The {EditSession} associated with the buffer.
redo: (editSession) -> @undoManager.redo(editSession)
redo: ->
@text.redo()
# Saves the buffer.
save: ->
@@ -411,25 +399,24 @@ class TextBuffer
# Identifies if a buffer is empty.
#
# Returns a {Boolean}.
isEmpty: -> @lines.length is 1 and @lines[0].length is 0
isEmpty: -> @text.isEmpty()
# Returns all valid {BufferMarker}s on the buffer.
getMarkers: ({includeInvalid} = {}) ->
markers = _.values(@validMarkers)
if includeInvalid
markers.concat(_.values(@invalidMarkers))
else
markers
getMarkers: ->
@text.getMarkers()
# Returns the {BufferMarker} with the given id.
getMarker: (id) ->
@validMarkers[id]
@text.getMarker(id)
destroyMarker: (id) ->
@getMarker(id)?.destroy()
# Public: Finds the first marker satisfying the given attributes
#
# Returns a {String} marker-identifier
findMarker: (attributes) ->
@findMarkers(attributes)[0]
@text.findMarker(attributes)
# Public: Finds all markers satisfying the given attributes
#
@@ -440,14 +427,13 @@ class TextBuffer
#
# Returns an {Array} of {BufferMarker}s
findMarkers: (attributes) ->
markers = @getMarkers().filter (marker) -> marker.matchesAttributes(attributes)
markers.sort (a, b) -> a.getRange().compare(b.getRange())
@text.findMarkers(attributes)
# Retrieves the quantity of markers in a buffer.
#
# Returns a {Number}.
getMarkerCount: ->
_.size(@validMarkers)
@text.getMarkers().length
# Constructs a new marker at a given range.
#
@@ -456,23 +442,12 @@ class TextBuffer
# Any attributes you pass will be associated with the marker and can be retrieved
# or used in marker queries.
# The following attribute keys reserved, and control the marker's initial range
# reverse - if `true`, the marker is reversed; that is, its head precedes the tail
# noTail - if `true`, the marker is created without a tail
# isReversed - if `true`, the marker is reversed; that is, its head precedes the tail
# hasTail - if `false`, the marker is created without a tail
#
# Returns a {Number} representing the new marker's ID.
markRange: (range, attributes={}) ->
optionKeys = ['invalidationStrategy', 'noTail', 'reverse']
options = _.pick(attributes, optionKeys)
attributes = _.omit(attributes, optionKeys)
marker = new BufferMarker(_.defaults({
id: (@nextMarkerId++).toString()
buffer: this
range
attributes
}, options))
@validMarkers[marker.id] = marker
@trigger 'marker-created', marker
marker
markRange: (range, options={}) ->
@text.markRange(range, options)
# Constructs a new marker at a given position.
#
@@ -481,16 +456,7 @@ class TextBuffer
#
# Returns a {Number} representing the new marker's ID.
markPosition: (position, options) ->
@markRange([position, position], _.defaults({noTail: true}, options))
# Given a buffer position, this finds all markers that contain the position.
#
# bufferPosition - A {Point} to check
#
# Returns an {Array} of {Numbers}, representing marker IDs containing `bufferPosition`.
markersForPosition: (position) ->
position = Point.fromObject(position)
@getMarkers().filter (marker) -> marker.containsPoint(position)
@text.markPosition(position, options)
# Identifies if a character sequence is within a certain range.
#
@@ -628,7 +594,7 @@ class TextBuffer
checkoutHead: ->
path = @getPath()
return unless path
git?.checkoutHead(path)
@project.getRepo()?.checkoutHead(path)
# Checks to see if a file exists.
#
@@ -638,37 +604,24 @@ class TextBuffer
### Internal ###
pushOperation: (operation, editSession) ->
if @undoManager
@undoManager.pushOperation(operation, editSession)
transact: (fn) -> @text.transact fn
beginTransaction: -> @text.beginTransaction()
commitTransaction: -> @text.commitTransaction()
abortTransaction: -> @text.abortTransaction()
change: (oldRange, newText, options={}) ->
oldRange = @clipRange(oldRange)
newText = @normalizeLineEndings(oldRange.start.row, newText) if options.normalizeLineEndings ? true
@text.change(oldRange, newText, options)
normalizeLineEndings: (startRow, text) ->
if lineEnding = @suggestedLineEndingForRow(startRow)
text.replace(/\r?\n/g, lineEnding)
else
operation.do()
transact: (fn) ->
if isNewTransaction = @undoManager.transact()
@pushOperation(new BufferChangeOperation(buffer: this)) # restores markers on undo
if fn
try
fn()
finally
@commit() if isNewTransaction
commit: ->
@pushOperation(new BufferChangeOperation(buffer: this)) # restores markers on redo
@undoManager.commit()
abort: -> @undoManager.abort()
change: (oldRange, newText, options) ->
oldRange = Range.fromObject(oldRange)
operation = new BufferChangeOperation({buffer: this, oldRange, newText, options})
range = @pushOperation(operation)
range
destroyMarker: (id) ->
if marker = @validMarkers[id] ? @invalidMarkers[id]
delete @validMarkers[id]
delete @invalidMarkers[id]
text
scheduleModifiedEvents: ->
clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout

View File

@@ -3,18 +3,15 @@ TokenizedLine = require 'tokenized-line'
EventEmitter = require 'event-emitter'
Subscriber = require 'subscriber'
Token = require 'token'
Range = require 'range'
Point = require 'point'
telepath = require 'telepath'
{Point, Range} = telepath
### Internal ###
module.exports =
class TokenizedBuffer
@idCounter: 1
grammar: null
currentGrammarScore: null
tabLength: null
buffer: null
aceAdaptor: null
tokenizedLines: null
@@ -22,9 +19,22 @@ class TokenizedBuffer
invalidRows: null
visible: false
constructor: (@buffer, { @tabLength } = {}) ->
@tabLength ?= 2
@id = @constructor.idCounter++
@acceptsDocuments: true
registerDeserializer(this)
@deserialize: (state) ->
new this(state)
constructor: (optionsOrState) ->
if optionsOrState instanceof telepath.Document
@state = optionsOrState
@buffer = project.bufferForId(optionsOrState.get('bufferId'))
else
{ @buffer, tabLength } = optionsOrState
@state = site.createDocument
deserializer: @constructor.name
bufferId: @buffer.id
tabLength: tabLength ? 2
@subscribe syntax, 'grammar-added grammar-updated', (grammar) =>
if grammar.injectionSelector?
@@ -34,10 +44,13 @@ class TokenizedBuffer
@setGrammar(grammar, newScore) if newScore > @currentGrammarScore
@on 'grammar-changed grammar-updated', => @resetTokenizedLines()
@subscribe @buffer, "changed.tokenized-buffer#{@id}", (e) => @handleBufferChange(e)
@subscribe @buffer, "changed", (e) => @handleBufferChange(e)
@reloadGrammar()
serialize: -> @state.clone()
getState: -> @state
setGrammar: (grammar, score) ->
return if grammar is @grammar
@unsubscribe(@grammar) if @grammar
@@ -71,12 +84,13 @@ class TokenizedBuffer
#
# Returns a {Number}.
getTabLength: ->
@tabLength
@state.get('tabLength')
# Specifies the tab length.
#
# tabLength - A {Number} that defines the new tab length.
setTabLength: (@tabLength) ->
setTabLength: (tabLength) ->
@state.set('tabLength', tabLength)
lastRow = @buffer.getLastRow()
@tokenizedLines = @buildPlaceholderTokenizedLinesForRows(0, lastRow)
@invalidateRow(0)
@@ -175,13 +189,15 @@ class TokenizedBuffer
buildPlaceholderTokenizedLineForRow: (row) ->
line = @buffer.lineForRow(row)
tokens = [new Token(value: line, scopes: [@grammar.scopeName])]
new TokenizedLine({tokens, @tabLength})
tabLength = @getTabLength()
new TokenizedLine({tokens, tabLength})
buildTokenizedTokenizedLineForRow: (row, ruleStack) ->
line = @buffer.lineForRow(row)
lineEnding = @buffer.lineEndingForRow(row)
tabLength = @getTabLength()
{ tokens, ruleStack } = @grammar.tokenizeLine(line, ruleStack, row is 0)
new TokenizedLine({tokens, ruleStack, @tabLength, lineEnding})
new TokenizedLine({tokens, ruleStack, tabLength, lineEnding})
# FIXME: benogle says: These are actually buffer rows as all buffer rows are
# accounted for in @tokenizedLines
@@ -231,7 +247,6 @@ class TokenizedBuffer
destroy: ->
@unsubscribe()
@buffer.off ".tokenized-buffer#{@id}"
iterateTokensInBufferRange: (bufferRange, iterator) ->
bufferRange = Range.fromObject(bufferRange)

View File

@@ -1,76 +0,0 @@
_ = require 'underscore'
# Internal: The object in charge of managing redo and undo operations.
module.exports =
class UndoManager
undoHistory: null
redoHistory: null
currentTransaction: null
constructor: ->
@clear()
clear: ->
@currentTransaction = [] if @currentTransaction?
@undoHistory = []
@redoHistory = []
pushOperation: (operation, editSession) ->
if @currentTransaction
@currentTransaction.push(operation)
else
@undoHistory.push([operation])
@redoHistory = []
try
operation.do?(editSession)
catch e
@clear()
throw e
transact: ->
isNewTransaction = not @currentTransaction?
@currentTransaction ?= []
isNewTransaction
commit: ->
unless @currentTransaction?
throw new Error("Trying to commit when there is no current transaction")
empty = @currentTransaction.length is 0
@undoHistory.push(@currentTransaction) unless empty
@currentTransaction = null
not empty
abort: ->
unless @currentTransaction?
throw new Error("Trying to abort when there is no current transaction")
if @commit()
@undo()
@redoHistory.pop()
undo: (editSession) ->
try
if batch = @undoHistory.pop()
opsInReverse = new Array(batch...)
opsInReverse.reverse()
op.undo?(editSession) for op in opsInReverse
@redoHistory.push batch
batch.oldSelectionRanges
catch e
@clear()
throw e
redo: (editSession) ->
try
if batch = @redoHistory.pop()
for op in batch
op.do?(editSession)
op.redo?(editSession)
@undoHistory.push(batch)
batch.newSelectionRanges
catch e
@clear()
throw e

View File

@@ -20,6 +20,7 @@ windowEventHandler = null
# This method is called in any window needing a general environment, including specs
window.setUpEnvironment = (windowMode) ->
window.site = new telepath.Site(1)
atom.windowMode = windowMode
window.resourcePath = remote.getCurrentWindow().loadSettings.resourcePath
@@ -63,6 +64,7 @@ window.startEditorWindow = ->
window.unloadEditorWindow = ->
return if not project and not rootView
windowState = atom.getWindowState()
windowState.set('project', project.serialize())
windowState.set('syntax', syntax.serialize())
windowState.set('rootView', rootView.serialize())
atom.deactivatePackages()
@@ -70,11 +72,9 @@ window.unloadEditorWindow = ->
atom.saveWindowState()
rootView.remove()
project.destroy()
git?.destroy()
windowEventHandler?.unsubscribe()
window.rootView = null
window.project = null
window.git = null
window.installAtomCommand = (callback) ->
commandPath = path.join(window.resourcePath, 'atom.sh')
@@ -93,32 +93,28 @@ window.onDrop = (e) ->
window.deserializeEditorWindow = ->
RootView = require 'root-view'
Project = require 'project'
Git = require 'git'
windowState = atom.getWindowState()
atom.packageStates = windowState.getObject('packageStates') ? {}
windowState.remove('packageStates')
window.project = deserialize(windowState.get('project'))
unless window.project?
window.project = new Project(atom.getLoadSettings().initialPath)
windowState.set('project', window.project.serialize())
windowState.set('project', window.project.getState())
window.rootView = deserialize(windowState.get('rootView'))
unless window.rootView?
window.rootView = new RootView()
windowState.set('rootView', window.rootView.serialize())
windowState.set('rootView', window.rootView.getState())
$(rootViewParentSelector).append(rootView)
window.git = Git.open(project.getPath())
project.on 'path-changed', ->
projectPath = project.getPath()
atom.getLoadSettings().initialPath = projectPath
window.git?.destroy()
window.git = Git.open(projectPath)
window.stylesheetElementForId = (id) ->
$("""head style[id="#{id}"]""")
@@ -206,14 +202,14 @@ window.registerDeferredDeserializer = (name, fn) ->
window.unregisterDeserializer = (klass) ->
delete deserializers[klass.name]
window.deserialize = (state) ->
window.deserialize = (state, params) ->
return unless state?
if deserializer = getDeserializer(state)
stateVersion = state.get?('version') ? state.version
return if deserializer.version? and deserializer.version isnt stateVersion
if (state instanceof telepath.Document) and not deserializer.acceptsDocuments
state = state.toObject()
deserializer.deserialize(state)
deserializer.deserialize(state, params)
else
console.warn "No deserializer found for", state

View File

@@ -8,6 +8,7 @@ dialog = require 'dialog'
fs = require 'fs'
path = require 'path'
net = require 'net'
url = require 'url'
socketPath = '/tmp/atom.sock'
@@ -37,7 +38,7 @@ class AtomApplication
installUpdate: null
version: null
constructor: ({@resourcePath, pathsToOpen, @version, test, pidToKillWhenClosed, @devMode, newWindow}) ->
constructor: ({@resourcePath, pathsToOpen, urlsToOpen, @version, test, pidToKillWhenClosed, @devMode, newWindow}) ->
global.atomApplication = this
@pidsToOpenWindows = {}
@@ -55,6 +56,8 @@ class AtomApplication
@runSpecs({exitWhenDone: true, @resourcePath})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, @devMode})
else if urlsToOpen.length > 0
@openUrl(urlToOpen) for urlToOpen in urlsToOpen
else
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow, @devMode})
@@ -175,6 +178,10 @@ class AtomApplication
event.preventDefault()
@openPath({pathToOpen})
app.on 'open-url', (event, urlToOpen) =>
event.preventDefault()
@openUrl(urlToOpen)
autoUpdater.on 'ready-for-update-on-quit', (event, version, quitAndUpdate) =>
event.preventDefault()
@installUpdate = quitAndUpdate
@@ -241,6 +248,17 @@ class AtomApplication
console.log("Killing process #{pid} failed: #{error.code}")
delete @pidsToOpenWindows[pid]
openUrl: (urlToOpen) ->
parsedUrl = url.parse(urlToOpen)
if parsedUrl.host is 'session'
sessionId = parsedUrl.path.split('/')[1]
console.log "Joining session #{sessionId}"
if sessionId
bootstrapScript = 'collaboration/lib/bootstrap'
new AtomWindow({bootstrapScript, @resourcePath, sessionId, @devMode})
else
console.log "Opening unknown url #{urlToOpen}"
runSpecs: ({exitWhenDone, resourcePath}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath

View File

@@ -18,18 +18,21 @@ require 'coffee-script'
delegate.browserMainParts.preMainMessageLoopRun = ->
args = parseCommandLine()
addPathToOpen = (event, filePath) ->
addPathToOpen = (event, pathToOpen) ->
event.preventDefault()
args.pathsToOpen.push(filePath)
args.pathsToOpen.push(pathToOpen)
app.on 'open-url', (event, url) =>
args.urlsToOpen = []
addUrlToOpen = (event, urlToOpen) ->
event.preventDefault()
dialog.showMessageBox
message: 'Atom opened with URL'
detail: url
buttons: ['OK']
args.urlsToOpen.push(urlToOpen)
app.on 'open-url', (event, urlToOpen) ->
event.preventDefault()
args.urlsToOpen.push(urlToOpen)
app.on 'open-file', addPathToOpen
app.on 'open-url', addUrlToOpen
app.on 'will-finish-launching', ->
setupCrashReporter()
@@ -37,6 +40,7 @@ delegate.browserMainParts.preMainMessageLoopRun = ->
app.on 'finish-launching', ->
app.removeListener 'open-file', addPathToOpen
app.removeListener 'open-url', addUrlToOpen
args.pathsToOpen = args.pathsToOpen.map (pathToOpen) ->
path.resolve(args.executedFrom ? process.cwd(), pathToOpen)