Organize TextBuffer into sections

Organize TextBuffer into sections

--- src/text-buffer.coffee
+++ src/text-buffer.coffee
@@ -38,9 +38,16 @@
   refcount: 0
   fileSubscriptions: null

+  @delegatesMethods 'undo', 'redo', 'transact', 'beginTransaction', 'commitTransaction',
+    'abortTransaction', 'isTransacting', 'clearUndoStack', toProperty: 'history'
+
+  ###
+  Section: Construction
+  ###
+
   # Public: Create a new buffer with the given params.
   #
-  # * `params` A {String} of text or an {Object} with the following keys:
+  # * `params` {Object} or {String} of text
   #   * `load` A {Boolean}, `true` to asynchronously load the buffer from disk
   #     after initialization.
   #   * `text` The initial {String} text of the buffer.
@@ -78,6 +85,10 @@
     filePath: @getPath()
     modifiedWhenLastPersisted: @isModified()
     digestWhenLastPersisted: @file?.getDigest()
+
+  ###
+  Section: Event Subscription
+  ###

   # Public: Invoke the given callback synchronously when the content of the
   # buffer changes.
@@ -243,6 +254,88 @@

     EmitterMixin::on.apply(this, arguments)

+  ###
+  Section: Metadata
+  ###
+
+  # Public: Determine whether the buffer is empty.
+  #
+  # Returns a {Boolean}.
+  isEmpty: ->
+    @getLastRow() is 0 and @lineLengthForRow(0) is 0
+
+  # Public: Determine if the in-memory contents of the buffer differ from its
+  # contents on disk.
+  #
+  # If the buffer is unsaved, always returns `true` unless the buffer is empty.
+  #
+  # Returns a {Boolean}.
+  isModified: ->
+    return false unless @loaded
+    if @file
+      if @file.exists()
+        @getText() != @cachedDiskContents
+      else
+        @wasModifiedBeforeRemove ? not @isEmpty()
+    else
+      not @isEmpty()
+
+  # Public: Determine if the in-memory contents of the buffer conflict with the
+  # on-disk contents of its associated file.
+  #
+  # Returns a {Boolean}.
+  isInConflict: -> @conflict
+
+  # Public: Get the number of milliseconds that will elapse without a change
+  # before {::onDidStopChanging} observers are invoked following a change.
+  #
+  # Returns a {Number}.
+  getStoppedChangingDelay: -> @stoppedChangingDelay
+
+  ###
+  Section: Path Data
+  ###
+
+  # Pubilc: Get the path of the associated file.
+  #
+  # Returns a {String}.
+  getPath: ->
+    @file?.getPath()
+
+  # Public: Set the path for the buffer's associated file.
+  #
+  # * `filePath` A {String} representing the new file path
+  setPath: (filePath) ->
+    return if filePath == @getPath()
+
+    if filePath
+      @file = new File(filePath)
+      @subscribeToFile()
+    else
+      @file = null
+
+    @emit "path-changed", this
+    @emitter.emit 'did-change-path', @getPath()
+
+  # Public: Get the path of the associated file.
+  #
+  # Returns a {String}.
+  getUri: ->
+    @getPath()
+
+  # Get the basename of the associated file.
+  #
+  # The basename is the name portion of the file's path, without the containing
+  # directories.
+  #
+  # Returns a {String}.
+  getBaseName: ->
+    @file?.getBaseName()
+
+  ###
+  Section: Reading Text
+  ###
+
   # Public: Get the entire text of the buffer.
   #
   # Returns a {String}.
@@ -254,281 +347,6 @@
       for row in [0..@getLastRow()]
         text += (@lineForRow(row) + @lineEndingForRow(row))
       @cachedText = text
-
-  # Public: Get the text of all lines in the buffer, without their line endings.
-  #
-  # Returns an {Array} of {String}s.
-  getLines: ->
-    @lines.slice()
-
-  # Public: Get the number of milliseconds that will elapse without a change
-  # before {::onDidStopChanging} observers are invoked following a change.
-  #
-  # Returns a {Number}.
-  getStoppedChangingDelay: -> @stoppedChangingDelay
-
-  # Public: Determine whether the buffer is empty.
-  #
-  # Returns a {Boolean}.
-  isEmpty: ->
-    @getLastRow() is 0 and @lineLengthForRow(0) is 0
-
-  # Public: Get the number of lines in the buffer.
-  #
-  # Returns a {Number}.
-  getLineCount: ->
-    @lines.length
-
-  # Public: Get the last 0-indexed row in the buffer.
-  #
-  # Returns a {Number}.
-  getLastRow: ->
-    @getLineCount() - 1
-
-  # Public: Get the text of the line at the given row, without its line ending.
-  #
-  # * `row` A {Number} representing a 0-indexed row.
-  #
-  # Returns a {String}.
-  lineForRow: (row) ->
-    @lines[row]
-
-  # Public: Get the text of the last line of the buffer, without its line
-  # ending.
-  #
-  # Returns a {String}.
-  getLastLine: ->
-    @lineForRow(@getLastRow())
-
-  # Public: Get the line ending for the given 0-indexed row.
-  #
-  # * `row` A {Number} indicating the row.
-  #
-  # Returns a {String}. The returned newline is represented as a literal string:
-  # `'\n'`, `'\r'`, `'\r\n'`, or `''` for the last line of the buffer, which
-  # doesn't end in a newline.
-  lineEndingForRow: (row) ->
-    @lineEndings[row]
-
-  # Public: Get the length of the line for the given 0-indexed row, without its
-  # line ending.
-  #
-  # * `row` A {Number} indicating the row.
-  #
-  # Returns a {Number}.
-  lineLengthForRow: (row) ->
-    @lines[row].length
-
-  # Public: Replace the entire contents of the buffer with the given text.
-  #
-  # * `text` A {String}
-  #
-  # Returns a {Range} spanning the new buffer contents.
-  setText: (text) ->
-    @setTextInRange(@getRange(), text, false)
-
-  # Public: Replace the current buffer contents by applying a diff based on the
-  # given text.
-  #
-  # * `text` A {String} containing the new buffer contents.
-  setTextViaDiff: (text) ->
-    currentText = @getText()
-    return if currentText == text
-
-    endsWithNewline = (str) ->
-      /[\r\n]+$/g.test(str)
-
-    computeBufferColumn = (str) ->
-      newlineIndex = Math.max(str.lastIndexOf('\n'), str.lastIndexOf('\r'))
-      if endsWithNewline(str)
-        0
-      else if newlineIndex == -1
-        str.length
-      else
-        str.length - newlineIndex - 1
-
-    @transact =>
-      row = 0
-      column = 0
-      currentPosition = [0, 0]
-
-      lineDiff = diff.diffLines(currentText, text)
-      changeOptions = normalizeLineEndings: false
-
-      for change in lineDiff
-        lineCount = change.value.match(newlineRegex)?.length ? 0
-        currentPosition[0] = row
-        currentPosition[1] = column
-
-        if change.added
-          @setTextInRange([currentPosition, currentPosition], change.value, changeOptions)
-          row += lineCount
-          column = computeBufferColumn(change.value)
-
-        else if change.removed
-          endRow = row + lineCount
-          endColumn = column + computeBufferColumn(change.value)
-          @setTextInRange([currentPosition, [endRow, endColumn]], '', changeOptions)
-
-        else
-          row += lineCount
-          column = computeBufferColumn(change.value)
-
-  # Public: Set the text in the given range.
-  #
-  # * `range` A {Range}
-  # * `text` A {String}
-  # * `normalizeLineEndings` (optional) {Boolean} (default: true)
-  #
-  # Returns the {Range} of the inserted text.
-  setTextInRange: (range, text, normalizeLineEndings=true) ->
-    patch = @buildPatch(range, text, normalizeLineEndings)
-    @history?.recordNewPatch(patch)
-    @applyPatch(patch)
-    patch.newRange
-
-  # Public: Insert text at the given position.
-  #
-  # * `position` A {Point} representing the insertion location. The position is
-  #   clipped before insertion.
-  # * `text` A {String} representing the text to insert.
-  # * `normalizeLineEndings` (optional) {Boolean} (default: true)\
-  #
-  # Returns the {Range} of the inserted text.
-  insert: (position, text, normalizeLineEndings) ->
-    @setTextInRange(new Range(position, position), text, normalizeLineEndings)
-
-  # Public: Append text to the end of the buffer.
-  #
-  # * `text` A {String} representing the text text to append.
-  #
-  # Returns the {Range} of the inserted text
-  append: (text, normalizeLineEndings) ->
-    @insert(@getEndPosition(), text, normalizeLineEndings)
-
-  # Public: Delete the text in the given range.
-  #
-  # * `range` A {Range} in which to delete. The range is clipped before deleting.
-  #
-  # Returns an empty {Range} starting at the start of deleted range.
-  delete: (range) ->
-    @setTextInRange(range, '')
-
-  # Public: Delete the line associated with a specified row.
-  #
-  # * `row` A {Number} representing the 0-indexed row to delete.
-  #
-  # Returns the {Range} of the deleted text.
-  deleteRow: (row) ->
-    @deleteRows(row, row)
-
-  # Public: Delete the lines associated with the specified row range.
-  #
-  # If the row range is out of bounds, it will be clipped. If the startRow is
-  # greater than the end row, they will be reordered.
-  #
-  # * `startRow` A {Number} representing the first row to delete.
-  # * `endRow` A {Number} representing the last row to delete, inclusive.
-  #
-  # Returns the {Range} of the deleted text.
-  deleteRows: (startRow, endRow) ->
-    lastRow = @getLastRow()
-
-    [startRow, endRow] = [endRow, startRow] if startRow > endRow
-
-    if endRow < 0
-      return new Range(@getFirstPosition(), @getFirstPosition())
-
-    if startRow > lastRow
-      return new Range(@getEndPosition(), @getEndPosition())
-
-    startRow = Math.max(0, startRow)
-    endRow = Math.min(lastRow, endRow)
-
-    if endRow < lastRow
-      startPoint = new Point(startRow, 0)
-      endPoint = new Point(endRow + 1, 0)
-    else
-      if startRow is 0
-        startPoint = new Point(startRow, 0)
-      else
-        startPoint = new Point(startRow - 1, @lineLengthForRow(startRow - 1))
-      endPoint = new Point(endRow, @lineLengthForRow(endRow))
-
-    @delete(new Range(startPoint, endPoint))
-
-  # Builds a {BufferPatch}, which is used to modify the buffer and is also
-  # pushed into the undo history so it can be undone.
-  buildPatch: (oldRange, newText, normalizeLineEndings) ->
-    oldRange = @clipRange(oldRange)
-    oldText = @getTextInRange(oldRange)
-    newRange = Range.fromText(oldRange.start, newText)
-    patch = new BufferPatch(oldRange, newRange, oldText, newText, normalizeLineEndings)
-    @markers?.handleBufferChange(patch)
-    patch
-
-  # Applies a {BufferPatch} to the buffer based on its old range and new text.
-  # Also applies any {MarkerPatch}es associated with the {BufferPatch}.
-  applyPatch: ({oldRange, newRange, oldText, newText, normalizeLineEndings, markerPatches}) ->
-    @cachedText = null
-
-    startRow = oldRange.start.row
-    endRow = oldRange.end.row
-    rowCount = endRow - startRow + 1
-
-    # Determine how to normalize the line endings of inserted text if enabled
-    if normalizeLineEndings
-      normalizedEnding = @lineEndingForRow(startRow)
-      if normalizedEnding is ''
-        if startRow > 0
-          normalizedEnding = @lineEndingForRow(startRow - 1)
-        else
-          normalizedEnding = null
-
-    # Split inserted text into lines and line endings
-    lines = []
-    lineEndings = []
-    lineStartIndex = 0
-    while result = newlineRegex.exec(newText)
-      lines.push(newText[lineStartIndex...result.index])
-      lineEndings.push(normalizedEnding ? result[0])
-      lineStartIndex = newlineRegex.lastIndex
-
-    lastLine = newText[lineStartIndex..]
-    lines.push(lastLine)
-    lineEndings.push('')
-
-    # Update first and last line so replacement preserves existing prefix and suffix of oldRange
-    prefix = @lineForRow(startRow)[0...oldRange.start.column]
-    lines[0] = prefix + lines[0]
-    suffix = @lineForRow(endRow)[oldRange.end.column...]
-    lastIndex = lines.length - 1
-    lines[lastIndex] += suffix
-    lastLineEnding = @lineEndingForRow(endRow)
-    lastLineEnding = normalizedEnding if lastLineEnding isnt '' and normalizedEnding?
-    lineEndings[lastIndex] = lastLineEnding
-
-    # Replace lines in oldRange with new lines
-    spliceArray(@lines, startRow, rowCount, lines)
-    spliceArray(@lineEndings, startRow, rowCount, lineEndings)
-
-    # Update the offset index for position <-> character offset translation
-    offsets = lines.map (line, index) ->
-      {rows: 1, characters: line.length + lineEndings[index].length}
-    @offsetIndex.spliceArray('rows', startRow, rowCount, offsets)
-
-    @markers?.pauseChangeEvents()
-    @markers?.applyPatches(markerPatches, true)
-
-    changeEvent = {oldRange, newRange, oldText, newText}
-
-    @conflict = false if @conflict and !@isModified()
-    @scheduleModifiedEvents()
-    @emit 'changed', changeEvent
-    @emitter.emit 'did-change', changeEvent
-    @markers?.resumeChangeEvents()
-    @emit 'markers-updated'
-    @emitter.emit 'did-update-markers'

   # Public: Get the text in a range.
   #
@@ -556,463 +374,45 @@
         text += @lineEndingForRow(row)
       text

-  # Public: Clip the given range so it starts and ends at valid positions.
-  #
-  # For example, the position `[1, 100]` is out of bounds if the line at row 1 is
-  # only 10 characters long, and it would be clipped to `(1, 10)`.
-  #
-  # * `range` A {Range} or range-compatible {Array} to clip.
-  #
-  # Returns the given {Range} if it is already in bounds, or a new clipped
-  # {Range} if the given range is out-of-bounds.
-  clipRange: (range) ->
-    range = Range.fromObject(range)
-    start = @clipPosition(range.start)
-    end = @clipPosition(range.end)
-    if range.start.isEqual(start) and range.end.isEqual(end)
-      range
-    else
-      new Range(start, end)
-
-  # Public: Clip the given point so it is at a valid position in the buffer.
-  #
-  # For example, the position (1, 100) is out of bounds if the line at row 1 is
-  # only 10 characters long, and it would be clipped to (1, 10)
-  #
-  # * `position` A {Point} or point-compatible {Array}.
-  #
-  # Returns a new {Point} if the given position is invalid, otherwise returns
-  # the given position.
-  clipPosition: (position) ->
-    position = Point.fromObject(position)
-    {row, column} = position
-    if row < 0
-      @getFirstPosition()
-    else if row > @getLastRow()
-      @getEndPosition()
-    else
-      column = Math.min(Math.max(column, 0), @lineLengthForRow(row))
-      if column is position.column
-        position
-      else
-        new Point(row, column)
-
-  # Public: Get the first position in the buffer, which is always `[0, 0]`.
-  #
-  # Returns a {Point}.
-  getFirstPosition: ->
-    new Point(0, 0)
-
-  # Public: Get the maximal position in the buffer, where new text would be
-  # appended.
-  #
-  # Returns a {Point}.
-  getEndPosition: ->
-    lastRow = @getLastRow()
-    new Point(lastRow, @lineLengthForRow(lastRow))
-
-  # Public: Get the range spanning from `[0, 0]` to {::getEndPosition}.
-  #
-  # Returns a {Range}.
-  getRange: ->
-    new Range(@getFirstPosition(), @getEndPosition())
-
-  # Public: Get the range for the given row
+  # Public: Get the text of all lines in the buffer, without their line endings.
+  #
+  # Returns an {Array} of {String}s.
+  getLines: ->
+    @lines.slice()
+
+  # Public: Get the text of the last line of the buffer, without its line
+  # ending.
+  #
+  # Returns a {String}.
+  getLastLine: ->
+    @lineForRow(@getLastRow())
+
+  # Public: Get the text of the line at the given row, without its line ending.
   #
   # * `row` A {Number} representing a 0-indexed row.
-  # * `includeNewline` A {Boolean} indicating whether or not to include the
-  #   newline, which results in a range that extends to the start
-  #   of the next line.
-  #
-  # Returns a {Range}.
-  rangeForRow: (row, includeNewline) ->
-    # Handle deprecated options hash
-    if typeof includeNewline is 'object'
-      Grim.deprecate("The second param is no longer an object, it's a boolean argument named `includeNewline`.")
-      {includeNewline} = includeNewline
-
-    if includeNewline and row < @getLastRow()
-      new Range(new Point(row, 0), new Point(row + 1, 0))
-    else
-      new Range(new Point(row, 0), new Point(row, @lineLengthForRow(row)))
-
-  # Public: Convert a position in the buffer in row/column coordinates to an
-  # absolute character offset, inclusive of line ending characters.
-  #
-  # The position is clipped prior to translating.
-  #
-  # * `position` A {Point}.
+  #
+  # Returns a {String}.
+  lineForRow: (row) ->
+    @lines[row]
+
+  # Public: Get the line ending for the given 0-indexed row.
+  #
+  # * `row` A {Number} indicating the row.
+  #
+  # Returns a {String}. The returned newline is represented as a literal string:
+  # `'\n'`, `'\r'`, `'\r\n'`, or `''` for the last line of the buffer, which
+  # doesn't end in a newline.
+  lineEndingForRow: (row) ->
+    @lineEndings[row]
+
+  # Public: Get the length of the line for the given 0-indexed row, without its
+  # line ending.
+  #
+  # * `row` A {Number} indicating the row.
   #
   # Returns a {Number}.
-  characterIndexForPosition: (position) ->
-    {row, column} = @clipPosition(Point.fromObject(position))
-
-    if row < 0 or row > @getLastRow() or column < 0 or column > @lineLengthForRow(row)
-      throw new Error("Position #{position} is invalid")
-
-    {characters} = @offsetIndex.totalTo(row, 'rows')
-    characters + column
-
-  # Public: Convert an absolute character offset, inclusive of newlines, to a
-  # position in the buffer in row/column coordinates.
-  #
-  # The offset is clipped prior to translating.
-  #
-  # * `offset` A {Number}.
-  #
-  # Returns a {Point}.
-  positionForCharacterIndex: (offset) ->
-    offset = Math.max(0, offset)
-    offset = Math.min(@getMaxCharacterIndex(), offset)
-
-    {rows, characters} = @offsetIndex.totalTo(offset, 'characters')
-    if rows > @getLastRow()
-      @getEndPosition()
-    else
-      new Point(rows, offset - characters)
-
-  # Public: Get the length of the buffer in characters.
-  #
-  # Returns a {Number}.
-  getMaxCharacterIndex: ->
-    @offsetIndex.totalTo(Infinity, 'rows').characters
-
-  loadSync: ->
-    @updateCachedDiskContentsSync()
-    @finishLoading()
-
-  load: ->
-    @updateCachedDiskContents().then => @finishLoading()
-
-  finishLoading: ->
-    if @isAlive()
-      @loaded = true
-      if @useSerializedText and @digestWhenLastPersisted is @file?.getDigest()
-        @emitModifiedStatusChanged(true)
-      else
-        @reload()
-      @clearUndoStack()
-    this
-
-  destroy: ->
-    unless @destroyed
-      @cancelStoppedChangingTimeout()
-      @fileSubscriptions?.dispose()
-      @unsubscribe()
-      @destroyed = true
-      @emit 'destroyed'
-      @emitter.emit 'did-destroy'
-
-  isAlive: -> not @destroyed
-
-  isDestroyed: -> @destroyed
-
-  isRetained: -> @refcount > 0
-
-  retain: ->
-    @refcount++
-    this
-
-  release: ->
-    @refcount--
-    @destroy() unless @isRetained()
-    this
-
-  subscribeToFile: ->
-    @fileSubscriptions?.dispose()
-    @fileSubscriptions = new CompositeDisposable
-
-    @fileSubscriptions.add @file.onDidChange =>
-      @conflict = true if @isModified()
-      previousContents = @cachedDiskContents
-
-      # Synchrounously update the disk contents because the {File} has already cached them. If the
-      # contents updated asynchrounously multiple `conlict` events could trigger for the same disk
-      # contents.
-      @updateCachedDiskContentsSync()
-      return if previousContents == @cachedDiskContents
-
-      if @conflict
-        @emit "contents-conflicted"
-        @emitter.emit 'did-conflict'
-      else
-        @reload()
-
-    @fileSubscriptions.add @file.onDidDelete =>
-      modified = @getText() != @cachedDiskContents
-      @wasModifiedBeforeRemove = modified
-      if modified
-        @updateCachedDiskContents()
-      else
-        @destroy()
-
-    @fileSubscriptions.add @file.onDidRename =>
-      @emit "path-changed", this
-      @emitter.emit 'did-change-path', @getPath()
-
-  # Identifies if the buffer belongs to multiple editors.
-  #
-  # For example, if the {EditorView} was split.
-  #
-  # Returns a {Boolean}.
-  hasMultipleEditors: -> @refcount > 1
-
-  # Public: Reload the buffer's contents from disk.
-  #
-  # Sets the buffer's content to the cached disk contents
-  reload: ->
-    @emit 'will-reload'
-    @emitter.emit 'will-reload'
-    @setTextViaDiff(@cachedDiskContents)
-    @emitModifiedStatusChanged(false)
-    @emit 'reloaded'
-    @emitter.emit 'did-reload'
-
-  # Rereads the contents of the file, and stores them in the cache.
-  updateCachedDiskContentsSync: ->
-    @cachedDiskContents = @file?.readSync() ? ""
-
-  # Rereads the contents of the file, and stores them in the cache.
-  updateCachedDiskContents: ->
-    Q(@file?.read() ? "").then (contents) =>
-      @cachedDiskContents = contents
-
-  # Get the basename of the associated file.
-  #
-  # The basename is the name portion of the file's path, without the containing
-  # directories.
-  #
-  # Returns a {String}.
-  getBaseName: ->
-    @file?.getBaseName()
-
-  # Pubilc: Get the path of the associated file.
-  #
-  # Returns a {String}.
-  getPath: ->
-    @file?.getPath()
-
-  # Public: Get the path of the associated file.
-  #
-  # Returns a {String}.
-  getUri: ->
-    @getPath()
-
-  # Public: Set the path for the buffer's associated file.
-  #
-  # * `filePath` A {String} representing the new file path
-  setPath: (filePath) ->
-    return if filePath == @getPath()
-
-    if filePath
-      @file = new File(filePath)
-      @subscribeToFile()
-    else
-      @file = null
-
-    @emit "path-changed", this
-    @emitter.emit 'did-change-path', @getPath()
-
-  # Deprecated: Use {::getEndPosition} instead
-  getEofPosition: ->
-    Grim.deprecate("Use TextBuffer::getEndPosition instead.")
-    @getEndPosition()
-
-  # Public: Save the buffer.
-  save: ->
-    @saveAs(@getPath())
-
-  # Public: Save the buffer at a specific path.
-  #
-  # * `filePath` The path to save at.
-  saveAs: (filePath) ->
-    unless filePath then throw new Error("Can't save buffer with no file path")
-
-    @emit 'will-be-saved', this
-    @emitter.emit 'will-save', {path: filePath}
-    @setPath(filePath)
-    @file.write(@getText())
-    @cachedDiskContents = @getText()
-    @conflict = false
-    @emitModifiedStatusChanged(false)
-    @emit 'saved', this
-    @emitter.emit 'did-save', {path: filePath}
-
-  # Public: Determine if the in-memory contents of the buffer differ from its
-  # contents on disk.
-  #
-  # If the buffer is unsaved, always returns `true` unless the buffer is empty.
-  #
-  # Returns a {Boolean}.
-  isModified: ->
-    return false unless @loaded
-    if @file
-      if @file.exists()
-        @getText() != @cachedDiskContents
-      else
-        @wasModifiedBeforeRemove ? not @isEmpty()
-    else
-      not @isEmpty()
-
-  # Public: Determine if the in-memory contents of the buffer conflict with the
-  # on-disk contents of its associated file.
-  #
-  # Returns a {Boolean}.
-  isInConflict: -> @conflict
-
-  destroyMarker: (id) ->
-    @getMarker(id)?.destroy()
-
-  # Identifies if a character sequence is within a certain range.
-  #
-  # * `regex` The {RegExp} to match.
-  # * `startIndex` A {Number} representing the starting character offset.
-  # * `endIndex` A {Number} representing the ending character offset.
-  #
-  # Returns an {Array} of matches for the given regex.
-  matchesInCharacterRange: (regex, startIndex, endIndex) ->
-    text = @getText()
-    matches = []
-
-    regex.lastIndex = startIndex
-    while match = regex.exec(text)
-      matchLength = match[0].length
-      matchStartIndex = match.index
-      matchEndIndex = matchStartIndex + matchLength
-
-      if matchEndIndex > endIndex
-        regex.lastIndex = 0
-        if matchStartIndex < endIndex and submatch = regex.exec(text[matchStartIndex...endIndex])
-          submatch.index = matchStartIndex
-          matches.push submatch
-        break
-
-      matchEndIndex++ if matchLength is 0
-      regex.lastIndex = matchEndIndex
-      matches.push match
-
-    matches
-
-  # Public: Scan regular expression matches in the entire buffer, calling the
-  # given iterator function on each match.
-  #
-  # If you're programmatically modifying the results, you may want to try
-  # {::backwardsScan} to avoid tripping over your own changes.
-  #
-  # * `regex` A {RegExp} to search for.
-  # * `iterator` A {Function} that's called on each match with an {Object}
-  #   containing the following keys:
-  #   * `match` The current regular expression match.
-  #   * `matchText` A {String} with the text of the match.
-  #   * `range` The {Range} of the match.
-  #   * `stop` Call this {Function} to terminate the scan.
-  #   * `replace` Call this {Function} with a {String} to replace the match.
-  scan: (regex, iterator) ->
-    @scanInRange regex, @getRange(), (result) =>
-      result.lineText = @lineForRow(result.range.start.row)
-      result.lineTextOffset = 0
-      iterator(result)
-
-  # Public: Scan regular expression matches in the entire buffer in reverse
-  # order, calling the given iterator function on each match.
-  #
-  # * `regex` A {RegExp} to search for.
-  # * `iterator` A {Function} that's called on each match with an {Object}
-  #   containing the following keys:
-  #   * `match` The current regular expression match.
-  #   * `matchText` A {String} with the text of the match.
-  #   * `range` The {Range} of the match.
-  #   * `stop` Call this {Function} to terminate the scan.
-  #   * `replace` Call this {Function} with a {String} to replace the match.
-  backwardsScan: (regex, iterator) ->
-    @backwardsScanInRange regex, @getRange(), (result) =>
-      result.lineText = @lineForRow(result.range.start.row)
-      result.lineTextOffset = 0
-      iterator(result)
-
-  # Public: Replace all regular expression matches in the entire buffer.
-  #
-  # * `regex` A {RegExp} representing the matches to be replaced.
-  # * `replacementText` A {String} representing the text to replace each match.
-  #
-  # Returns a {Number} representing the number of replacements made.
-  replace: (regex, replacementText) ->
-    doSave = !@isModified()
-    replacements = 0
-
-    @transact =>
-      @scan regex, ({matchText, replace}) ->
-        replace(matchText.replace(regex, replacementText))
-        replacements++
-
-    @save() if doSave
-
-    replacements
-
-  # Public: Scan regular expression matches in a given range , calling the given
-  # iterator function on each match.
-  #
-  # * `regex` A {RegExp} to search for.
-  # * `range` A {Range} in which to search.
-  # * `iterator` A {Function} that's called on each match with an {Object}
-  #   containing the following keys:
-  #   * `match` The current regular expression match.
-  #   * `matchText` A {String} with the text of the match.
-  #   * `range` The {Range} of the match.
-  #   * `stop` Call this {Function} to terminate the scan.
-  #   * `replace` Call this {Function} with a {String} to replace the match.
-  scanInRange: (regex, range, iterator, reverse=false) ->
-    range = @clipRange(range)
-    global = regex.global
-    flags = "gm"
-    flags += "i" if regex.ignoreCase
-    regex = new RegExp(regex.source, flags)
-
-    startIndex = @characterIndexForPosition(range.start)
-    endIndex = @characterIndexForPosition(range.end)
-
-    matches = @matchesInCharacterRange(regex, startIndex, endIndex)
-    lengthDelta = 0
-
-    keepLooping = null
-    replacementText = null
-    stop = -> keepLooping = false
-    replace = (text) -> replacementText = text
-
-    matches.reverse() if reverse
-    for match in matches
-      matchLength = match[0].length
-      matchStartIndex = match.index
-      matchEndIndex = matchStartIndex + matchLength
-
-      startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta)
-      endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta)
-      range = new Range(startPosition, endPosition)
-      keepLooping = true
-      replacementText = null
-      matchText = match[0]
-      iterator({ match, matchText, range, stop, replace })
-
-      if replacementText?
-        @setTextInRange(range, replacementText)
-        lengthDelta += replacementText.length - matchLength unless reverse
-
-      break unless global and keepLooping
-
-  # Public: Scan regular expression matches in a given range in reverse order,
-  # calling the given iterator function on each match.
-  #
-  # * `regex` A {RegExp} to search for.
-  # * `range` A {Range} in which to search.
-  # * `iterator` A {Function} that's called on each match with an {Object}
-  #   containing the following keys:
-  #   * `match` The current regular expression match.
-  #   * `matchText` A {String} with the text of the match.
-  #   * `range` The {Range} of the match.
-  #   * `stop` Call this {Function} to terminate the scan.
-  #   * `replace` Call this {Function} with a {String} to replace the match.
-  backwardsScanInRange: (regex, range, iterator) ->
-    @scanInRange regex, range, iterator, true
+  lineLengthForRow: (row) ->
+    @lines[row].length

   # Public: Determine if the given row contains only whitespace.
   #
@@ -1047,82 +447,228 @@
         return row unless @isRowBlank(row)
     null

-  # Deprecate
-  usesSoftTabs: ->
-    Grim.deprecate("Use Editor::usesSoftTabs instead. TextBuffer doesn't have enough context to determine this.")
-    for row in [0..@getLastRow()]
-      if match = @lineForRow(row).match(/^\s/)
-        return match[0][0] != '\t'
-    undefined
-
-  # Deprecated: Call {::setTextInRange} instead.
-  change: (oldRange, newText, options={}) ->
-    Grim.deprecate("Use TextBuffer::setTextInRange instead.")
-    @setTextInRange(oldRange, newText, options.normalizeLineEndings)
-
-  cancelStoppedChangingTimeout: ->
-    clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout
-
-  scheduleModifiedEvents: ->
-    @cancelStoppedChangingTimeout()
-    stoppedChangingCallback = =>
-      @stoppedChangingTimeout = null
-      modifiedStatus = @isModified()
-      @emit 'contents-modified', modifiedStatus
-      @emitter.emit 'did-stop-changing'
-      @emitModifiedStatusChanged(modifiedStatus)
-    @stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay)
-
-  emitModifiedStatusChanged: (modifiedStatus) ->
-    return if modifiedStatus is @previousModifiedStatus
-    @previousModifiedStatus = modifiedStatus
-    @emit 'modified-status-changed', modifiedStatus
-    @emitter.emit 'did-change-modified', modifiedStatus
-
-  logLines: (start=0, end=@getLastRow())->
-    for row in [start..end]
-      line = @lineForRow(row)
-      console.log row, line, line.length
-
-  @delegatesMethods 'undo', 'redo', 'transact', 'beginTransaction', 'commitTransaction',
-    'abortTransaction', 'isTransacting', 'clearUndoStack', toProperty: 'history'
-
-  # Public: Undo the last operation. If a transaction is in progress, aborts it.
-  undo: -> @history.undo()
-
-  # Public: Redo the last operation
-  redo: -> @history.redo()
-
-  # Public: Batch multiple operations as a single undo/redo step.
-  #
-  # Any group of operations that are logically grouped from the perspective of
-  # undoing and redoing should be performed in a transaction. If you want to
-  # abort the transaction, call {::abortTransaction} to terminate the function's
-  # execution and revert any changes performed up to the abortion.
-  #
-  # * `fn` A {Function} to call inside the transaction.
-  transact: (fn) -> @history.transact(fn)
-
-  # Public: Start an open-ended transaction.
-  #
-  # Call {::commitTransaction} or {::abortTransaction} to terminate the
-  # transaction. If you nest calls to transactions, only the outermost
-  # transaction is considered. You must match every begin with a matching
-  # commit, but a single call to abort will cancel all nested transactions.
-  beginTransaction: -> @history.beginTransaction()
-
-  # Public: Commit an open-ended transaction started with {::beginTransaction}
-  # and push it to the undo stack.
-  #
-  # If transactions are nested, only the outermost commit takes effect.
-  commitTransaction: -> @history.commitTransaction()
-
-  # Public: Abort an open transaction, undoing any operations performed so far
-  # within the transaction.
-  abortTransaction: -> @history.abortTransaction()
-
-  # Public: Clear the undo stack.
-  clearUndoStack: -> @history.clearUndoStack()
+  ###
+  Section: Mutating Text
+  ###
+
+  # Public: Replace the entire contents of the buffer with the given text.
+  #
+  # * `text` A {String}
+  #
+  # Returns a {Range} spanning the new buffer contents.
+  setText: (text) ->
+    @setTextInRange(@getRange(), text, false)
+
+  # Public: Replace the current buffer contents by applying a diff based on the
+  # given text.
+  #
+  # * `text` A {String} containing the new buffer contents.
+  setTextViaDiff: (text) ->
+    currentText = @getText()
+    return if currentText == text
+
+    endsWithNewline = (str) ->
+      /[\r\n]+$/g.test(str)
+
+    computeBufferColumn = (str) ->
+      newlineIndex = Math.max(str.lastIndexOf('\n'), str.lastIndexOf('\r'))
+      if endsWithNewline(str)
+        0
+      else if newlineIndex == -1
+        str.length
+      else
+        str.length - newlineIndex - 1
+
+    @transact =>
+      row = 0
+      column = 0
+      currentPosition = [0, 0]
+
+      lineDiff = diff.diffLines(currentText, text)
+      changeOptions = normalizeLineEndings: false
+
+      for change in lineDiff
+        lineCount = change.value.match(newlineRegex)?.length ? 0
+        currentPosition[0] = row
+        currentPosition[1] = column
+
+        if change.added
+          @setTextInRange([currentPosition, currentPosition], change.value, changeOptions)
+          row += lineCount
+          column = computeBufferColumn(change.value)
+
+        else if change.removed
+          endRow = row + lineCount
+          endColumn = column + computeBufferColumn(change.value)
+          @setTextInRange([currentPosition, [endRow, endColumn]], '', changeOptions)
+
+        else
+          row += lineCount
+          column = computeBufferColumn(change.value)
+
+  # Public: Set the text in the given range.
+  #
+  # * `range` A {Range}
+  # * `text` A {String}
+  # * `normalizeLineEndings` (optional) {Boolean} (default: true)
+  #
+  # Returns the {Range} of the inserted text.
+  setTextInRange: (range, text, normalizeLineEndings=true) ->
+    patch = @buildPatch(range, text, normalizeLineEndings)
+    @history?.recordNewPatch(patch)
+    @applyPatch(patch)
+    patch.newRange
+
+  # Public: Insert text at the given position.
+  #
+  # * `position` A {Point} representing the insertion location. The position is
+  #   clipped before insertion.
+  # * `text` A {String} representing the text to insert.
+  # * `normalizeLineEndings` (optional) {Boolean} (default: true)\
+  #
+  # Returns the {Range} of the inserted text.
+  insert: (position, text, normalizeLineEndings) ->
+    @setTextInRange(new Range(position, position), text, normalizeLineEndings)
+
+  # Public: Append text to the end of the buffer.
+  #
+  # * `text` A {String} representing the text text to append.
+  #
+  # Returns the {Range} of the inserted text
+  append: (text, normalizeLineEndings) ->
+    @insert(@getEndPosition(), text, normalizeLineEndings)
+
+  # Builds a {BufferPatch}, which is used to modify the buffer and is also
+  # pushed into the undo history so it can be undone.
+  buildPatch: (oldRange, newText, normalizeLineEndings) ->
+    oldRange = @clipRange(oldRange)
+    oldText = @getTextInRange(oldRange)
+    newRange = Range.fromText(oldRange.start, newText)
+    patch = new BufferPatch(oldRange, newRange, oldText, newText, normalizeLineEndings)
+    @markers?.handleBufferChange(patch)
+    patch
+
+  # Applies a {BufferPatch} to the buffer based on its old range and new text.
+  # Also applies any {MarkerPatch}es associated with the {BufferPatch}.
+  applyPatch: ({oldRange, newRange, oldText, newText, normalizeLineEndings, markerPatches}) ->
+    @cachedText = null
+
+    startRow = oldRange.start.row
+    endRow = oldRange.end.row
+    rowCount = endRow - startRow + 1
+
+    # Determine how to normalize the line endings of inserted text if enabled
+    if normalizeLineEndings
+      normalizedEnding = @lineEndingForRow(startRow)
+      if normalizedEnding is ''
+        if startRow > 0
+          normalizedEnding = @lineEndingForRow(startRow - 1)
+        else
+          normalizedEnding = null
+
+    # Split inserted text into lines and line endings
+    lines = []
+    lineEndings = []
+    lineStartIndex = 0
+    while result = newlineRegex.exec(newText)
+      lines.push(newText[lineStartIndex...result.index])
+      lineEndings.push(normalizedEnding ? result[0])
+      lineStartIndex = newlineRegex.lastIndex
+
+    lastLine = newText[lineStartIndex..]
+    lines.push(lastLine)
+    lineEndings.push('')
+
+    # Update first and last line so replacement preserves existing prefix and suffix of oldRange
+    prefix = @lineForRow(startRow)[0...oldRange.start.column]
+    lines[0] = prefix + lines[0]
+    suffix = @lineForRow(endRow)[oldRange.end.column...]
+    lastIndex = lines.length - 1
+    lines[lastIndex] += suffix
+    lastLineEnding = @lineEndingForRow(endRow)
+    lastLineEnding = normalizedEnding if lastLineEnding isnt '' and normalizedEnding?
+    lineEndings[lastIndex] = lastLineEnding
+
+    # Replace lines in oldRange with new lines
+    spliceArray(@lines, startRow, rowCount, lines)
+    spliceArray(@lineEndings, startRow, rowCount, lineEndings)
+
+    # Update the offset index for position <-> character offset translation
+    offsets = lines.map (line, index) ->
+      {rows: 1, characters: line.length + lineEndings[index].length}
+    @offsetIndex.spliceArray('rows', startRow, rowCount, offsets)
+
+    @markers?.pauseChangeEvents()
+    @markers?.applyPatches(markerPatches, true)
+
+    changeEvent = {oldRange, newRange, oldText, newText}
+
+    @conflict = false if @conflict and !@isModified()
+    @scheduleModifiedEvents()
+    @emit 'changed', changeEvent
+    @emitter.emit 'did-change', changeEvent
+    @markers?.resumeChangeEvents()
+    @emit 'markers-updated'
+    @emitter.emit 'did-update-markers'
+
+  ###
+  Section: Removing Text
+  ###
+
+  # Public: Delete the text in the given range.
+  #
+  # * `range` A {Range} in which to delete. The range is clipped before deleting.
+  #
+  # Returns an empty {Range} starting at the start of deleted range.
+  delete: (range) ->
+    @setTextInRange(range, '')
+
+  # Public: Delete the line associated with a specified row.
+  #
+  # * `row` A {Number} representing the 0-indexed row to delete.
+  #
+  # Returns the {Range} of the deleted text.
+  deleteRow: (row) ->
+    @deleteRows(row, row)
+
+  # Public: Delete the lines associated with the specified row range.
+  #
+  # If the row range is out of bounds, it will be clipped. If the startRow is
+  # greater than the end row, they will be reordered.
+  #
+  # * `startRow` A {Number} representing the first row to delete.
+  # * `endRow` A {Number} representing the last row to delete, inclusive.
+  #
+  # Returns the {Range} of the deleted text.
+  deleteRows: (startRow, endRow) ->
+    lastRow = @getLastRow()
+
+    [startRow, endRow] = [endRow, startRow] if startRow > endRow
+
+    if endRow < 0
+      return new Range(@getFirstPosition(), @getFirstPosition())
+
+    if startRow > lastRow
+      return new Range(@getEndPosition(), @getEndPosition())
+
+    startRow = Math.max(0, startRow)
+    endRow = Math.min(lastRow, endRow)
+
+    if endRow < lastRow
+      startPoint = new Point(startRow, 0)
+      endPoint = new Point(endRow + 1, 0)
+    else
+      if startRow is 0
+        startPoint = new Point(startRow, 0)
+      else
+        startPoint = new Point(startRow - 1, @lineLengthForRow(startRow - 1))
+      endPoint = new Point(endRow, @lineLengthForRow(endRow))
+
+    @delete(new Range(startPoint, endPoint))
+
+  ###
+  Section: Markers
+  ###

   # Public: Create a marker with the given range. This marker will maintain
   # its logical location as the buffer is changed, so if you mark a particular
@@ -1160,15 +706,17 @@
   # Returns a {Marker}.
   markPosition: (position, properties) -> @markers.markPosition(position, properties)

+  # Public: Get all existing markers on the buffer.
+  #
+  # Returns an {Array} of {Marker}s.
+  getMarkers: -> @markers.getMarkers()
+
   # Public: Get an existing marker by its id.
+  #
+  # * `id` {Number} id of the marker to retrieve
   #
   # Returns a {Marker}.
   getMarker: (id) -> @markers.getMarker(id)
-
-  # Public: Get all existing markers on the buffer.
-  #
-  # Returns an {Array} of {Marker}s.
-  getMarkers: -> @markers.getMarkers()

   # Public: Find markers conforming to the given parameters.
   #
@@ -1195,6 +743,513 @@
   # Returns a {Number}.
   getMarkerCount: -> @markers.getMarkerCount()

+  destroyMarker: (id) ->
+    @getMarker(id)?.destroy()
+
+  ###
+  Section: History
+  ###
+
+  # Public: Undo the last operation. If a transaction is in progress, aborts it.
+  undo: -> @history.undo()
+
+  # Public: Redo the last operation
+  redo: -> @history.redo()
+
+  # Public: Batch multiple operations as a single undo/redo step.
+  #
+  # Any group of operations that are logically grouped from the perspective of
+  # undoing and redoing should be performed in a transaction. If you want to
+  # abort the transaction, call {::abortTransaction} to terminate the function's
+  # execution and revert any changes performed up to the abortion.
+  #
+  # * `fn` A {Function} to call inside the transaction.
+  transact: (fn) -> @history.transact(fn)
+
+  # Public: Start an open-ended transaction.
+  #
+  # Call {::commitTransaction} or {::abortTransaction} to terminate the
+  # transaction. If you nest calls to transactions, only the outermost
+  # transaction is considered. You must match every begin with a matching
+  # commit, but a single call to abort will cancel all nested transactions.
+  beginTransaction: -> @history.beginTransaction()
+
+  # Public: Commit an open-ended transaction started with {::beginTransaction}
+  # and push it to the undo stack.
+  #
+  # If transactions are nested, only the outermost commit takes effect.
+  commitTransaction: -> @history.commitTransaction()
+
+  # Public: Abort an open transaction, undoing any operations performed so far
+  # within the transaction.
+  abortTransaction: -> @history.abortTransaction()
+
+  # Public: Clear the undo stack.
+  clearUndoStack: -> @history.clearUndoStack()
+
+  ###
+  Section: Search And Replace
+  ###
+
+  # Public: Scan regular expression matches in the entire buffer, calling the
+  # given iterator function on each match.
+  #
+  # If you're programmatically modifying the results, you may want to try
+  # {::backwardsScan} to avoid tripping over your own changes.
+  #
+  # * `regex` A {RegExp} to search for.
+  # * `iterator` A {Function} that's called on each match with an {Object}
+  #   containing the following keys:
+  #   * `match` The current regular expression match.
+  #   * `matchText` A {String} with the text of the match.
+  #   * `range` The {Range} of the match.
+  #   * `stop` Call this {Function} to terminate the scan.
+  #   * `replace` Call this {Function} with a {String} to replace the match.
+  scan: (regex, iterator) ->
+    @scanInRange regex, @getRange(), (result) =>
+      result.lineText = @lineForRow(result.range.start.row)
+      result.lineTextOffset = 0
+      iterator(result)
+
+  # Public: Scan regular expression matches in the entire buffer in reverse
+  # order, calling the given iterator function on each match.
+  #
+  # * `regex` A {RegExp} to search for.
+  # * `iterator` A {Function} that's called on each match with an {Object}
+  #   containing the following keys:
+  #   * `match` The current regular expression match.
+  #   * `matchText` A {String} with the text of the match.
+  #   * `range` The {Range} of the match.
+  #   * `stop` Call this {Function} to terminate the scan.
+  #   * `replace` Call this {Function} with a {String} to replace the match.
+  backwardsScan: (regex, iterator) ->
+    @backwardsScanInRange regex, @getRange(), (result) =>
+      result.lineText = @lineForRow(result.range.start.row)
+      result.lineTextOffset = 0
+      iterator(result)
+
+  # Public: Scan regular expression matches in a given range , calling the given
+  # iterator function on each match.
+  #
+  # * `regex` A {RegExp} to search for.
+  # * `range` A {Range} in which to search.
+  # * `iterator` A {Function} that's called on each match with an {Object}
+  #   containing the following keys:
+  #   * `match` The current regular expression match.
+  #   * `matchText` A {String} with the text of the match.
+  #   * `range` The {Range} of the match.
+  #   * `stop` Call this {Function} to terminate the scan.
+  #   * `replace` Call this {Function} with a {String} to replace the match.
+  scanInRange: (regex, range, iterator, reverse=false) ->
+    range = @clipRange(range)
+    global = regex.global
+    flags = "gm"
+    flags += "i" if regex.ignoreCase
+    regex = new RegExp(regex.source, flags)
+
+    startIndex = @characterIndexForPosition(range.start)
+    endIndex = @characterIndexForPosition(range.end)
+
+    matches = @matchesInCharacterRange(regex, startIndex, endIndex)
+    lengthDelta = 0
+
+    keepLooping = null
+    replacementText = null
+    stop = -> keepLooping = false
+    replace = (text) -> replacementText = text
+
+    matches.reverse() if reverse
+    for match in matches
+      matchLength = match[0].length
+      matchStartIndex = match.index
+      matchEndIndex = matchStartIndex + matchLength
+
+      startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta)
+      endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta)
+      range = new Range(startPosition, endPosition)
+      keepLooping = true
+      replacementText = null
+      matchText = match[0]
+      iterator({ match, matchText, range, stop, replace })
+
+      if replacementText?
+        @setTextInRange(range, replacementText)
+        lengthDelta += replacementText.length - matchLength unless reverse
+
+      break unless global and keepLooping
+
+  # Public: Scan regular expression matches in a given range in reverse order,
+  # calling the given iterator function on each match.
+  #
+  # * `regex` A {RegExp} to search for.
+  # * `range` A {Range} in which to search.
+  # * `iterator` A {Function} that's called on each match with an {Object}
+  #   containing the following keys:
+  #   * `match` The current regular expression match.
+  #   * `matchText` A {String} with the text of the match.
+  #   * `range` The {Range} of the match.
+  #   * `stop` Call this {Function} to terminate the scan.
+  #   * `replace` Call this {Function} with a {String} to replace the match.
+  backwardsScanInRange: (regex, range, iterator) ->
+    @scanInRange regex, range, iterator, true
+
+  # Public: Replace all regular expression matches in the entire buffer.
+  #
+  # * `regex` A {RegExp} representing the matches to be replaced.
+  # * `replacementText` A {String} representing the text to replace each match.
+  #
+  # Returns a {Number} representing the number of replacements made.
+  replace: (regex, replacementText) ->
+    doSave = !@isModified()
+    replacements = 0
+
+    @transact =>
+      @scan regex, ({matchText, replace}) ->
+        replace(matchText.replace(regex, replacementText))
+        replacements++
+
+    @save() if doSave
+
+    replacements
+
+  # Identifies if a character sequence is within a certain range.
+  #
+  # * `regex` The {RegExp} to match.
+  # * `startIndex` A {Number} representing the starting character offset.
+  # * `endIndex` A {Number} representing the ending character offset.
+  #
+  # Returns an {Array} of matches for the given regex.
+  matchesInCharacterRange: (regex, startIndex, endIndex) ->
+    text = @getText()
+    matches = []
+
+    regex.lastIndex = startIndex
+    while match = regex.exec(text)
+      matchLength = match[0].length
+      matchStartIndex = match.index
+      matchEndIndex = matchStartIndex + matchLength
+
+      if matchEndIndex > endIndex
+        regex.lastIndex = 0
+        if matchStartIndex < endIndex and submatch = regex.exec(text[matchStartIndex...endIndex])
+          submatch.index = matchStartIndex
+          matches.push submatch
+        break
+
+      matchEndIndex++ if matchLength is 0
+      regex.lastIndex = matchEndIndex
+      matches.push match
+
+    matches
+
+  ###
+  Section: Buffer Range Data
+  ###
+
+  # Public: Get the range spanning from `[0, 0]` to {::getEndPosition}.
+  #
+  # Returns a {Range}.
+  getRange: ->
+    new Range(@getFirstPosition(), @getEndPosition())
+
+  # Public: Get the number of lines in the buffer.
+  #
+  # Returns a {Number}.
+  getLineCount: ->
+    @lines.length
+
+  # Public: Get the last 0-indexed row in the buffer.
+  #
+  # Returns a {Number}.
+  getLastRow: ->
+    @getLineCount() - 1
+
+  # Public: Get the first position in the buffer, which is always `[0, 0]`.
+  #
+  # Returns a {Point}.
+  getFirstPosition: ->
+    new Point(0, 0)
+
+  # Public: Get the maximal position in the buffer, where new text would be
+  # appended.
+  #
+  # Returns a {Point}.
+  getEndPosition: ->
+    lastRow = @getLastRow()
+    new Point(lastRow, @lineLengthForRow(lastRow))
+
+  # Public: Get the length of the buffer in characters.
+  #
+  # Returns a {Number}.
+  getMaxCharacterIndex: ->
+    @offsetIndex.totalTo(Infinity, 'rows').characters
+
+  # Public: Get the range for the given row
+  #
+  # * `row` A {Number} representing a 0-indexed row.
+  # * `includeNewline` A {Boolean} indicating whether or not to include the
+  #   newline, which results in a range that extends to the start
+  #   of the next line.
+  #
+  # Returns a {Range}.
+  rangeForRow: (row, includeNewline) ->
+    # Handle deprecated options hash
+    if typeof includeNewline is 'object'
+      Grim.deprecate("The second param is no longer an object, it's a boolean argument named `includeNewline`.")
+      {includeNewline} = includeNewline
+
+    if includeNewline and row < @getLastRow()
+      new Range(new Point(row, 0), new Point(row + 1, 0))
+    else
+      new Range(new Point(row, 0), new Point(row, @lineLengthForRow(row)))
+
+  # Public: Convert a position in the buffer in row/column coordinates to an
+  # absolute character offset, inclusive of line ending characters.
+  #
+  # The position is clipped prior to translating.
+  #
+  # * `position` A {Point}.
+  #
+  # Returns a {Number}.
+  characterIndexForPosition: (position) ->
+    {row, column} = @clipPosition(Point.fromObject(position))
+
+    if row < 0 or row > @getLastRow() or column < 0 or column > @lineLengthForRow(row)
+      throw new Error("Position #{position} is invalid")
+
+    {characters} = @offsetIndex.totalTo(row, 'rows')
+    characters + column
+
+  # Public: Convert an absolute character offset, inclusive of newlines, to a
+  # position in the buffer in row/column coordinates.
+  #
+  # The offset is clipped prior to translating.
+  #
+  # * `offset` A {Number}.
+  #
+  # Returns a {Point}.
+  positionForCharacterIndex: (offset) ->
+    offset = Math.max(0, offset)
+    offset = Math.min(@getMaxCharacterIndex(), offset)
+
+    {rows, characters} = @offsetIndex.totalTo(offset, 'characters')
+    if rows > @getLastRow()
+      @getEndPosition()
+    else
+      new Point(rows, offset - characters)
+
+  # Public: Clip the given range so it starts and ends at valid positions.
+  #
+  # For example, the position `[1, 100]` is out of bounds if the line at row 1 is
+  # only 10 characters long, and it would be clipped to `(1, 10)`.
+  #
+  # * `range` A {Range} or range-compatible {Array} to clip.
+  #
+  # Returns the given {Range} if it is already in bounds, or a new clipped
+  # {Range} if the given range is out-of-bounds.
+  clipRange: (range) ->
+    range = Range.fromObject(range)
+    start = @clipPosition(range.start)
+    end = @clipPosition(range.end)
+    if range.start.isEqual(start) and range.end.isEqual(end)
+      range
+    else
+      new Range(start, end)
+
+  # Public: Clip the given point so it is at a valid position in the buffer.
+  #
+  # For example, the position (1, 100) is out of bounds if the line at row 1 is
+  # only 10 characters long, and it would be clipped to (1, 10)
+  #
+  # * `position` A {Point} or point-compatible {Array}.
+  #
+  # Returns a new {Point} if the given position is invalid, otherwise returns
+  # the given position.
+  clipPosition: (position) ->
+    position = Point.fromObject(position)
+    {row, column} = position
+    if row < 0
+      @getFirstPosition()
+    else if row > @getLastRow()
+      @getEndPosition()
+    else
+      column = Math.min(Math.max(column, 0), @lineLengthForRow(row))
+      if column is position.column
+        position
+      else
+        new Point(row, column)
+
+
+  ###
+  Section: Buffer Operations
+  ###
+
+  # Public: Save the buffer.
+  save: ->
+    @saveAs(@getPath())
+
+  # Public: Save the buffer at a specific path.
+  #
+  # * `filePath` The path to save at.
+  saveAs: (filePath) ->
+    unless filePath then throw new Error("Can't save buffer with no file path")
+
+    @emit 'will-be-saved', this
+    @emitter.emit 'will-save', {path: filePath}
+    @setPath(filePath)
+    @file.write(@getText())
+    @cachedDiskContents = @getText()
+    @conflict = false
+    @emitModifiedStatusChanged(false)
+    @emit 'saved', this
+    @emitter.emit 'did-save', {path: filePath}
+
+  # Public: Reload the buffer's contents from disk.
+  #
+  # Sets the buffer's content to the cached disk contents
+  reload: ->
+    @emit 'will-reload'
+    @emitter.emit 'will-reload'
+    @setTextViaDiff(@cachedDiskContents)
+    @emitModifiedStatusChanged(false)
+    @emit 'reloaded'
+    @emitter.emit 'did-reload'
+
+  # Rereads the contents of the file, and stores them in the cache.
+  updateCachedDiskContentsSync: ->
+    @cachedDiskContents = @file?.readSync() ? ""
+
+  # Rereads the contents of the file, and stores them in the cache.
+  updateCachedDiskContents: ->
+    Q(@file?.read() ? "").then (contents) =>
+      @cachedDiskContents = contents
+
+  ###
+  Section: Private Utility Methods
+  ###
+
   markerCreated: (marker) ->
     @emit 'marker-created', marker
     @emitter.emit 'did-create-marker', marker
+
+  loadSync: ->
+    @updateCachedDiskContentsSync()
+    @finishLoading()
+
+  load: ->
+    @updateCachedDiskContents().then => @finishLoading()
+
+  finishLoading: ->
+    if @isAlive()
+      @loaded = true
+      if @useSerializedText and @digestWhenLastPersisted is @file?.getDigest()
+        @emitModifiedStatusChanged(true)
+      else
+        @reload()
+      @clearUndoStack()
+    this
+
+  destroy: ->
+    unless @destroyed
+      @cancelStoppedChangingTimeout()
+      @fileSubscriptions?.dispose()
+      @unsubscribe()
+      @destroyed = true
+      @emit 'destroyed'
+      @emitter.emit 'did-destroy'
+
+  isAlive: -> not @destroyed
+
+  isDestroyed: -> @destroyed
+
+  isRetained: -> @refcount > 0
+
+  retain: ->
+    @refcount++
+    this
+
+  release: ->
+    @refcount--
+    @destroy() unless @isRetained()
+    this
+
+  subscribeToFile: ->
+    @fileSubscriptions?.dispose()
+    @fileSubscriptions = new CompositeDisposable
+
+    @fileSubscriptions.add @file.onDidChange =>
+      @conflict = true if @isModified()
+      previousContents = @cachedDiskContents
+
+      # Synchrounously update the disk contents because the {File} has already cached them. If the
+      # contents updated asynchrounously multiple `conlict` events could trigger for the same disk
+      # contents.
+      @updateCachedDiskContentsSync()
+      return if previousContents == @cachedDiskContents
+
+      if @conflict
+        @emit "contents-conflicted"
+        @emitter.emit 'did-conflict'
+      else
+        @reload()
+
+    @fileSubscriptions.add @file.onDidDelete =>
+      modified = @getText() != @cachedDiskContents
+      @wasModifiedBeforeRemove = modified
+      if modified
+        @updateCachedDiskContents()
+      else
+        @destroy()
+
+    @fileSubscriptions.add @file.onDidRename =>
+      @emit "path-changed", this
+      @emitter.emit 'did-change-path', @getPath()
+
+  # Identifies if the buffer belongs to multiple editors.
+  #
+  # For example, if the {EditorView} was split.
+  #
+  # Returns a {Boolean}.
+  hasMultipleEditors: -> @refcount > 1
+
+  cancelStoppedChangingTimeout: ->
+    clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout
+
+  scheduleModifiedEvents: ->
+    @cancelStoppedChangingTimeout()
+    stoppedChangingCallback = =>
+      @stoppedChangingTimeout = null
+      modifiedStatus = @isModified()
+      @emit 'contents-modified', modifiedStatus
+      @emitter.emit 'did-stop-changing'
+      @emitModifiedStatusChanged(modifiedStatus)
+    @stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay)
+
+  emitModifiedStatusChanged: (modifiedStatus) ->
+    return if modifiedStatus is @previousModifiedStatus
+    @previousModifiedStatus = modifiedStatus
+    @emit 'modified-status-changed', modifiedStatus
+    @emitter.emit 'did-change-modified', modifiedStatus
+
+  # Deprecate
+  usesSoftTabs: ->
+    Grim.deprecate("Use Editor::usesSoftTabs instead. TextBuffer doesn't have enough context to determine this.")
+    for row in [0..@getLastRow()]
+      if match = @lineForRow(row).match(/^\s/)
+        return match[0][0] != '\t'
+    undefined
+
+  # Deprecated: Call {::setTextInRange} instead.
+  change: (oldRange, newText, options={}) ->
+    Grim.deprecate("Use TextBuffer::setTextInRange instead.")
+    @setTextInRange(oldRange, newText, options.normalizeLineEndings)
+
+  # Deprecated: Use {::getEndPosition} instead
+  getEofPosition: ->
+    Grim.deprecate("Use TextBuffer::getEndPosition instead.")
+    @getEndPosition()
+
+  logLines: (start=0, end=@getLastRow())->
+    for row in [start..end]
+      line = @lineForRow(row)
+      console.log row, line, line.length


Add View::findSiblingViews()

Add View::findSiblingViews()

--- src/marbles/view.js.coffee
+++ src/marbles/view.js.coffee
@@ -138,6 +138,9 @@
     @[key] = view.cid if view
     view

+  findSiblingViews: (view_class_name) =>
+    @parentView()?.childViews(view_class_name)
+
   context: =>
     {}


watch for global mouse events - resolves #9

Former-commit-id: 1afde344d7ce8ba876f25f74581526ce2c266d2d [formerly 72293f006b776c36a75d0e64ac926a35233044b2]
Former-commit-id: a5b39457e45890e1fdb7ce30cfaa09e05d11ed72

--- src/utils/mouse.coffee
+++ src/utils/mouse.coffee
@@ -1,6 +1,6 @@
 module.exports = Mouse =

-  # returns the mouse x & y coords
+  # returns the mouse x & y coords relative to the target
   getMouseCoords: (e) ->
     # center the view on double click
     mouseX = e.offsetX
@@ -21,3 +21,8 @@

     # TODO: else
     return [mouseX,mouseY]
+
+  getMouseCoordsScreen: (e) ->
+    mouseX = e.screenX
+    mouseY = e.screenY
+    return [mouseX,mouseY]


Fix incorrect name for pandoc path option

--- lib/langs/markdown-beautify.coffee
+++ lib/langs/markdown-beautify.coffee
@@ -7,7 +7,7 @@

 getCmd = (inputPath, outputPath, options, cb) ->
   optionsStr = " --read markdown --write markdown --output \"" + outputPath + "\" \"" + inputPath + "\""
-  pandocPath = options.markdown_beautifier_path # jshint ignore: line
+  pandocPath = options.pandoc_path # jshint ignore: line
   yamlFrontMatter = options.yaml_front_matter # jshint ignore: line
   cmd = ""
   if pandocPath?


Removed this commit

Removed this commit

I was just messing around, forgot that I had committed it.

--- src/octonode/client.coffee
+++ src/octonode/client.coffee
@@ -191,14 +191,6 @@
       return callback(err) if err
       if s isnt 200 then callback(new HttpError('Client rate_limit error', s)) else callback null, b.rate.remaining, b.rate.limit

-  checkToken: (id, callback) ->
-    if typeof @token == 'string'
-      @get "/applications/#{id}tokens/#{@token}", (err, s, b) ->
-        return callback(err) if err
-        if s isnt 200 then callback(new HttpError("Client token checking error", s)) else callback null, true
-
-    else typeof @token == 'object' and @token.id
-      throw new Error("Cannot check token without providing it first!")
 # Export modules
 module.exports = (token, credentials...) ->
   new Client(token, credentials...)


Only disable a package if it isn't already disabled

--- src/package-manager.coffee
+++ src/package-manager.coffee
@@ -199,7 +199,10 @@
   # Returns the {Package} that was disabled or null if it isn't loaded.
   disablePackage: (name) ->
     pack = @loadPackage(name)
-    pack?.disable()
+
+    unless @isPackageDisabled(name)
+      pack?.disable()
+
     pack

   # Public: Is the package with the given name disabled?


:memo: Add docs to all public methods

:memo: Add docs to all public methods

--- src/package-manager.coffee
+++ src/package-manager.coffee
@@ -46,42 +46,44 @@
   getPackageDirPaths: ->
     _.clone(@packageDirPaths)

+  # Private:
   getPackageState: (name) ->
     @packageStates[name]

+  # Private:
   setPackageState: (name, state) ->
     @packageStates[name] = state

-  # Public:
+  # Public: Enable the package with the given name
   enablePackage: (name) ->
     pack = @loadPackage(name)
     pack?.enable()
     pack

-  # Public:
+  # Public: Disable the package with the given name
   disablePackage: (name) ->
     pack = @loadPackage(name)
     pack?.disable()
     pack

-  # Internal-only: Activate all the packages that should be activated.
+  # Private: Activate all the packages that should be activated.
   activate: ->
     for [activator, types] in @packageActivators
       packages = @getLoadedPackagesForTypes(types)
       activator.activatePackages(packages)
     @emit 'activated'

-  # Public: another type of package manager can handle other package types.
+  # Private: another type of package manager can handle other package types.
   # See ThemeManager
   registerPackageActivator: (activator, types) ->
     @packageActivators.push([activator, types])

-  # Internal-only:
+  # Private:
   activatePackages: (packages) ->
     @activatePackage(pack.name) for pack in packages
     @observeDisabledPackages()

-  # Internal-only: Activate a single package by name
+  # Private: Activate a single package by name
   activatePackage: (name, options) ->
     return pack if pack = @getActivePackage(name)
     if pack = @loadPackage(name, options)
@@ -89,10 +91,12 @@
       pack.activate(options)
       pack

+  # Private: Deactivate all packages
   deactivatePackages: ->
     @deactivatePackage(pack.name) for pack in @getActivePackages()
     @unobserveDisabledPackages()

+  # Private: Deactivate the package with the given name
   deactivatePackage: (name) ->
     if pack = @getActivePackage(name)
       @setPackageState(pack.name, state) if state = pack.serialize?()
@@ -101,20 +105,25 @@
     else
       throw new Error("No active package for name '#{name}'")

+  # Public: Get an array of all the active packages
   getActivePackages: ->
     _.values(@activePackages)

+  # Public: Get the active package with the given name
   getActivePackage: (name) ->
     @activePackages[name]

+  # Public: Is the package with the given name active?
   isPackageActive: (name) ->
     @getActivePackage(name)?

+  # Private:
   unobserveDisabledPackages: ->
     return unless @observingDisabledPackages
     atom.config.unobserve('core.disabledPackages')
     @observingDisabledPackages = false

+  # Private:
   observeDisabledPackages: ->
     return if @observingDisabledPackages

@@ -128,6 +137,7 @@

     @observingDisabledPackages = true

+  # Private:
   loadPackages: (options) ->
     # Ensure atom exports is already in the require cache so the load time
     # of the first package isn't skewed by being the first to require atom
@@ -139,6 +149,7 @@
     @loadPackage(packagePath, options) for packagePath in packagePaths
     @emit 'loaded'

+  # Private:
   loadPackage: (nameOrPath, options) ->
     if packagePath = @resolvePackagePath(nameOrPath)
       name = path.basename(nameOrPath)
@@ -150,10 +161,12 @@
     else
       throw new Error("Could not resolve '#{nameOrPath}' to a package path")

+  # Private:
   unloadPackages: ->
     @unloadPackage(name) for name in _.keys(@loadedPackages)
     null

+  # Private:
   unloadPackage: (name) ->
     if @isPackageActive(name)
       throw new Error("Tried to unload active package '#{name}'")
@@ -163,12 +176,15 @@
     else
       throw new Error("No loaded package for name '#{name}'")

+  # Public: Get the loaded package with the given name
   getLoadedPackage: (name) ->
     @loadedPackages[name]

+  # Public: Is the package with the given name loaded?
   isPackageLoaded: (name) ->
     @getLoadedPackage(name)?

+  # Public: Get an array of all the loaded packages
   getLoadedPackages: ->
     _.values(@loadedPackages)

@@ -178,6 +194,7 @@
   getLoadedPackagesForTypes: (types) ->
     pack for pack in @getLoadedPackages() when pack.getType() in types

+  # Public: Resolve the given package name to a path on disk.
   resolvePackagePath: (name) ->
     return name if fs.isDirectorySync(name)

@@ -185,18 +202,22 @@
     return packagePath if fs.isDirectorySync(packagePath)

     packagePath = path.join(@resourcePath, 'node_modules', name)
-    return packagePath if @isInternalPackage(packagePath)
-
+    return packagePath if @hasAtomEngine(packagePath)
+
+  # Public: Is the package with the given name disabled?
   isPackageDisabled: (name) ->
     _.include(atom.config.get('core.disabledPackages') ? [], name)

-  isInternalPackage: (packagePath) ->
-    {engines} = Package.loadMetadata(packagePath, true)
-    engines?.atom?
-
-  isBundledPackage: (packageName) ->
+  # Private:
+  hasAtomEngine: (packagePath) ->
+    metadata = Package.loadMetadata(packagePath, true)
+    metadata?.engines?.atom?
+
+  # Public: Is the package with the given name bundled with Atom?
+  isBundledPackage: (name) ->
     @getPackageDependencies().hasOwnProperty(packageName)

+  # Private:
   getPackageDependencies: ->
     unless @packageDependencies?
       try
@@ -206,6 +227,7 @@

     @packageDependencies

+  # Public: Get an array of all the available package paths.
   getAvailablePackagePaths: ->
     packagePaths = []

@@ -220,9 +242,11 @@

     _.uniq(packagePaths)

+  # Public: Get an array of all the available package names.
   getAvailablePackageNames: ->
     _.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath)

+  # Public: Get an array of all the available package metadata.
   getAvailablePackageMetadata: ->
     packages = []
     for packagePath in @getAvailablePackagePaths()


Reorder methods

Reorder methods

--- lib/suggestion-list-element.coffee
+++ lib/suggestion-list-element.coffee
@@ -254,6 +254,16 @@
     return text unless text?.length and text.indexOf('$') isnt -1 # No snippets
     text.replace(@emptySnippetGroupRegex, '') # Remove all occurrences of $0 or ${0} or ${0:}

+  removeSnippetsFromText: (snippets, text) ->
+    return text unless text.length and snippets?.length
+    index = 0
+    result = ''
+    for {snippetStart, snippetEnd, body} in snippets
+      result += text.slice(index, snippetStart) + body
+      index = snippetEnd + 1
+    result += text.slice(index, text.length) if index isnt text.length
+    result
+
   findSnippetIndices: (snippets) ->
     return unless snippets?
     indices = {}
@@ -272,16 +282,6 @@
         indices[endIndex] = SnippetEnd

     indices
-
-  removeSnippetsFromText: (snippets, text) ->
-    return text unless text.length and snippets?.length
-    index = 0
-    result = ''
-    for {snippetStart, snippetEnd, body} in snippets
-      result += text.slice(index, snippetStart) + body
-      index = snippetEnd + 1
-    result += text.slice(index, text.length) if index isnt text.length
-    result

   findCharacterMatcheIndices: (text, replacementPrefix) ->
     return unless text?.length and replacementPrefix?.length


Composite the result frames into the scene

The alignment is way off, though

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -1,6 +1,7 @@
 # Scaffolds for AYP
 # That good, handmade shit.

+fs = require 'fs'
 path = require 'path'
 Url = require 'url'
 Redis = require 'redis'
@@ -33,7 +34,9 @@

         console.log " => Built image: #{image.width}x#{image.height}"
         # TODO: Temp file name
-        image.savePng path.resolve(ROOT, "out.png"), 0, (err) ->
+        outPath = path.resolve(ROOT, "out.png")
+        try fs.unlinkSync(outPath) # lol
+        image.savePng outPath, 0, (err) ->
           return console.error "Failed to write result:", err if err
           msg.reply "TODO: Upload result: #{image.width}x#{image.height}"

@@ -56,7 +59,19 @@
     # to vary the loader to accommodate.
     GD.openPng path.resolve(BG_BASE, "b29.png"), (err, bg) ->
       return cb(err, bg) if err
-      # TODO: Composite `panels` onto `bg`
+      # TODO: Composite `panels` onto `bg` correctly
+      left = 0
+      top = 6
+      for panel in panels
+        do (panel) ->
+          panelDims = [panel.width, panel.height]
+          panel.copyResampled bg,
+            (left += 6), top, # dst
+            0, 0,             # src
+            panelDims..., panelDims... # We keep the same size
+
+          # `left` has to grow by the width of the panel, also
+          left += panel.width
       cb(false, bg)

 # Turn a set of 6 `lines` into 3 panels


dashboard : generalsettings fields added to view regardless of group slug

--- client/Dashboard/views/generalsettingsview.coffee
+++ client/Dashboard/views/generalsettingsview.coffee
@@ -22,10 +22,10 @@
     #   @settingsForm.buttons.Remove.hide()

   createFormFields: (formOptions) ->
-    group = @getData()
+    group              = @getData()
+    formOptions.fields = {}

     if group.slug isnt "koding"
-      formOptions.fields      = {}
       formOptions.fields.Logo =
         label                 : "Logo"
         itemClass             : GroupLogoSettings


AddManagedVMModal: make ui browserified and fix style

--- client/app/lib/providers/addmanagedvmmodal.coffee
+++ client/app/lib/providers/addmanagedvmmodal.coffee
@@ -1,67 +1,67 @@
 kd = require 'kd'
-KDView = kd.View
-KDModalView = kd.ModalView
-KDLoaderView = kd.LoaderView

-class AddManagedVMModal extends KDModalView
+States = { 'Initial', 'ListKites', 'FailedToConnect' }

-  view             =
-    header         : (title) -> new KDView
-      partial      : title
-      cssClass     : 'view-header'
+INSTALL_INSTRUCTIONS = """
+  $ curl https://kd.io/kites/klient/latest | bash - </br>
+  # Enter your koding.com credentials when asked for
+"""

-    code           : (code) -> new KDView
-      partial      : code
-      cssClass     : 'view-code'
+view             =

-    waiting        : (title) ->
+  header         : (title) -> new kd.View
+    partial      : title
+    cssClass     : 'view-header'

-      container    = new KDView
-        cssClass   : 'view-waiting'
+  code           : (code) -> new kd.View
+    partial      : code
+    cssClass     : 'view-code'

-      container.addSubView new KDLoaderView
-        showLoader : yes
-        size       :
-          width    : 20
-          height   : 20
+  waiting        : (title) ->

-      container.addSubView new KDView
-        partial    : title
-        cssClass   : 'title'
+    container    = new kd.View
+      cssClass   : 'view-waiting'

-      return container
+    container.addSubView new kd.LoaderView
+      showLoader : yes
+      size       :
+        width    : 20
+        height   : 20

-    list           : (data) ->
+    container.addSubView new kd.View
+      partial    : title
+      cssClass   : 'title'

-      new KDView partial: data
+    return container

-  addFollowingsTo = (parent, views)->
+  list           : (data) ->

-    map = {}
-    for own key, value of views
-      value = [value]  unless Array.isArray value
-      parent.addSubView map[key] = view[key] value...
-
-    return map
+    new kd.View partial: data

-  States = { 'Initial', 'ListKites', 'FailedToConnect' }
+addFollowingsTo = (parent, views)->
+
+  map = {}
+  for own key, value of views
+    value = [value]  unless Array.isArray value
+    parent.addSubView map[key] = view[key] value...
+
+  return map

-  INSTALL_INSTRUCTIONS = """
-    $ curl https://kd.io/kites/klient/latest | bash - </br>
-    # Enter your koding.com credentials when asked for
-  """
+module.exports = class AddManagedVMModal extends kd.ModalView

   constructor: (options = {}, data) ->

-    options   =
-      title   : 'Add your own VM'
-      width   : 640
+    options    =
+      title    : 'Add your own VM'
+      width    : 640
+      cssClass : 'managed-vm modal'

     super options, data

-    @addSubView @container = new KDView
+    @addSubView @container = new kd.View
+

   viewAppended: ->


Move aloha cleanup code to onClose method.


storage: do not force mounting of snapshots every few minutes anymore (instead rely on ZFS parameter)

--- src/smc-hub/storage.coffee
+++ src/smc-hub/storage.coffee
@@ -1363,7 +1363,7 @@
                             dbg("SUCCESS: task_mount_snapshots_on_all_compute_vms")
                         cb?(err)

-            task_ensure_zfs_snapshots_stay_mounted = (cb) ->
+            task_ensure_zfs_snapshots_are_mounted = (cb) ->
                 misc_node.execute_code
                     command : "ls /projects/.zfs/snapshot/*/XXX"
                     bash    : true
@@ -1387,16 +1387,15 @@
             setInterval(task_update_snapshots, 1000*60*5)
             task_update_snapshots()

-            # ensure ZFS snapshots stay mounted every 3 minutes
-            setInterval(task_ensure_zfs_snapshots_stay_mounted, 1000*60*3)
-
-            # also do this on startup:
-            task_ensure_zfs_snapshots_stay_mounted () =>
+            # mount all of the ZFS snapshots
+            # they should stay mounted due to
+            #      echo "options zfs zfs_expire_snapshot=99999999" >> /etc/modprobe.d/zfs.conf
+            task_ensure_zfs_snapshots_are_mounted () ->
                 task_mount_snapshots_on_all_compute_vms()

             cb()
-    ], (err) =>
+    ], (err) ->
         if err
             dbg("error -- #{err}")
             process.exit(1)


check for project access before even trying to make a websocket to the project

--- src/smc-util/client.coffee
+++ src/smc-util/client.coffee
@@ -232,7 +232,12 @@
         @call_callbacks    = {}
         @_project_title_cache = {}
         @_usernames_cache = {}
-        @_redux = undefined # set this if you want to be able to use mark_file
+
+        # Browser client should set @_redux, since this
+        # is used in a few ways:
+        #   - to be able to use mark_file
+        #   - raising an error on attempt to get project_websocket for non-collab
+        @_redux = undefined

         @register_data_handler(JSON_CHANNEL, @handle_json_data)

@@ -1442,6 +1447,9 @@
     # File Management
     #################################################
     project_websocket: (project_id) =>
+        group = @_redux?.getStore('projects')?.get_my_group(project_id)
+        if not group? or group == 'public'
+            throw Error("no access to project websocket")
         return await require('smc-webapp/project/websocket/connect').connection_to_project(project_id)

     project_directory_listing: (opts) =>


schema; add indexes to make searching for blobs that need to be updated to gcloud or backed up trivial (instead of very hard)

--- src/smc-util/schema.coffee
+++ src/smc-util/schema.coffee
@@ -284,7 +284,9 @@
             type : 'timestamp'
             desc : 'if true, then this blob was saved to an offsite backup at the given time'
     indexes:
-        expire : []
+        expire : []   # when expired
+        needs_gcloud : [(x) -> x.hasFields('expire').not().and(x.hasFields('gcloud').not())]  # never-expiring blobs that haven't been uploaded to gcloud  -- find via .getAll(true, index:'needs_gcloud')
+        needs_backup : [(x) -> x.hasFields('expire').not().and(x.hasFields('backup').not())]  # never-expiring blobs that haven't been backed up offsite -- find via .getAll(true, index:'needs_backup')

 schema.central_log =
     desc : 'Table for logging system stuff that happens.  Meant to help in running and understanding the system better.'


make it so the react TimeAgo component has hover text by default

--- src/smc-webapp/r_misc.cjsx
+++ src/smc-webapp/r_misc.cjsx
@@ -21,7 +21,7 @@
 async = require('async')

 {React, ReactDOM, rclass, rtypes, is_redux, is_redux_actions} = require('./smc-react')
-{Alert, Button, ButtonToolbar, Col, Input, OverlayTrigger, Popover, Row, Well} = require('react-bootstrap')
+{Alert, Button, ButtonToolbar, Col, Input, OverlayTrigger, Popover, Tooltip, Row, Well} = require('react-bootstrap')
 {HelpEmailLink, SiteName, CompanyName, PricingUrl, PolicyTOSPageUrl, PolicyIndexPageUrl, PolicyPricingPageUrl} = require('./customize')

 Combobox = require('react-widgets/lib/Combobox')
@@ -442,15 +442,28 @@
     displayName : 'Misc-TimeAgo'

     propTypes :
-        placeholder : rtypes.number
+        popover     : rtypes.bool
+        placement   : rtypes.string

     getDefaultProps: ->
+        popover   : true
         minPeriod : 45000
+        placement : 'top'
         # critical to use minPeriod>>1000, or things will get really slow in the client!!
         # Also, given our custom formatter, anything more than about 45s is pointless (since we don't show seconds)

+    render_timeago: (d) ->
+        <TimeAgo date={d} style={@props.style} formatter={timeago_formatter} minPeriod={@props.minPeriod} />
+
     render: ->
-        <TimeAgo date={@props.date} style={@props.style} formatter={timeago_formatter} minPeriod={@props.minPeriod} />
+        d = if misc.is_date(@props.date) then @props.date else new Date(@props.date)
+        if @props.popover
+            s = d.toLocaleString()
+            <Tip title={s} id={s} placement={@props.placement}>
+                {@render_timeago(d)}
+            </Tip>
+        else
+            @render_timeago(d)

 # Important:
@@ -695,7 +708,7 @@
     propTypes :
         title     : rtypes.oneOfType([rtypes.string, rtypes.node]).isRequired
         placement : rtypes.string   # 'top', 'right', 'bottom', left' -- defaults to 'right'
-        tip       : rtypes.oneOfType([rtypes.string, rtypes.node]).isRequired
+        tip       : rtypes.oneOfType([rtypes.string, rtypes.node])
         size      : rtypes.string   # "xsmall", "small", "medium", "large"
         delayShow : rtypes.number
         icon      : rtypes.string
@@ -709,15 +722,22 @@
         {<Icon name={@props.icon}/> if @props.icon} {@props.title}

     render_popover : ->
-        <Popover
-            bsSize = {@props.size}
-            title  = {@render_title()}
-            id     = {@props.id ? "tip"}
-            >
-
-                {@props.tip}
-
-        </Popover>
+        if @props.tip
+            <Popover
+                bsSize = {@props.size}
+                title  = {@render_title()}
+                id     = {@props.id ? "tip"}
+                >
+
+                    {@props.tip}
+
+            </Popover>
+        else
+            <Tooltip
+                bsSize = {@props.size}
+                id     = {@props.id ? "tip"} >
+                {@render_title()}
+            </Tooltip>

     render : ->
         <OverlayTrigger


Utils: way better domain tester

Utils: way better domain tester


flinch chance is optionally 0

flinch chance is optionally 0

--- server/move.coffee
+++ server/move.coffee
@@ -18,7 +18,7 @@
     @spectra = attributes.damage || '???'
     @chLevel = attributes.criticalHitLevel || 1
     @flags = attributes.flags
-    @flinchChance = attributes.flinchChance / 100
+    @flinchChance = (attributes.flinchChance || 0) / 100
     @pp = attributes.pp

   isPhysical: =>


Workspace: Compare by instance identifiers

--- client/ide/routes.coffee
+++ client/ide/routes.coffee
@@ -100,7 +100,7 @@

     for instance in ideApps.instances
       isSameMachine   = instance.mountedMachineUId is machineUId
-      isSameWorkspace = instance.workspaceData is workspace
+      isSameWorkspace = instance.workspaceData.getId() is workspace.getId()

       if isSameMachine
         if isSameWorkspace then ideInstance = instance


Refixes housing table searching and sorting

--- app/assets/javascripts/housing_locations.js.coffee
+++ app/assets/javascripts/housing_locations.js.coffee
@@ -6,7 +6,7 @@
     "columnDefs": [
       {
         "orderable": false,
-        "targets": [2,3,7,8]
+        "targets": [5,6,7,8]
       },
       {
         "searchable": false,
@@ -22,14 +22,15 @@
     "columnDefs": [
       {
         "orderable": false,
-        "targets": [2,3]
+        "targets": [5,6,7,8]
       },
       {
         "searchable": false,
-        "targets": [2,3,4,5,6]
+        "targets": [2,3,4,5,6,7,8]
       }
     ],
     "dom": '<"wrapper"ftpr>'
   })
+

   $('div.dataTables_filter input').focus()


aac: Add getChannelsByChannelConfiguration()

--- aac.coffee
+++ aac.coffee
@@ -279,6 +279,18 @@
       when 8 then 7
       else
         throw new Error "#{channels} channels audio is not supported"
+
+  getChannelsByChannelConfiguration: (channelConfiguration) ->
+    switch channelConfiguration
+      when 1 then 1
+      when 2 then 2
+      when 3 then 3
+      when 4 then 4
+      when 5 then 5
+      when 6 then 6
+      when 7 then 8
+      else
+        throw new Error "Channel configuration #{channelConfiguration} is not supported"

   # @param opts: {
   #   frameLength (int): 1024 or 960


Began converting dict to adjust for the fact that we always are working with pythonscript types

--- src/builtin/dict.coffee
+++ src/builtin/dict.coffee
@@ -2,16 +2,17 @@
   constructor: (iterable) ->
     @value = {}
     if iterable?
-      for key in iterable # TODO: Coffeescript uses in when using array, of when using object
-        @value[key] = iterable[key]
+      for key, value of iterable # TODO: Coffeescript uses in when using array, of when using object
+        @value[key] = value

   # Checks if the key exists in this
   __contains__: (key) ->
-    return key of @value
+    return new Bool key of @value

   # Removes the key,value pair from this
   __delitem__: (key) ->
     delete @value[key]
+    return

   __eq__: (dict) ->
     keys = Object.keys(@value)
@@ -19,16 +20,16 @@
     set = new Set(keys)
     otherSet = new Set(otherKeys)
     if set.__ne__(otherSet) #if keys are different, not equal
-      return false
+      return new Bool false
     for key in keys
       val = @__getitem__(key)
       otherVal = dict.__getitem__(key)
       if val != otherVal #if values differ, not equal. Unsure if this should be __ne__
-        return false
-    return true
+        return new Bool false
+    return new Bool true

   __ge__: (dict) ->
-    return @__gt__(dict) or @__eq__(dict)
+    return new Bool @__gt__(dict) or @__eq__(dict)

   __getattribute__: (attr) ->

@@ -37,7 +38,7 @@
     return @value[key]

   __gt__: (dict) ->
-    return not @__le__(dict)
+    return new Bool not @__le__(dict)

   # **Unimplemented**
   __init__: ->
@@ -46,31 +47,31 @@
     return new DictionaryKeyIterator(@)

   __le__: (dict) ->
-    return @__lt__(dict) or @__eq__(dict)
+    return new Bool @__lt__(dict) or @__eq__(dict)

   __len__: ->
-    return Object.keys(Object(@value)).length
+    return new Int Object.keys(Object(@value)).length

   # First checks length of dictionary, then compares sorted keys and their associated values
   __lt__: (dict) ->
-    if @__len__() < dict.__len__()
-      return true
-    else if @__len__() > dict.__len__()
-      return false
+    if @__len__().value < dict.__len__().value
+      return new Bool true
+    else if @__len__().value > dict.__len__().value
+      return new Bool false
     else
       keys = Object.keys(@value).sort()
       otherKeys = Object.keys(dict.value).sort()
       for key,i in keys
         if key >= otherKeys[i]
-          return false
-        val = @__getitem__(key)
-        otherVal = @__getitem__(otherKeys[i])
+          return new Bool false
+        val = @__getitem__ key
+        otherVal = @__getitem__ otherKeys[i]
         if val >= otherVal
-          return false
-      return true
+          return new Bool false
+      return new Bool true

   __ne__: (dict) ->
-    return not @__eq__(dict)
+    return new Bool not @__eq__ dict

   # **Unimplemented**
   __repr__: ->
@@ -100,14 +101,14 @@
       return d

   has_key: (key) ->
-    return key in @value
+    return new Bool key of @value

   items: ->
-    items = [] #Unsure if this should be JS list or Pythonscript List
+    items = new List()
     for key of @value
-      val = @__getitem__(key)
-      tuple = new Tuple([key, val])
-      items.push(tuple)
+      val = @__getitem__ key
+      tuple = new Tuple [key, val]
+      items.append tuple
     return items

   iteritems: ->
@@ -121,32 +122,35 @@

   # Returns a list of keys of this
   keys: ->
-    return Object.keys(@value)
+    keys = new List Object.keys(@value)
+    return keys

   # Returns the popped value at key if key exists.
   # Else returns `d` if default `d` exists
   # Else raises `KeyError`
   pop: (key, d) ->
-    if key in @keys()
+    keys = @keys()
+    if keys.__contains__(key)
       val = @value[key]
       delete @value[key]
       return val
     else if d?
       return d
     else
-      (new KeyError "#{key}").raise()
+      raise new KeyError "#{key}"

   # Pops random item from this and returns a 2-tuple
   popitem: ->
-    if @__len__() == 0
-      (new KeyError "popitem(): dictionary is empty").raise()
-    randomKey = @keys[0]
-    randomValue = @__getitem__(randomKey)
-    return new Tuple([randomKey, randomValue])
+    if @__len__().value == 0
+      raise new KeyError "popitem(): dictionary is empty"
+    keys = @keys()
+    randomKey = keys.__getitem__ 0
+    randomValue = @__getitem__ randomKey
+    return new Tuple [randomKey, randomValue]

   setdefault: (key, d) ->
-    if key not in @keys()
-      @__setitem__(key, d)
+    if key not in @keys().value
+      @__setitem__ key, d
     return @value[key]

   # **Unimplemented**
@@ -155,10 +159,10 @@
   # Returns a list of values of this
   values: ->
     keys = @keys()
-    values = []
-    for key in keys
+    values = new List()
+    for key in keys.value
       val = @__getitem__(key)
-      values.push(val)
+      values.append(val)
     return values

   # **Unimplemented**


Fix missing console output for regular files

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -2626,6 +2626,7 @@
                   continue
                 else
                   ## Regular file
+                  console.log '*', file
                   @maybeInit()
                   input = inputRequire file, @settings
                   ###


Added global 'document' to prevent errors

Chaplin, in particular, makes assumptions about the presence of
'document' in the global scope. Eventually we may remove this but for
now it's an easy way to prevent Chaplin errors.

--- src/tb/index.coffee
+++ src/tb/index.coffee
@@ -1,3 +1,7 @@
+global = @
+
+global.document = {}
+
 module.exports =

   load: (options = {}) ->


ClampBounds docs

ClampBounds docs

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -25,7 +25,7 @@
   # => 2.5 # half the player's width

   player.I.y
-  # => 311.5 # The default App.height (320)
+  # => 311.5 # The default App.height (320) -


removed somaClicking  bug; fixes #481

removed somaClicking  bug; fixes #481

--- app/assets/javascripts/oxalis/model/settings/backbone_to_oxalis_adapter_model.coffee
+++ app/assets/javascripts/oxalis/model/settings/backbone_to_oxalis_adapter_model.coffee
@@ -39,7 +39,7 @@
       @skeletonTracingAdapter.set("particleSize", @oxalisModel.user.get("particleSize"))
       @skeletonTracingAdapter.deleteActiveNode = @skeletonTracingModel.deleteActiveNode.bind(@skeletonTracingModel)

-      if @oxalisModel.settings.somaClicking
+      if @oxalisModel.settings.somaClickingAllowed
         @skeletonTracingAdapter.set("somaClicking", @oxalisModel.user.get("newNodeNewTree"))

@@ -59,7 +59,7 @@
       )

       @listenTo(@skeletonTracingAdapter, "change:somaClicking", (model, bool) ->
-        @oxalisModel.user.set("newNodeNewTree")
+        @oxalisModel.user.set("newNodeNewTree", bool)
       )

       @listenTo(@skeletonTracingAdapter, "change:activeNodeId", (model, id) ->


Fix isTouch test

Fix isTouch test

--- src/coffee/glide.coffee
+++ src/coffee/glide.coffee
@@ -221,13 +221,11 @@
   #
   # Returns True if the device is touch enabled, else False.
   isTouch: =>
-    if typeof @touch is 'undefined'
-      if @isAndroid()
-        if !!('ontouchstart' of window)
-          @touch = true
+    if typeof @touch is "undefined"
+      if !!('ontouchstart' of window)
+        @touch = true
       else
-        if window.Touch?
-          @touch = true
+        @touch = false
     else
       @touch


Documented the View class

Documented the View class

--- www/src/theoricus/mvc/view.coffee
+++ www/src/theoricus/mvc/view.coffee
@@ -7,6 +7,8 @@
 Factory = null

 ###*
+  The View class is responsible for manipulating the templates (DOM).
+
   @class View
 ###
 module.exports = class View
@@ -20,7 +22,7 @@
   title: null

   ###*
-    The template's html as jQuery object.
+    Stores template's html as jQuery object.

     @property el {Object}
   ###
@@ -36,14 +38,14 @@

   # @property [String] class name
   ###*
-    Stores class name
+    Stores the class name

     @property classname {String}
   ###
   classname : null

   ###*
-    Namespace is the class folder path relative to the views folder.
+    Namespace is the folder path relative to the `views` folder.

     @property namespace {String}
   ###
@@ -57,7 +59,7 @@
   process   : null

   ###*
-    Responsible for binding the DOM events on the view. Use the format `selector event: handler` to define an event.
+    Object responsible for binding the DOM events on the view. Use the format `selector event: handler` to define an event. It is called after the `template` was rendered in the document.

     @property events {Object}
     @example
@@ -122,7 +124,7 @@

   ###*
-    Responsible for loading the given template, and appending it to {{#crossLink "View"}}{{/crossLink}} `el` element.
+    Responsible for loading the given template, and appending it to view's `el` element.

     @method render_template
     @param template {String} Path to the template to be rendered.
@@ -148,7 +150,9 @@
         @on_resize()

   ###*
-    If there is an `after_render` method implemented, it will be executed after the view's template is appended to the document. Useful for caching DOM elements as jQuery objects.
+    If there is an `after_render` method implemented, it will be executed after the view's template is appended to the document.
+
+    Useful for caching DOM elements as jQuery objects.

     @method after_render
     @param data {Object} Reference to the `@data`
@@ -175,7 +179,9 @@
       ( @el.find sel ).bind   ev, null, @[funk]

   ###*
-    If there is a `@before_in` method implemented, it will be called before the view execute it's intro animations. Useful to setting up the DOM elements properties before animating them.
+    If there is a `@before_in` method implemented, it will be called before the view execute its intro animations.
+
+    Useful to setting up the DOM elements properties before animating them.

     @method before_in
   ###
@@ -183,7 +189,7 @@
   ###*
     The `in` method is where the view intro animations are defined. It is executed after the `@after_render` method.

-    The `@after_in` method must be called at the end of the animations, to Theoricus knows that the View finished animating.
+    The `@after_in` method must be called at the end of the animations, so Theoricus knows that the View finished animating.

     @method in
   ###
@@ -198,16 +204,26 @@
       @el.animate {opacity: 1}, 300, => @after_in?()

   ###*
-    If there is an`@after_in` method implemented, it will be called after the view finish it's intro animations.
+    If there is an`@after_in` method implemented, it will be called after the view finish its intro animations.

     Will only be executed if the {{#crossLink "Config"}}{{/crossLink}} property `disable_transitions` is `false`.

     @method after_in
   ###

-  ###
-  Triggers view "animation out", "after_out" must be called in the end
-   @param [Function] after_out Callback function to be triggered in the end
+  ###*
+    If there is an`@before_out` method implemented, it will be called before the view executes its exit animations.
+
+    @method before_out
+  ###
+
+  ###*
+    The `@out` method is responsible for the view's exit animations.
+
+    At the end of the animations, the `after_out` callback must be called.
+
+    @method out
+    @param after_out {Function} Callback to be called when the animation ends.
   ###
   out:( after_out )->
     @before_out?()
@@ -218,11 +234,20 @@
     else
       @el.animate {opacity: 0}, 300, after_out

-  ###
-  Destroy the view after the 'out' animation, the default behavior is to just
-  empty it's container element.
-
-  before_destroy will be called just before emptying it.
+  ###*
+    If there is an`@before_destroy` method implemented, it will be called before removing the view's template from the document.
+
+    @method before_destroy
+  ###
+
+  ###*
+    Destroy the view after executing the `@out` method, the default behaviour empties its `el` element and unbind the `window.resize` event.
+
+    If overwritten, the `super` method must be called.
+
+    Useful for removing variables assignments that needs to be removed from memory by the Garbage Collector, avoiding Memory Leaks.
+
+    @method destroy
   ###
   destroy: () ->
     if @on_resize?
@@ -233,10 +258,13 @@

   # ~> Shortcuts

-  ###
-  Shortcut for application navigate
-
-  @param [String] url URL to navigate
+  ###*
+    Shortcut for application navigate.
+
+    Navigate to the given URL.
+
+    @method navigate
+    @param url {String} URL to navigate to.
   ###
   navigate:( url )->
     @the.processes.router.navigate url


fix infinite persona logout cycle

fix infinite persona logout cycle

--- www/src/views/user/current.coffee
+++ www/src/views/user/current.coffee
@@ -47,6 +47,7 @@

         setPersonaWatch: ->
             user = @model.get("persona_user")
+            user = null unless user?

             navigator.id.watch
                 loggedInUser: user


Refactored main container caching

Refactored main container caching

--- static/js/live_logs.coffee
+++ static/js/live_logs.coffee
@@ -3,23 +3,24 @@
 		# Root container of all elements
 		container: "#liveLogsContainer"
 	}
+	# Root container
+	container: null
+
+	constructor: ->
+		@container= $(@opts.container)

 	init: ->
 		# On browser resize keep root container size equal
 		$(window).resize =>
-			@getContainer().width $(window).width()
-			@getContainer().height $(window).height()
-		@getContainer().show()
+			@container.width $(window).width()
+			@container.height $(window).height()
+		@container.show()
 		# Subscribe to new log event
 		PubSub.subscribe "log_message", (type, data) =>
-			console.log data
 			@appendMessage data.message

 	appendMessage: (message) ->
-		@getContainer().html(message)
-
-	getContainer: ->
-		$(@opts.container)
+		@container.html(message)

 $ ->
 	$("#live_logs").click ->


bot won't reply in DMs unless command matches

--- lib/bot.coffee
+++ lib/bot.coffee
@@ -156,9 +156,9 @@

 controller.on "direct_message", (bot, message) ->
   if message.text.indexOf("/") != 0
-    processCommand(bot, message)
-
-processCommand = (bot, message) ->
+    processCommand(bot, message, true)
+
+processCommand = (bot, message, isDM = false) ->

   context = new ReplyContext(defaultBot, bot, message)

@@ -217,7 +217,8 @@
       if newVersion
         help += "\n\n:scream: *<#{newVersion.html_url}|The Looker Slack integration is out of date! Version #{newVersion.tag_name} is now available.>* :scream:"

-      context.replyPrivate({text: help, parse: "none", attachments: []})
+      if !isDM || message.text.toLowerCase() == "help"
+        context.replyPrivate({text: help, parse: "none", attachments: []})

     refreshCommands()


object fixes

object fixes

--- lib/esl.coffee
+++ lib/esl.coffee
@@ -406,13 +406,13 @@
         when 'disconnect'  then @end()

   start_call_server: ->
-    @connect (req,res) ->
+    @connect (req,res) =>
       unique_id = req.body['Unique-ID']
       @auto_cleanup
       # Handle the incoming connection
-      @linger (req,res) ->
-        @filter Unique_ID, unique_id, (req,res) ->
-          @event_json 'ALL', (req,res) ->
+      @linger (req,res) =>
+        @filter Unique_ID, unique_id, (req,res) =>
+          @event_json 'ALL', (req,res) =>
             @emit 'CONNECT', req, res

 #### Connection Listener (socket events handler)


fix redis port

fix redis port

--- app.coffee
+++ app.coffee
@@ -4,12 +4,12 @@
 less = require 'less'
 fs = require 'fs'
 _ = require 'underscore'
-redis  = require "redis"
+redis  = require "redis-url".connect(process.env.REDISTOGO_URL)

 app = express.createServer()
 app.use assets()
 app.use express.bodyParser()
-client = redis.createClient()
+client = redis.createClient(process.env.REDISTOGO_URL)

 app.set 'view engine', 'ejs'


Add missing /g flag to regular expression

--- app.coffee
+++ app.coffee
@@ -137,8 +137,8 @@
 		result

 	comparison = comparison.replace /\$v(==|<=|>=|<|>)([0-9]+\.[0-9]+\.[0-9]+.-?[0-9]+.[0-9]+)/g, (comparison, operator, v2) -> """(comparatorHelper($v, "#{v2}") #{operator} 0)"""
-	comparison = comparison.replace /\$v~(\/(?:[^/\\]|\\.)+\/i?)/, (comparison, regex) -> """(#{regex}.test($v))"""
-	comparison = comparison.replace /\$v!~(\/(?:[^/\\]|\\.)+\/i?)/, (comparison, regex) -> """(!#{regex}.test($v))"""
+	comparison = comparison.replace /\$v~(\/(?:[^/\\]|\\.)+\/i?)/g, (comparison, regex) -> """(#{regex}.test($v))"""
+	comparison = comparison.replace /\$v!~(\/(?:[^/\\]|\\.)+\/i?)/g, (comparison, regex) -> """(!#{regex}.test($v))"""

 	comparator = new Function '$v', 'comparatorHelper', 'return ' + comparison


On configuration file change, wait up to 100ms for file to be relinked

This fixes a problem where switching git branches (among other
operations) caused vee to crash as the .vee file was momentarily
unlinked.

--- app.coffee
+++ app.coffee
@@ -24,8 +24,22 @@

 watch = (file) ->
   fs.watch file, {persistent: false}, ->
-    console.log "A config file changed, restarting".yellow
-    restart()
+    waitForFileToExist file, ->
+      console.log "A config file changed, restarting".yellow
+      restart()
+
+waitForFileToExist = (file, callback) ->
+  start = +(new Date)
+  waitTime = 100
+  do checkFile = ->
+    fs.exists file, (exists) ->
+      if exists
+        callback()
+      else if +(new Date) - start < waitTime
+        process.nextTick checkFile
+      else
+        console.error ".vee configuration file not found within #{waitTime}ms".red
+        process.exit(1)

 loadCfg = (file) ->
   cfg = fs.readFileSync(file).toString('utf8')