Add more specs

Add more specs

--- spec/text-buffer-spec.coffee
+++ spec/text-buffer-spec.coffee
@@ -2,11 +2,15 @@
 {readFileSync} = fs
 {join} = require 'path'
 temp = require 'temp'
+{File} = require 'pathwatcher'
 TextBuffer = require '../src/text-buffer'
 SampleText = readFileSync(join(__dirname, 'fixtures', 'sample.js'), 'utf8')

 describe "TextBuffer", ->
   buffer = null
+
+  afterEach ->
+    buffer = null

   describe "construction", ->
     it "can be constructed empty", ->
@@ -553,6 +557,79 @@
       bufferB.undo()
       expect(bufferB.getText()).toBe "hello\nworld\r\nhow are you doing?"

+    describe "when the buffer has a path", ->
+      [filePath, buffer2] = []
+
+      beforeEach ->
+        filePath = temp.openSync('atom').path
+        fs.writeFileSync(filePath, "words")
+        buffer = new TextBuffer({filePath, load: true})
+
+        waitsFor ->
+          buffer.loaded
+
+      afterEach ->
+        buffer2?.destroy()
+
+      describe "when the serialized buffer had no unsaved changes", ->
+        it "loads the current contents of the file at the serialized path", ->
+          expect(buffer.isModified()).toBeFalsy()
+          buffer2 = buffer.testSerialization()
+          buffer2.load()
+
+          waitsFor ->
+            buffer2.loaded
+
+          runs ->
+            expect(buffer2.isModified()).toBeFalsy()
+            expect(buffer2.getPath()).toBe(buffer.getPath())
+            expect(buffer2.getText()).toBe(buffer.getText())
+
+      describe "when the serialized buffer had unsaved changes", ->
+        describe "when the disk contents were changed since serialization", ->
+          it "loads the disk contents instead of the previous unsaved state", ->
+            buffer.setText("BUFFER CHANGE")
+            fs.writeFileSync(filePath, "DISK CHANGE")
+
+            buffer2 = buffer.testSerialization()
+
+            waitsFor ->
+              buffer2.cachedDiskContents
+
+            runs ->
+              expect(buffer2.getPath()).toBe(buffer.getPath())
+              expect(buffer2.getText()).toBe("DISK CHANGE")
+              expect(buffer2.isModified()).toBeFalsy()
+
+        describe "when the disk contents are the same since serialization", ->
+          it "restores the previous unsaved state of the buffer", ->
+            previousText = buffer.getText()
+            buffer.setText("abc")
+
+            buffer2 = buffer.testSerialization()
+            buffer2.load()
+
+            waitsFor ->
+              buffer2.loaded
+
+            runs ->
+              expect(buffer2.getPath()).toBe(buffer.getPath())
+              expect(buffer2.getText()).toBe(buffer.getText())
+              expect(buffer2.isModified()).toBeTruthy()
+              buffer2.setText(previousText)
+              expect(buffer2.isModified()).toBeFalsy()
+
+      describe "when the serialized buffer was unsaved and had no path", ->
+        it "restores the previous unsaved state of the buffer", ->
+          buffer.destroy()
+
+          buffer = new TextBuffer()
+          buffer.setText("abc")
+
+          buffer2 = buffer.testSerialization()
+          expect(buffer2.getPath()).toBeUndefined()
+          expect(buffer2.getText()).toBe("abc")
+
   describe "::getRange()", ->
     it "returns the range of the entire buffer text", ->
       buffer = new TextBuffer("abc\ndef\nghi")
@@ -915,3 +992,797 @@

       runs ->
         expect(buffer.isModified()).toBeFalsy()
+
+  describe ".getLines()", ->
+    it "returns an array of lines in the text contents", ->
+      filePath = require.resolve('./fixtures/sample.js')
+      fileContents = fs.readFileSync(filePath, 'utf8')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+      runs ->
+        expect(buffer.getLines().length).toBe fileContents.split("\n").length
+        expect(buffer.getLines().join('\n')).toBe fileContents
+
+  describe ".change(range, string)", ->
+    changeHandler = null
+
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      fileContents = fs.readFileSync(filePath, 'utf8')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+      runs ->
+        changeHandler = jasmine.createSpy('changeHandler')
+        buffer.on 'changed', changeHandler
+
+    describe "when used to insert (called with an empty range and a non-empty string)", ->
+      describe "when the given string has no newlines", ->
+        it "inserts the string at the location of the given range", ->
+          range = [[3, 4], [3, 4]]
+          buffer.change range, "foo"
+
+          expect(buffer.lineForRow(2)).toBe "    if (items.length <= 1) return items;"
+          expect(buffer.lineForRow(3)).toBe "    foovar pivot = items.shift(), current, left = [], right = [];"
+          expect(buffer.lineForRow(4)).toBe "    while(items.length > 0) {"
+
+          expect(changeHandler).toHaveBeenCalled()
+          [event] = changeHandler.argsForCall[0]
+          expect(event.oldRange).toEqual range
+          expect(event.newRange).toEqual [[3, 4], [3, 7]]
+          expect(event.oldText).toBe ""
+          expect(event.newText).toBe "foo"
+
+      describe "when the given string has newlines", ->
+        it "inserts the lines at the location of the given range", ->
+          range = [[3, 4], [3, 4]]
+
+          buffer.change range, "foo\n\nbar\nbaz"
+
+          expect(buffer.lineForRow(2)).toBe "    if (items.length <= 1) return items;"
+          expect(buffer.lineForRow(3)).toBe "    foo"
+          expect(buffer.lineForRow(4)).toBe ""
+          expect(buffer.lineForRow(5)).toBe "bar"
+          expect(buffer.lineForRow(6)).toBe "bazvar pivot = items.shift(), current, left = [], right = [];"
+          expect(buffer.lineForRow(7)).toBe "    while(items.length > 0) {"
+
+          expect(changeHandler).toHaveBeenCalled()
+          [event] = changeHandler.argsForCall[0]
+          expect(event.oldRange).toEqual range
+          expect(event.newRange).toEqual [[3, 4], [6, 3]]
+          expect(event.oldText).toBe ""
+          expect(event.newText).toBe "foo\n\nbar\nbaz"
+
+    describe "when used to remove (called with a non-empty range and an empty string)", ->
+      describe "when the range is contained within a single line", ->
+        it "removes the characters within the range", ->
+          range = [[3, 4], [3, 7]]
+          buffer.change range, ""
+
+          expect(buffer.lineForRow(2)).toBe "    if (items.length <= 1) return items;"
+          expect(buffer.lineForRow(3)).toBe "     pivot = items.shift(), current, left = [], right = [];"
+          expect(buffer.lineForRow(4)).toBe "    while(items.length > 0) {"
+
+          expect(changeHandler).toHaveBeenCalled()
+          [event] = changeHandler.argsForCall[0]
+          expect(event.oldRange).toEqual range
+          expect(event.newRange).toEqual [[3, 4], [3, 4]]
+          expect(event.oldText).toBe "var"
+          expect(event.newText).toBe ""
+
+      describe "when the range spans 2 lines", ->
+        it "removes the characters within the range and joins the lines", ->
+          range = [[3, 16], [4, 4]]
+          buffer.change range, ""
+
+          expect(buffer.lineForRow(2)).toBe "    if (items.length <= 1) return items;"
+          expect(buffer.lineForRow(3)).toBe "    var pivot = while(items.length > 0) {"
+          expect(buffer.lineForRow(4)).toBe "      current = items.shift();"
+
+          expect(changeHandler).toHaveBeenCalled()
+          [event] = changeHandler.argsForCall[0]
+          expect(event.oldRange).toEqual range
+          expect(event.newRange).toEqual [[3, 16], [3, 16]]
+          expect(event.oldText).toBe "items.shift(), current, left = [], right = [];\n    "
+          expect(event.newText).toBe ""
+
+      describe "when the range spans more than 2 lines", ->
+        it "removes the characters within the range, joining the first and last line and removing the lines in-between", ->
+          buffer.change [[3, 16], [11, 9]], ""
+
+          expect(buffer.lineForRow(2)).toBe "    if (items.length <= 1) return items;"
+          expect(buffer.lineForRow(3)).toBe "    var pivot = sort(Array.apply(this, arguments));"
+          expect(buffer.lineForRow(4)).toBe "};"
+
+    describe "when used to replace text with other text (called with non-empty range and non-empty string)", ->
+      it "replaces the old text with the new text", ->
+        range = [[3, 16], [11, 9]]
+        oldText = buffer.getTextInRange(range)
+
+        buffer.change range, "foo\nbar"
+
+        expect(buffer.lineForRow(2)).toBe "    if (items.length <= 1) return items;"
+        expect(buffer.lineForRow(3)).toBe "    var pivot = foo"
+        expect(buffer.lineForRow(4)).toBe "barsort(Array.apply(this, arguments));"
+        expect(buffer.lineForRow(5)).toBe "};"
+
+        expect(changeHandler).toHaveBeenCalled()
+        [event] = changeHandler.argsForCall[0]
+        expect(event.oldRange).toEqual range
+        expect(event.newRange).toEqual [[3, 16], [4, 3]]
+        expect(event.oldText).toBe oldText
+        expect(event.newText).toBe "foo\nbar"
+
+    it "allows a 'changed' event handler to safely undo the change", ->
+      buffer.once 'changed', -> buffer.undo()
+      buffer.change([0, 0], "hello")
+      expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
+
+  describe ".setText(text)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "changes the entire contents of the buffer and emits a change event", ->
+      lastRow = buffer.getLastRow()
+      expectedPreRange = [[0,0], [lastRow, buffer.lineForRow(lastRow).length]]
+      changeHandler = jasmine.createSpy('changeHandler')
+      buffer.on 'changed', changeHandler
+
+      newText = "I know you are.\nBut what am I?"
+      buffer.setText(newText)
+
+      expect(buffer.getText()).toBe newText
+      expect(changeHandler).toHaveBeenCalled()
+
+      [event] = changeHandler.argsForCall[0]
+      expect(event.newText).toBe newText
+      expect(event.oldRange).toEqual expectedPreRange
+      expect(event.newRange).toEqual [[0, 0], [1, 14]]
+
+  describe ".setTextViaDiff(text)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "can change the entire contents of the buffer when there are no newlines", ->
+      buffer.setText('BUFFER CHANGE')
+      newText = 'DISK CHANGE'
+      buffer.setTextViaDiff(newText)
+      expect(buffer.getText()).toBe newText
+
+    describe "with standard newlines", ->
+      it "can change the entire contents of the buffer with no newline at the end", ->
+        newText = "I know you are.\nBut what am I?"
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+      it "can change the entire contents of the buffer with a newline at the end", ->
+        newText = "I know you are.\nBut what am I?\n"
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+      it "can change a few lines at the beginning in the buffer", ->
+        newText = buffer.getText().replace(/function/g, 'omgwow')
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+      it "can change a few lines in the middle of the buffer", ->
+        newText = buffer.getText().replace(/shift/g, 'omgwow')
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+      it "can adds a newline at the end", ->
+        newText = buffer.getText() + '\n'
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+    describe "with windows newlines", ->
+      beforeEach ->
+        buffer.setText(buffer.getText().replace(/\n/g, '\r\n'))
+
+      it "adds a newline at the end", ->
+        newText = buffer.getText() + '\r\n'
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+      it "changes the entire contents of the buffer with smaller content with no newline at the end", ->
+        newText = "I know you are.\r\nBut what am I?"
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+      it "changes the entire contents of the buffer with smaller content with newline at the end", ->
+        newText = "I know you are.\r\nBut what am I?\r\n"
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+      it "changes a few lines at the beginning in the buffer", ->
+        newText = buffer.getText().replace(/function/g, 'omgwow')
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+      it "changes a few lines in the middle of the buffer", ->
+        newText = buffer.getText().replace(/shift/g, 'omgwow')
+        buffer.setTextViaDiff(newText)
+        expect(buffer.getText()).toBe newText
+
+  describe ".save()", ->
+    saveBuffer = null
+
+    afterEach ->
+      saveBuffer?.destroy()
+
+    describe "when the buffer has a path", ->
+      filePath = null
+
+      beforeEach ->
+        filePath = join(temp.dir, 'temp.txt')
+        fs.writeFileSync(filePath, "")
+        saveBuffer = new TextBuffer({filePath, load: true})
+
+        waitsFor ->
+          saveBuffer.loaded
+
+        runs ->
+          saveBuffer.setText("blah")
+
+      it "saves the contents of the buffer to the path", ->
+        saveBuffer.setText 'Buffer contents!'
+        saveBuffer.save()
+        expect(fs.readFileSync(filePath, 'utf8')).toEqual 'Buffer contents!'
+
+      it "fires will-be-saved and saved events around the call to fs.writeFileSync", ->
+        events = []
+        beforeSave1 = -> events.push('beforeSave1')
+        beforeSave2 = -> events.push('beforeSave2')
+        afterSave1 = -> events.push('afterSave1')
+        afterSave2 = -> events.push('afterSave2')
+
+        saveBuffer.on 'will-be-saved', beforeSave1
+        saveBuffer.on 'will-be-saved', beforeSave2
+        spyOn(File.prototype, 'write').andCallFake -> events.push 'File::write'
+        saveBuffer.on 'saved', afterSave1
+        saveBuffer.on 'saved', afterSave2
+
+        saveBuffer.save()
+        expect(events).toEqual ['beforeSave1', 'beforeSave2', 'File::write', 'afterSave1', 'afterSave2']
+
+      it "fires will-reload and reloaded events when reloaded", ->
+        events = []
+
+        saveBuffer.on 'will-reload', -> events.push 'will-reload'
+        saveBuffer.on 'reloaded', -> events.push 'reloaded'
+        saveBuffer.reload()
+        expect(events).toEqual ['will-reload', 'reloaded']
+
+      it "no longer reports being in conflict", ->
+        saveBuffer.setText('a')
+        saveBuffer.save()
+        saveBuffer.setText('ab')
+
+        fs.writeFileSync(saveBuffer.getPath(), 'c')
+        conflictHandler = jasmine.createSpy('conflictHandler')
+        saveBuffer.on 'contents-conflicted', conflictHandler
+
+        waitsFor ->
+          conflictHandler.callCount > 0
+
+        runs ->
+          expect(saveBuffer.isInConflict()).toBe true
+          saveBuffer.save()
+          expect(saveBuffer.isInConflict()).toBe false
+
+    describe "when the buffer has no path", ->
+      it "throws an exception", ->
+        saveBuffer = new TextBuffer({load: true})
+
+        waitsFor ->
+          saveBuffer.loaded
+
+        runs ->
+          saveBuffer.setText "hi"
+          expect(-> saveBuffer.save()).toThrow()
+
+  describe "reload()", ->
+    it "reloads current text from disk and clears any conflicts", ->
+      filePath = require.resolve('./fixtures/sample.js')
+      fileContents = fs.readFileSync(filePath, 'utf8')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+      runs ->
+        buffer.setText("abc")
+        buffer.conflict = true
+
+        buffer.reload()
+        expect(buffer.isModified()).toBeFalsy()
+        expect(buffer.isInConflict()).toBeFalsy()
+        expect(buffer.getText()).toBe(fileContents)
+
+  describe ".saveAs(path)", ->
+    [filePath, saveAsBuffer] = []
+
+    afterEach ->
+      saveAsBuffer?.destroy()
+
+    it "saves the contents of the buffer to the path", ->
+      filePath = join(temp.dir, 'temp.txt')
+      fs.removeSync(filePath)
+
+      saveAsBuffer = new TextBuffer()
+      eventHandler = jasmine.createSpy('eventHandler')
+      saveAsBuffer.on 'path-changed', eventHandler
+
+      saveAsBuffer.setText 'Buffer contents!'
+      saveAsBuffer.saveAs(filePath)
+      expect(fs.readFileSync(filePath, 'utf8')).toEqual 'Buffer contents!'
+
+      expect(eventHandler).toHaveBeenCalledWith(saveAsBuffer)
+
+    it "stops listening to events on previous path and begins listening to events on new path", ->
+      changeHandler = null
+      originalPath = join(temp.dir, 'original.txt')
+      newPath = join(temp.dir, 'new.txt')
+      fs.writeFileSync(originalPath, "")
+
+      saveAsBuffer = new TextBuffer({filePath: originalPath, load: true})
+
+      waitsFor ->
+        saveAsBuffer.loaded
+
+      runs ->
+        changeHandler = jasmine.createSpy('changeHandler')
+        saveAsBuffer.on 'changed', changeHandler
+        saveAsBuffer.saveAs(newPath)
+        expect(changeHandler).not.toHaveBeenCalled()
+
+        fs.writeFileSync(originalPath, "should not trigger buffer event")
+
+      waits 20
+
+      runs ->
+        expect(changeHandler).not.toHaveBeenCalled()
+        fs.writeFileSync(newPath, "should trigger buffer event")
+
+      waitsFor ->
+        changeHandler.callCount > 0
+
+  describe ".getTextInRange(range)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    describe "when range is empty", ->
+      it "returns an empty string", ->
+        range = [[1,1], [1,1]]
+        expect(buffer.getTextInRange(range)).toBe ""
+
+    describe "when range spans one line", ->
+      it "returns characters in range", ->
+        range = [[2,8], [2,13]]
+        expect(buffer.getTextInRange(range)).toBe "items"
+
+        lineLength = buffer.lineForRow(2).length
+        range = [[2,0], [2,lineLength]]
+        expect(buffer.getTextInRange(range)).toBe "    if (items.length <= 1) return items;"
+
+    describe "when range spans multiple lines", ->
+      it "returns characters in range (including newlines)", ->
+        lineLength = buffer.lineForRow(2).length
+        range = [[2,0], [3,0]]
+        expect(buffer.getTextInRange(range)).toBe "    if (items.length <= 1) return items;\n"
+
+        lineLength = buffer.lineForRow(2).length
+        range = [[2,10], [4,10]]
+        expect(buffer.getTextInRange(range)).toBe "ems.length <= 1) return items;\n    var pivot = items.shift(), current, left = [], right = [];\n    while("
+
+    describe "when the range starts before the start of the buffer", ->
+      it "clips the range to the start of the buffer", ->
+        expect(buffer.getTextInRange([[-Infinity, -Infinity], [0, Infinity]])).toBe buffer.lineForRow(0)
+
+    describe "when the range ends after the end of the buffer", ->
+      it "clips the range to the end of the buffer", ->
+        expect(buffer.getTextInRange([[12], [13, Infinity]])).toBe buffer.lineForRow(12)
+
+  describe ".scan(regex, fn)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "retunrns lineText and lineTextOffset", ->
+      matches = []
+      buffer.scan /current/, (match) ->
+        matches.push(match)
+      expect(matches.length).toBe 1
+
+      expect(matches[0].matchText).toEqual 'current'
+      expect(matches[0].lineText).toEqual '    var pivot = items.shift(), current, left = [], right = [];'
+      expect(matches[0].lineTextOffset).toBe 0
+
+  describe ".scanInRange(range, regex, fn)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    describe "when given a regex with a ignore case flag", ->
+      it "does a case-insensitive search", ->
+        matches = []
+        buffer.scanInRange /cuRRent/i, [[0,0], [12,0]], ({match, range}) ->
+          matches.push(match)
+        expect(matches.length).toBe 1
+
+    describe "when given a regex with no global flag", ->
+      it "calls the iterator with the first match for the given regex in the given range", ->
+        matches = []
+        ranges = []
+        buffer.scanInRange /cu(rr)ent/, [[4,0], [6,44]], ({match, range}) ->
+          matches.push(match)
+          ranges.push(range)
+
+        expect(matches.length).toBe 1
+        expect(ranges.length).toBe 1
+
+        expect(matches[0][0]).toBe 'current'
+        expect(matches[0][1]).toBe 'rr'
+        expect(ranges[0]).toEqual [[5,6], [5,13]]
+
+    describe "when given a regex with a global flag", ->
+      it "calls the iterator with each match for the given regex in the given range", ->
+        matches = []
+        ranges = []
+        buffer.scanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({match, range}) ->
+          matches.push(match)
+          ranges.push(range)
+
+        expect(matches.length).toBe 3
+        expect(ranges.length).toBe 3
+
+        expect(matches[0][0]).toBe 'current'
+        expect(matches[0][1]).toBe 'rr'
+        expect(ranges[0]).toEqual [[5,6], [5,13]]
+
+        expect(matches[1][0]).toBe 'current'
+        expect(matches[1][1]).toBe 'rr'
+        expect(ranges[1]).toEqual [[6,6], [6,13]]
+
+        expect(matches[2][0]).toBe 'current'
+        expect(matches[2][1]).toBe 'rr'
+        expect(ranges[2]).toEqual [[6,34], [6,41]]
+
+    describe "when the last regex match exceeds the end of the range", ->
+      describe "when the portion of the match within the range also matches the regex", ->
+        it "calls the iterator with the truncated match", ->
+          matches = []
+          ranges = []
+          buffer.scanInRange /cu(r*)/g, [[4,0], [6,9]], ({match, range}) ->
+            matches.push(match)
+            ranges.push(range)
+
+          expect(matches.length).toBe 2
+          expect(ranges.length).toBe 2
+
+          expect(matches[0][0]).toBe 'curr'
+          expect(matches[0][1]).toBe 'rr'
+          expect(ranges[0]).toEqual [[5,6], [5,10]]
+
+          expect(matches[1][0]).toBe 'cur'
+          expect(matches[1][1]).toBe 'r'
+          expect(ranges[1]).toEqual [[6,6], [6,9]]
+
+      describe "when the portion of the match within the range does not matches the regex", ->
+        it "calls the iterator with the truncated match", ->
+          matches = []
+          ranges = []
+          buffer.scanInRange /cu(r*)e/g, [[4,0], [6,9]], ({match, range}) ->
+            matches.push(match)
+            ranges.push(range)
+
+          expect(matches.length).toBe 1
+          expect(ranges.length).toBe 1
+
+          expect(matches[0][0]).toBe 'curre'
+          expect(matches[0][1]).toBe 'rr'
+          expect(ranges[0]).toEqual [[5,6], [5,11]]
+
+    describe "when the iterator calls the 'replace' control function with a replacement string", ->
+      it "replaces each occurrence of the regex match with the string", ->
+        ranges = []
+        buffer.scanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({range, replace}) ->
+          ranges.push(range)
+          replace("foo")
+
+        expect(ranges[0]).toEqual [[5,6], [5,13]]
+        expect(ranges[1]).toEqual [[6,6], [6,13]]
+        expect(ranges[2]).toEqual [[6,30], [6,37]]
+
+        expect(buffer.lineForRow(5)).toBe '      foo = items.shift();'
+        expect(buffer.lineForRow(6)).toBe '      foo < pivot ? left.push(foo) : right.push(current);'
+
+      it "allows the match to be replaced with the empty string", ->
+        buffer.scanInRange /current/g, [[4,0], [6,59]], ({replace}) ->
+          replace("")
+
+        expect(buffer.lineForRow(5)).toBe '       = items.shift();'
+        expect(buffer.lineForRow(6)).toBe '       < pivot ? left.push() : right.push(current);'
+
+    describe "when the iterator calls the 'stop' control function", ->
+      it "stops the traversal", ->
+        ranges = []
+        buffer.scanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({range, stop}) ->
+          ranges.push(range)
+          stop() if ranges.length == 2
+
+        expect(ranges.length).toBe 2
+
+  describe ".backwardsScanInRange(range, regex, fn)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    describe "when given a regex with no global flag", ->
+      it "calls the iterator with the last match for the given regex in the given range", ->
+        matches = []
+        ranges = []
+        buffer.backwardsScanInRange /cu(rr)ent/, [[4,0], [6,44]], ({match, range}) ->
+          matches.push(match)
+          ranges.push(range)
+
+        expect(matches.length).toBe 1
+        expect(ranges.length).toBe 1
+
+        expect(matches[0][0]).toBe 'current'
+        expect(matches[0][1]).toBe 'rr'
+        expect(ranges[0]).toEqual [[6,34], [6,41]]
+
+    describe "when given a regex with a global flag", ->
+      it "calls the iterator with each match for the given regex in the given range, starting with the last match", ->
+        matches = []
+        ranges = []
+        buffer.backwardsScanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({match, range}) ->
+          matches.push(match)
+          ranges.push(range)
+
+        expect(matches.length).toBe 3
+        expect(ranges.length).toBe 3
+
+        expect(matches[0][0]).toBe 'current'
+        expect(matches[0][1]).toBe 'rr'
+        expect(ranges[0]).toEqual [[6,34], [6,41]]
+
+        expect(matches[1][0]).toBe 'current'
+        expect(matches[1][1]).toBe 'rr'
+        expect(ranges[1]).toEqual [[6,6], [6,13]]
+
+        expect(matches[2][0]).toBe 'current'
+        expect(matches[2][1]).toBe 'rr'
+        expect(ranges[2]).toEqual [[5,6], [5,13]]
+
+    describe "when the iterator calls the 'replace' control function with a replacement string", ->
+      it "replaces each occurrence of the regex match with the string", ->
+        ranges = []
+        buffer.backwardsScanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({range, replace}) ->
+          ranges.push(range)
+          replace("foo") unless range.start.isEqual([6,6])
+
+        expect(ranges[0]).toEqual [[6,34], [6,41]]
+        expect(ranges[1]).toEqual [[6,6], [6,13]]
+        expect(ranges[2]).toEqual [[5,6], [5,13]]
+
+        expect(buffer.lineForRow(5)).toBe '      foo = items.shift();'
+        expect(buffer.lineForRow(6)).toBe '      current < pivot ? left.push(foo) : right.push(current);'
+
+    describe "when the iterator calls the 'stop' control function", ->
+      it "stops the traversal", ->
+        ranges = []
+        buffer.backwardsScanInRange /cu(rr)ent/g, [[4,0], [6,59]], ({range, stop}) ->
+          ranges.push(range)
+          stop() if ranges.length == 2
+
+        expect(ranges.length).toBe 2
+        expect(ranges[0]).toEqual [[6,34], [6,41]]
+        expect(ranges[1]).toEqual [[6,6], [6,13]]
+
+  describe ".characterIndexForPosition(position)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "returns the total number of characters that precede the given position", ->
+      expect(buffer.characterIndexForPosition([0, 0])).toBe 0
+      expect(buffer.characterIndexForPosition([0, 1])).toBe 1
+      expect(buffer.characterIndexForPosition([0, 29])).toBe 29
+      expect(buffer.characterIndexForPosition([1, 0])).toBe 30
+      expect(buffer.characterIndexForPosition([2, 0])).toBe 61
+      expect(buffer.characterIndexForPosition([12, 2])).toBe 408
+      expect(buffer.characterIndexForPosition([Infinity])).toBe 408
+
+    describe "when the buffer contains crlf line endings", ->
+      it "returns the total number of characters that precede the given position", ->
+        buffer.setText("line1\r\nline2\nline3\r\nline4")
+        expect(buffer.characterIndexForPosition([1])).toBe 7
+        expect(buffer.characterIndexForPosition([2])).toBe 13
+        expect(buffer.characterIndexForPosition([3])).toBe 20
+
+  describe ".positionForCharacterIndex(position)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "returns the position based on character index", ->
+      expect(buffer.positionForCharacterIndex(0)).toEqual [0, 0]
+      expect(buffer.positionForCharacterIndex(1)).toEqual [0, 1]
+      expect(buffer.positionForCharacterIndex(29)).toEqual [0, 29]
+      expect(buffer.positionForCharacterIndex(30)).toEqual [1, 0]
+      expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0]
+      expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2]
+
+    describe "when the buffer contains crlf line endings", ->
+      it "returns the position based on character index", ->
+        buffer.setText("line1\r\nline2\nline3\r\nline4")
+        expect(buffer.positionForCharacterIndex(7)).toEqual [1, 0]
+        expect(buffer.positionForCharacterIndex(13)).toEqual [2, 0]
+        expect(buffer.positionForCharacterIndex(20)).toEqual [3, 0]
+
+  describe ".usesSoftTabs()", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "returns true if the first indented line begins with tabs", ->
+      buffer.setText("function() {\n  foo();\n}")
+      expect(buffer.usesSoftTabs()).toBeTruthy()
+      buffer.setText("function() {\n\tfoo();\n}")
+      expect(buffer.usesSoftTabs()).toBeFalsy()
+      buffer.setText("")
+      expect(buffer.usesSoftTabs()).toBeUndefined()
+
+  describe ".isEmpty()", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "returns true for an empty buffer", ->
+      buffer.setText('')
+      expect(buffer.isEmpty()).toBeTruthy()
+
+    it "returns false for a non-empty buffer", ->
+      buffer.setText('a')
+      expect(buffer.isEmpty()).toBeFalsy()
+      buffer.setText('a\nb\nc')
+      expect(buffer.isEmpty()).toBeFalsy()
+      buffer.setText('\n')
+      expect(buffer.isEmpty()).toBeFalsy()
+
+  describe "'contents-modified' event", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "triggers the 'contents-modified' event with the current modified status when the buffer changes, rate-limiting events with a delay", ->
+      delay = buffer.stoppedChangingDelay
+      contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler")
+      buffer.on 'contents-modified', contentsModifiedHandler
+
+      buffer.insert([0, 0], 'a')
+      expect(contentsModifiedHandler).not.toHaveBeenCalled()
+
+      waits delay / 2
+
+      runs ->
+        buffer.insert([0, 0], 'b')
+        expect(contentsModifiedHandler).not.toHaveBeenCalled()
+
+      waits delay / 2
+
+      runs ->
+        expect(contentsModifiedHandler).not.toHaveBeenCalled()
+
+      waits delay / 2
+
+      runs ->
+        expect(contentsModifiedHandler).toHaveBeenCalledWith(true)
+
+        contentsModifiedHandler.reset()
+        buffer.undo()
+        buffer.undo()
+
+      waits delay
+
+      runs ->
+        expect(contentsModifiedHandler).toHaveBeenCalledWith(false)
+
+  describe ".append(text)", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    it "adds text to the end of the buffer", ->
+      buffer.setText("")
+      buffer.append("a")
+      expect(buffer.getText()).toBe "a"
+      buffer.append("b\nc")
+      expect(buffer.getText()).toBe "ab\nc"
+
+  describe "line ending support", ->
+    beforeEach ->
+      filePath = require.resolve('./fixtures/sample.js')
+      buffer = new TextBuffer({filePath, load: true})
+
+      waitsFor ->
+        buffer.loaded
+
+    describe ".getText()", ->
+      it "returns the text with the corrent line endings for each row", ->
+        buffer.setText("a\r\nb\nc")
+        expect(buffer.getText()).toBe "a\r\nb\nc"
+        buffer.setText("a\r\nb\nc\n")
+        expect(buffer.getText()).toBe "a\r\nb\nc\n"
+
+    describe "when editing a line", ->
+      it "preserves the existing line ending", ->
+        buffer.setText("a\r\nb\nc")
+        buffer.insert([0, 1], "1")
+        expect(buffer.getText()).toBe "a1\r\nb\nc"
+
+    describe "when inserting text with multiple lines", ->
+      describe "when the current line has a line ending", ->
+        it "uses the same line ending as the line where the text is inserted", ->
+          buffer.setText("a\r\n")
+          buffer.insert([0, 1], "hello\n1\n\n2")
+          expect(buffer.getText()).toBe "ahello\r\n1\r\n\r\n2\r\n"
+
+      describe "when the current line has no line ending (because it's the last line of the buffer)", ->
+        describe "when the buffer contains only a single line", ->
+          it "honors the line endings in the inserted text", ->
+            buffer.setText("initialtext")
+            buffer.append("hello\n1\r\n2\n")
+            expect(buffer.getText()).toBe "initialtexthello\n1\r\n2\n"
+
+        describe "when the buffer contains a preceding line", ->
+          it "uses the line ending of the preceding line", ->
+            buffer.setText("\ninitialtext")
+            buffer.append("hello\n1\r\n2\n")
+            expect(buffer.getText()).toBe "\ninitialtexthello\n1\n2\n"


But without the space.

But without the space.

--- spec/text-editor-spec.coffee
+++ spec/text-editor-spec.coffee
@@ -1192,7 +1192,7 @@
     describe '.getCursorScope()', ->
       it 'returns the current scope', ->
         descriptor = editor.getCursorScope()
-        expect(descriptor.scopes).toContain ('source.js')
+        expect(descriptor.scopes).toContain('source.js')

   describe "selection", ->
     selection = null


Update the spec.

Update the spec.

--- spec/text-editor-spec.coffee
+++ spec/text-editor-spec.coffee
@@ -1189,14 +1189,10 @@
           cursor2 = editor.addCursorAtBufferPosition([1, 4])
           expect(cursor2.marker).toBe cursor1.marker

-    describe '.logCursorScope()', ->
-      beforeEach ->
-        spyOn(atom.notifications, 'addInfo')
-
-      it 'opens a notification', ->
-        editor.logCursorScope()
-
-        expect(atom.notifications.addInfo).toHaveBeenCalled()
+    describe '.getCursorScope()', ->
+      it 'returns the current scope', ->
+        descriptor = editor.getCursorScope()
+        expect(descriptor.scopes).toContain ('source.js')

   describe "selection", ->
     selection = null


Ensure middleware instance is ready before attaching

--- spec/utils/middleware.coffee
+++ spec/utils/middleware.coffee
@@ -14,17 +14,24 @@

   constructor: (@component, @baseDir) ->

+  attachAndStart: (instance, callback) ->
+    @instance = instance
+    @actionIn = noflo.internalSocket.createSocket()
+    @instance.inPorts.in.attach @actionIn
+    @actionIn.port = 'in'
+    @instance.start()
+    @instance.network.once 'start', ->
+      callback null
+
   before: (callback) ->
     loader = new noflo.ComponentLoader @baseDir
     loader.load @component, (err, instance) =>
       return callback err if err
-      @instance = instance
-      @actionIn = noflo.internalSocket.createSocket()
-      @instance.inPorts.in.attach @actionIn
-      @actionIn.port = 'in'
-      @instance.start()
-      @instance.network.once 'start', ->
-        callback null
+      if instance.isReady()
+        @attachAndStart instance, callback
+        return
+      instance.once 'ready', =>
+        @attachAndStart instance, callback

   beforeEach: ->
     @passAction = noflo.internalSocket.createSocket()


Don't need these, we'll use the JS one.

Don't need these, we'll use the JS one.

--- spec/git-repository-async-spec.coffee
+++ spec/git-repository-async-spec.coffee
@@ -1,460 +0,0 @@
-temp = require 'temp'
-GitRepositoryAsync = require '../src/git-repository-async'
-Git = require 'nodegit'
-fs = require 'fs-plus'
-os = require 'os'
-path = require 'path'
-Task = require '../src/task'
-Project = require '../src/project'
-
-# Clean up when the process exits
-temp.track()
-
-copyRepository = ->
-  workingDirPath = temp.mkdirSync('atom-working-dir')
-  fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
-  fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
-  fs.realpathSync(workingDirPath)
-
-openFixture = (fixture) ->
-  GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture))
-
-describe "GitRepositoryAsync", ->
-  repo = null
-
-  # beforeEach ->
-  #   gitPath = path.join(temp.dir, '.git')
-  #   fs.removeSync(gitPath) if fs.isDirectorySync(gitPath)
-  #
-  # afterEach ->
-  #   repo.destroy() if repo?.repo?
-
-  describe "@open(path)", ->
-
-    # This just exercises the framework, but I'm trying to match the sync specs to start
-    it "repo is null when no repository is found", ->
-      repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt'))
-
-      waitsForPromise {shouldReject: true}, ->
-        repo.repoPromise
-
-      runs ->
-        expect(repo.repo).toBe null
-
-  describe ".getPath()", ->
-    # XXX HEAD isn't a git directory.. what's this spec supposed to be about?
-    xit "returns the repository path for a .git directory path", ->
-      # Rejects as malformed
-      repo = GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD'))
-
-      onSuccess = jasmine.createSpy('onSuccess')
-
-      waitsForPromise ->
-        repo.getPath().then(onSuccess)
-
-      runs ->
-        expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git')
-        expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath)
-
-    it "returns the repository path for a repository path", ->
-      repo = openFixture('master.git')
-
-      onSuccess = jasmine.createSpy('onSuccess')
-
-      waitsForPromise ->
-        repo.getPath().then(onSuccess)
-
-      runs ->
-        expectedPath = path.join(__dirname, 'fixtures', 'git', 'master.git')
-        expect(onSuccess.mostRecentCall.args[0]).toBe(expectedPath)
-
-  describe ".isPathIgnored(path)", ->
-    it "resolves true for an ignored path", ->
-      repo = openFixture('ignore.git')
-      onSuccess = jasmine.createSpy('onSuccess')
-      waitsForPromise ->
-        repo.isPathIgnored('a.txt').then(onSuccess).catch (e) -> console.log e
-
-      runs ->
-        expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()
-
-    it "resolves false for a non-ignored path", ->
-      repo = openFixture('ignore.git')
-      onSuccess = jasmine.createSpy('onSuccess')
-      waitsForPromise ->
-        repo.isPathIgnored('b.txt').then(onSuccess)
-      runs ->
-        expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()
-
-
-  describe ".isPathModified(path)", ->
-    [repo, filePath, newPath, emptyPath] = []
-
-    beforeEach ->
-      workingDirPath = copyRepository()
-      repo = GitRepositoryAsync.open(workingDirPath)
-      filePath = path.join(workingDirPath, 'a.txt')
-      newPath = path.join(workingDirPath, 'new-path.txt')
-      fs.writeFileSync(newPath, "i'm new here")
-      emptyPath = path.join(workingDirPath, 'empty-path.txt')
-
-    describe "when the path is unstaged", ->
-      it "resolves false if the path has not been modified", ->
-        onSuccess = jasmine.createSpy('onSuccess')
-        waitsForPromise ->
-          repo.isPathModified(filePath).then(onSuccess)
-        runs ->
-          expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()
-
-      it "resolves true if the path is modified", ->
-        fs.writeFileSync(filePath, "change")
-        onSuccess = jasmine.createSpy('onSuccess')
-        waitsForPromise ->
-          repo.isPathModified(filePath).then(onSuccess)
-        runs ->
-          expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()
-
-      it "resolves false if the path is new", ->
-        onSuccess = jasmine.createSpy('onSuccess')
-        waitsForPromise ->
-          repo.isPathModified(newPath).then(onSuccess)
-        runs ->
-          expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()
-
-      it "resolves false if the path is invalid", ->
-        onSuccess = jasmine.createSpy('onSuccess')
-        waitsForPromise ->
-          repo.isPathModified(emptyPath).then(onSuccess)
-        runs ->
-          expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()
-
-  describe ".isPathNew(path)", ->
-    [filePath, newPath] = []
-
-    beforeEach ->
-      workingDirPath = copyRepository()
-      repo = GitRepositoryAsync.open(workingDirPath)
-      filePath = path.join(workingDirPath, 'a.txt')
-      newPath = path.join(workingDirPath, 'new-path.txt')
-      fs.writeFileSync(newPath, "i'm new here")
-
-    describe "when the path is unstaged", ->
-      it "returns true if the path is new", ->
-        onSuccess = jasmine.createSpy('onSuccess')
-        waitsForPromise ->
-          repo.isPathNew(newPath).then(onSuccess)
-        runs ->
-          expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()
-
-      it "returns false if the path isn't new", ->
-        onSuccess = jasmine.createSpy('onSuccess')
-        waitsForPromise ->
-          repo.isPathModified(newPath).then(onSuccess)
-        runs ->
-          expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()
-
-
-  describe ".checkoutHead(path)", ->
-    [filePath] = []
-
-    beforeEach ->
-      workingDirPath = copyRepository()
-      repo = GitRepositoryAsync.open(workingDirPath)
-      filePath = path.join(workingDirPath, 'a.txt')
-
-    it "no longer reports a path as modified after checkout", ->
-      onSuccess = jasmine.createSpy('onSuccess')
-      waitsForPromise ->
-        repo.isPathModified(filePath).then(onSuccess)
-      runs ->
-        expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()
-        fs.writeFileSync(filePath, 'ch ch changes')
-
-      onSuccess = jasmine.createSpy('onSuccess')
-      waitsForPromise ->
-        repo.isPathModified(filePath).then(onSuccess)
-      runs ->
-        expect(onSuccess.mostRecentCall.args[0]).toBeTruthy()
-
-      # Don't need to assert that this succeded because waitsForPromise should
-      # fail if it was rejected..
-      waitsForPromise ->
-        repo.checkoutHead(filePath)
-      runs ->
-        onSuccess = jasmine.createSpy('onSuccess')
-
-      waitsForPromise ->
-        repo.isPathModified(filePath).then(onSuccess)
-      runs ->
-        expect(onSuccess.mostRecentCall.args[0]).toBeFalsy()
-
-    it "restores the contents of the path to the original text", ->
-      fs.writeFileSync(filePath, 'ch ch changes')
-      waitsForPromise ->
-        repo.checkoutHead(filePath)
-      runs ->
-        expect(fs.readFileSync(filePath, 'utf8')).toBe ''
-
-    it "fires a did-change-status event if the checkout completes successfully", ->
-      fs.writeFileSync(filePath, 'ch ch changes')
-      statusHandler = jasmine.createSpy('statusHandler')
-
-      waitsForPromise ->
-        repo.getPathStatus(filePath)
-      runs ->
-        repo.onDidChangeStatus statusHandler
-
-      waitsForPromise ->
-        repo.checkoutHead(filePath)
-      runs ->
-        expect(statusHandler.callCount).toBe 1
-        expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0}
-
-      waitsForPromise ->
-        repo.checkoutHead(filePath)
-      runs ->
-        expect(statusHandler.callCount).toBe 1
-
-  xdescribe ".checkoutHeadForEditor(editor)", ->
-    [filePath, editor] = []
-
-    beforeEach ->
-      workingDirPath = copyRepository()
-      repo = repo = GitRepositoryAsync.open(workingDirPath)
-      filePath = path.join(workingDirPath, 'a.txt')
-      fs.writeFileSync(filePath, 'ch ch changes')
-
-      waitsForPromise ->
-        atom.workspace.open(filePath)
-
-      runs ->
-        editor = atom.workspace.getActiveTextEditor()
-
-    xit "displays a confirmation dialog by default", ->
-      spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK()
-      atom.config.set('editor.confirmCheckoutHeadRevision', true)
-
-      waitsForPromise ->
-        repo.checkoutHeadForEditor(editor)
-      runs ->
-        expect(fs.readFileSync(filePath, 'utf8')).toBe ''
-
-    xit "does not display a dialog when confirmation is disabled", ->
-      spyOn(atom, 'confirm')
-      atom.config.set('editor.confirmCheckoutHeadRevision', false)
-
-      waitsForPromise ->
-        repo.checkoutHeadForEditor(editor)
-      runs ->
-        expect(fs.readFileSync(filePath, 'utf8')).toBe ''
-        expect(atom.confirm).not.toHaveBeenCalled()
-
-  xdescribe ".destroy()", ->
-    it "throws an exception when any method is called after it is called", ->
-      repo = new GitRepository(require.resolve('./fixtures/git/master.git/HEAD'))
-      repo.destroy()
-      expect(-> repo.getShortHead()).toThrow()
-
-  describe ".getPathStatus(path)", ->
-    [filePath] = []
-
-    beforeEach ->
-      workingDirectory = copyRepository()
-      repo = GitRepositoryAsync.open(workingDirectory)
-      filePath = path.join(workingDirectory, 'file.txt')
-
-    it "trigger a status-changed event when the new status differs from the last cached one", ->
-      statusHandler = jasmine.createSpy("statusHandler")
-      repo.onDidChangeStatus statusHandler
-      fs.writeFileSync(filePath, '')
-
-      waitsForPromise ->
-        repo.getPathStatus(filePath)
-
-      runs ->
-        expect(statusHandler.callCount).toBe 1
-        status = Git.Status.STATUS.WT_MODIFIED
-        expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status}
-        fs.writeFileSync(filePath, 'abc')
-
-      waitsForPromise ->
-        status = repo.getPathStatus(filePath)
-
-      runs ->
-        expect(statusHandler.callCount).toBe 1
-
-  describe ".getDirectoryStatus(path)", ->
-    [directoryPath, filePath] = []
-
-    beforeEach ->
-      workingDirectory = copyRepository()
-      repo = GitRepositoryAsync.open(workingDirectory)
-      directoryPath = path.join(workingDirectory, 'dir')
-      filePath = path.join(directoryPath, 'b.txt')
-
-    it "gets the status based on the files inside the directory", ->
-      onSuccess = jasmine.createSpy('onSuccess')
-      onSuccess2 = jasmine.createSpy('onSuccess2')
-
-      waitsForPromise ->
-        repo.getDirectoryStatus(directoryPath).then(onSuccess)
-
-      runs ->
-        expect(onSuccess.callCount).toBe 1
-        expect(repo.isStatusModified(onSuccess.mostRecentCall)).toBe false
-        fs.writeFileSync(filePath, 'abc')
-
-      waitsForPromise ->
-        repo.getDirectoryStatus(directoryPath).then(onSuccess2)
-      runs ->
-        expect(onSuccess2.callCount).toBe 1
-        expect(repo.isStatusModified(onSuccess2.argsForCall[0][0])).toBe true
-
-
-  describe ".refreshStatus()", ->
-    [newPath, modifiedPath, cleanPath, originalModifiedPathText] = []
-
-    beforeEach ->
-      workingDirectory = copyRepository()
-      repo = GitRepositoryAsync.open(workingDirectory)
-      modifiedPath = path.join(workingDirectory, 'file.txt')
-      newPath = path.join(workingDirectory, 'untracked.txt')
-      cleanPath = path.join(workingDirectory, 'other.txt')
-      fs.writeFileSync(cleanPath, 'Full of text')
-      fs.writeFileSync(newPath, '')
-      newPath = fs.absolute newPath  # specs could be running under symbol path.
-
-    it "returns status information for all new and modified files", ->
-      fs.writeFileSync(modifiedPath, 'making this path modified')
-      statusHandler = jasmine.createSpy('statusHandler')
-      onSuccess = jasmine.createSpy('onSuccess')
-      repo.onDidChangeStatuses statusHandler
-      waitsForPromise ->
-        repo.refreshStatus().then(onSuccess)
-
-      runs ->
-        # Callers will use the promise returned by refreshStatus, not the
-        # cache directly
-        expect(onSuccess.mostRecentCall.args[0]).toEqual(repo.pathStatusCache)
-        expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
-        expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy()
-        expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
-
-  # This tests the async implementation's events directly, but ultimately I
-  # think we want users to just be able to subscribe to events on GitRepository
-  # and have them bubble up from async-land
-
-  describe "buffer events", ->
-    [editor] = []
-
-    beforeEach ->
-      atom.project.setPaths([copyRepository()])
-
-    it "emits a status-changed event when a buffer is saved", ->
-      waitsForPromise ->
-        atom.workspace.open('other.txt').then (o) ->
-          editor = o
-      runs ->
-        editor.insertNewline()
-        statusHandler = jasmine.createSpy('statusHandler')
-        repo = atom.project.getRepositories()[0]
-        repo.async.onDidChangeStatus statusHandler
-        editor.save()
-
-        waitsFor ->
-          statusHandler.callCount >= 1
-        runs ->
-          expect(statusHandler.callCount).toBe 1
-          expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
-
-    it "emits a status-changed event when a buffer is reloaded", ->
-      waitsForPromise ->
-        atom.workspace.open('other.txt').then (o) ->
-          editor = o
-      runs ->
-        fs.writeFileSync(editor.getPath(), 'changed')
-
-        statusHandler = jasmine.createSpy('statusHandler')
-        atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler
-        editor.getBuffer().reload()
-        reloadHandler = jasmine.createSpy 'reloadHandler'
-
-        waitsFor ->
-          statusHandler.callCount >= 1
-        runs ->
-          expect(statusHandler.callCount).toBe 1
-          expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
-          buffer = editor.getBuffer()
-          buffer.onDidReload(reloadHandler)
-          buffer.reload()
-
-        waitsFor ->
-          reloadHandler.callCount >= 1
-        runs ->
-          expect(statusHandler.callCount).toBe 1
-
-    it "emits a status-changed event when a buffer's path changes", ->
-      waitsForPromise ->
-        atom.workspace.open('other.txt').then (o) ->
-          editor = o
-      runs ->
-        fs.writeFileSync(editor.getPath(), 'changed')
-
-        statusHandler = jasmine.createSpy('statusHandler')
-        atom.project.getRepositories()[0].async.onDidChangeStatus statusHandler
-        editor.getBuffer().emitter.emit 'did-change-path'
-        waitsFor ->
-          statusHandler.callCount >= 1
-        runs ->
-          expect(statusHandler.callCount).toBe 1
-          expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
-
-        pathHandler = jasmine.createSpy('pathHandler')
-        buffer = editor.getBuffer()
-        buffer.onDidChangePath pathHandler
-        buffer.emitter.emit 'did-change-path'
-        waitsFor ->
-          pathHandler.callCount >= 1
-        runs ->
-          expect(statusHandler.callCount).toBe 1
-
-    it "stops listening to the buffer when the repository is destroyed (regression)", ->
-      waitsForPromise ->
-        atom.workspace.open('other.txt').then (o) ->
-          editor = o
-      runs ->
-        atom.project.getRepositories()[0].destroy()
-        expect(-> editor.save()).not.toThrow()
-
-  describe "when a project is deserialized", ->
-    [buffer, project2] = []
-
-    afterEach ->
-      project2?.destroy()
-
-    it "subscribes to all the serialized buffers in the project", ->
-      atom.project.setPaths([copyRepository()])
-
-      waitsForPromise ->
-        atom.workspace.open('file.txt')
-
-      runs ->
-        project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
-        project2.deserialize(atom.project.serialize(), atom.deserializers)
-        buffer = project2.getBuffers()[0]
-
-      waitsFor ->
-        buffer.loaded
-
-      runs ->
-        originalContent = buffer.getText()
-        buffer.append('changes')
-
-        statusHandler = jasmine.createSpy('statusHandler')
-        project2.getRepositories()[0].async.onDidChangeStatus statusHandler
-        buffer.save()
-        waitsFor ->
-          statusHandler.callCount >= 1
-        runs ->
-          expect(statusHandler.callCount).toBe 1
-          expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256}


Add test for Request.waitFor

Add test for Request.waitFor

--- test/request.spec.coffee
+++ test/request.spec.coffee
@@ -37,6 +37,11 @@
     it 'is not pageable if the config does not exist for the service', ->
       delete service.api.pagination
       expect(service.makeRequest('mockMethod').isPageable()).toEqual(false)
+
+  describe 'waitFor', ->
+    it 'creates a ResourceWaiter object', ->
+      service.api.waiters = state: operation: 'mockMethod'
+      expect(service.waitFor('state'))

   describe 'send', ->
     it 'accepts an optional callback', ->


Add extra test to ensure error object propagates to 'error' event

--- test/request.spec.coffee
+++ test/request.spec.coffee
@@ -28,11 +28,13 @@

     it 'propagates errors to error event', ->
       helpers.mockHttpResponse 200, {}, ''
+      err = null
       expect(->
         req = service.makeRequest('mockMethod')
         req.on 'extractData', -> throw new Error('error')
-        req.send()
+        req.send((e) -> err = e)
       ).not.toThrow('error')
+      expect(err.message).toEqual('error')

   describe 'isPageable', ->
     beforeEach ->


Change cssimage task run logic  - it runs only while build and by direct call

--- app/templates/gulp/gulpfile.coffee
+++ app/templates/gulp/gulpfile.coffee
@@ -12,7 +12,7 @@
 gulp.task "fonts", require './tasks/fonts'
 gulp.task "images", require './tasks/images'
 gulp.task "cssimage", require './tasks/cssimage'
-gulp.task "styles", ["cssimage"], require './tasks/styles'
+gulp.task "styles", require './tasks/styles'
 gulp.task "extras", require './tasks/extras'
 gulp.task "extras:js", require './tasks/extras-js'
 gulp.task "imagepreload", require './tasks/imagepreload'
@@ -29,6 +29,7 @@
   build = ["clean"]
   build.push ["images", "fonts", "extras"] unless PROP.isDev
   build.push "sprites"
+  build.push "cssimage"
   build.push "styles"
   build.push if PROP.isDev then "scripts" else "rjs"
   build.push "extras:js" unless PROP.isDev


childProcess.pid fixed

childProcess.pid fixed

--- builders/ProcessMonitor.coffee
+++ builders/ProcessMonitor.coffee
@@ -84,7 +84,6 @@
     return data
   startProcess : _.throttle ()->
     cmd = "#{@options.run[0]} #{@options.run[1].join(" ")}"
-    cmd = "NODE_PATH=$NODE_PATH:#{__dirname}/../our_modules;#{cmd}"
     log.info "Starting the process $>#{cmd}"
     @nodeServer         = exec cmd
     @nodeServer.stdout.on 'data', (data)=>
@@ -107,8 +106,8 @@
   ,1000

   stopProcess : ()->
-    log.info "Stopping the process... #{@nodeServer.pid+1}"
-    exec "kill -9 #{@nodeServer.pid+1}",(err,stdout,stderr)=>
+    log.info "Stopping the process... #{@nodeServer.pid}"
+    exec "kill -9 #{@nodeServer.pid}",(err,stdout,stderr)=>

 module.exports = ProcessMonitor


Global->Component->Local debug flag override semantics

--- components.coffee
+++ components.coffee
@@ -14,6 +14,13 @@
 componentDefinitionQueue = []

 componentDebug = Meteor.settings.public?.debug?.jframework?.components ? false
+componentDebugStack = []
+_pushDebugFlag = (flag = null) ->
+    componentDebugStack.push componentDebug
+    componentDebug = flag ? componentDebug
+_popDebugFlag = ->
+    componentDebug = componentDebugStack.pop()
+
 _debugDepth = 0
 _getDebugPrefix = (component = null, tabWidth = 4) ->
     numSpaces = Math.max 0, tabWidth * _debugDepth - 1
@@ -45,6 +52,7 @@
     delete reactSpec.props
     delete reactSpec.state
     delete reactSpec.reactives
+    delete reactSpec.debug

     reactSpec.displayName = componentName

@@ -62,8 +70,14 @@
                 throw new Meteor.Error "Can't pass undefined to #{componentName}.#{stateFieldName}"
             else if value is undefined
                 # Getter
+                _pushDebugFlag stateFieldSpec.debug ? componentSpec.debug
+
                 ret = @state[stateFieldName].get()
-                if componentDebug then console.log _getDebugPrefix(@), "#{stateFieldName}()", ret
+
+                if componentDebug
+                    console.log _getDebugPrefix(@), "#{stateFieldName}()", ret
+                _popDebugFlag()
+
                 ret
             else
                 # Setter
@@ -148,9 +162,11 @@
         for propName, propSpec of propSpecs
             @_props[propName] = new ReactiveVar @props[propName],
                 propSpec.same ? J.util.equals
-            @prop[propName] = do (propName) => =>
+            @prop[propName] = do (propName, propSpec) => =>
+                _pushDebugFlag propSpec.debug ? componentSpec.debug
                 ret = @_props[propName].get()
                 if componentDebug then console.log _getDebugPrefix(@), "prop.#{propName}()", ret
+                _popDebugFlag()
                 ret

         # Set up @reactives
@@ -158,11 +174,18 @@
         for reactiveName, reactiveSpec of reactiveSpecByName
             @reactives[reactiveName] = do (reactiveName, reactiveSpec) => J.AutoVar(
                 =>
-                    if componentDebug then console.log _getDebugPrefix(@), "!#{reactiveName}()"
-                    _debugDepth += 1
+                    _pushDebugFlag reactiveSpec.debug ? componentSpec.debug
+                    if componentDebug
+                        console.log _getDebugPrefix(@), "!#{reactiveName}()"
+                        _debugDepth += 1
+
                     ret = reactiveSpec.val.call @
-                    if componentDebug then console.log _getDebugPrefix(), ret
-                    _debugDepth -= 1
+
+                    if componentDebug
+                        console.log _getDebugPrefix(), ret
+                        _debugDepth -= 1
+                    _popDebugFlag()
+
                     ret
                 (
                     if reactiveSpec.onChange? then (oldValue, newValue) =>
@@ -190,11 +213,18 @@
                     stateFromRoute[stateFieldName]
                 else if _.isFunction stateFieldSpec.default
                     # TODO: If the type is J.$function then use the other if-branch
-                    if componentDebug then console.log _getDebugPrefix(@), "#{stateFieldName} !default()"
-                    _debugDepth += 1
+                    _pushDebugFlag stateFieldSpec.debug ? componentSpec.debug
+                    if componentDebug
+                        console.log _getDebugPrefix(@), "#{stateFieldName} !default()"
+                        _debugDepth += 1
+
                     ret = stateFieldSpec.default.apply @
-                    if componentDebug then console.log _getDebugPrefix(), ret
-                    _debugDepth -= 1
+
+                    if componentDebug
+                        console.log _getDebugPrefix(), ret
+                        _debugDepth -= 1
+                    _popDebugFlag()
+
                     ret
                 else
                     stateFieldSpec.default
@@ -322,10 +352,16 @@
             @_renderComp.stop()

         @_renderComp = Tracker.autorun (c) =>
-            if componentDebug then console.log _getDebugPrefix() + (if _debugDepth > 0 then " " else "") + "#{@toString()} render!#{if c.firstRun then '' else ' - from AutoRun'}"
-            _debugDepth += 1
+            _pushDebugFlag componentSpec.debug
+            if componentDebug
+                console.log _getDebugPrefix() + (if _debugDepth > 0 then " " else "") + "#{@toString()} render!#{if c.firstRun then '' else ' - from AutoRun'}"
+                _debugDepth += 1
+
             renderedComponent = componentSpec.render.apply @
-            _debugDepth -= 1
+
+            if componentDebug
+                _debugDepth -= 1
+            _popDebugFlag()

         @_renderComp.onInvalidate (c) =>
             return if c.stopped


Disable minecraft modal and announcements modal when freeOnly

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -197,8 +197,8 @@

   # Minecraft Modal:
   maybeShowMinecraftModal: ->
+    return false if features.freeOnly
     userQualifiesForMinecraftModal = (user) ->
-      return false if features.codePlay
       return true if user.isAdmin()
       return false if user.isPremium()
       return false if user.isAnonymous()
@@ -991,6 +991,7 @@
     )

   maybeShowPendingAnnouncement: () ->
+    return false if features.freeOnly # TODO: handle announcements that can be shown to free only servers
     latest = window.serverConfig.latestAnnouncement
     myLatest = me.get('lastAnnouncementSeen')
     return unless typeof latest is 'number'


Add missing 'class' keyword

Add missing 'class' keyword

--- scripts/views/workspace/content/aloha-edit.coffee
+++ scripts/views/workspace/content/aloha-edit.coffee
@@ -7,7 +7,7 @@
   #'mathjax'
 ], ($, _, Backbone, Marionette, Aloha) ->

-  return AlohaEditView extends Marionette.ItemView
+  return class AlohaEditView extends Marionette.ItemView
     # **NOTE:** This template is not wrapped in an element
     template: () -> throw 'You need to specify a template, modelKey, and optionally alohaOptions'
     modelKey: null


Use popup instance variable like Ace autocompleter does

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
@@ -224,7 +224,7 @@
 					#   is likely to end up with `\href{}ref{}`
 					if !data?
 						completions = this.completions
-						popup = editor.completer.popup
+						popup = this.popup
 						data = popup.getData(popup.getRow())
 						data.completer =
 							insertMatch: (editor, matchData) ->


buildscript: fix error in buildLocalAsync

--- build_detail.coffee
+++ build_detail.coffee
@@ -437,7 +437,7 @@
   flash_dir = path.join __dirname, 'PepperFlash'

   download_theme = downloadThemesAsync theme_root
-  install_flash = installFlashAsync os.platform(), os.arch(), download_dir,
+  install_flash = installFlashAsync "#{os.platform()}-#{os.arch()}", download_dir,
     flash_dir
   install_npm_bower = (async ->
     yield npmInstallAsync __dirname, ['--production']


Port MakeRange to Process API

Port MakeRange to Process API

--- components/MakeRange.coffee
+++ components/MakeRange.coffee
@@ -1,51 +1,28 @@
 noflo = require 'noflo'

-class MakeRange extends noflo.Component
+exports.getComponent = ->
   description: 'Make an array of evenly-spaced numbers'
   icon: 'folder-o'
-  constructor: ->
-    @from = null
-    @to = null
-    @count = 2
-
-    @inPorts = new noflo.InPorts
-      from:
-        datatype: 'number'
-        required: true
-      to:
-        datatype: 'number'
-        required: true
-      count:
-        datatype: 'int'
-        required: true
-
-    @outPorts = new noflo.OutPorts
-      range:
-        datatype: 'array'
-
-    @inPorts.from.on 'data', (data) =>
-      @from = data
-      @compute()
-
-    @inPorts.to.on 'data', (data) =>
-      @to = data
-      @compute()
-
-    @inPorts.count.on 'data', (data) =>
-      @count = data
-      @compute()
-
-  compute: ->
-    return unless @outPorts.range.isAttached()
-    if @from? and @to? and @count? and @count > 1
-      range = []
-      f = @from
-      spread = @to - @from
-      increment = spread / (@count-1)
-      for i in [0..@count-1]
-        range[i] = @from + increment*i
-      @outPorts.range.send range
-    else if @from? and @count is 1
-      @outPorts.range.send @from
-
-exports.getComponent = -> new MakeRange
+  c.inPorts.add 'from',
+    datatype: 'number'
+  c.inPorts.add 'to',
+    datatype: 'number'
+  c.inPorts.add 'count',
+    datatype: 'int'
+  c.inPorts.add 'range',
+    datatype: 'array'
+  c.process (input, output) ->
+    return unless input.hasData 'from', 'to', 'count'
+    [from, to, count] = input.getData 'from', 'to', 'count'
+    if count is 1
+      output.sendDone
+        range: [from]
+      return
+    range = []
+    f = from
+    spread = to - from
+    increment = spread / (count-1)
+    for i in [0..count-1]
+      range[i] = from + increment*i
+    output.sendDone
+      range: range


Check file descriptor once

Check file descriptor once

--- components/SCDDetect.coffee
+++ components/SCDDetect.coffee
@@ -11,7 +11,6 @@
   ctx = canvas.getContext '2d'
   imageData = ctx.getImageData 0, 0, canvas.width, canvas.height
   data = imageData.data
-
   tmpFile = new temporary.File

   rs = canvas.pngStream()
@@ -31,10 +30,6 @@
       return
     ws.once 'close', ->
       fs.fsync fd, ->
-        if fd < 0
-          callback new Error 'Bad file descriptor'
-          tmpFile.unlink()
-          return
         try
           onEnd tmpFile, cascade, callback
         catch error
@@ -45,9 +40,6 @@
 onEnd = (tmpFile, cascade, callback) ->
   bin = path.join __dirname, '../build/Release/scddetect'
   exec "#{bin} #{tmpFile.path} #{cascade}", (err, stdout, stderr) ->
-    console.log 'stdout', stdout
-    console.log 'stderr', stderr
-    console.log 'err', err
     if err
       callback err
       tmpFile.unlink()


fix(draft-factory): Correctly load messages when creating a draft on a not-loaded thread. Sentry 26671

--- src/flux/stores/draft-factory.coffee
+++ src/flux/stores/draft-factory.coffee
@@ -152,7 +152,7 @@
     if behavior not in ['prefer-existing-if-pristine', 'prefer-existing']
       return Promise.resolve(null)

-    getMessages = DatabaseStore.findBy(Message, {threadId: message.threadId})
+    getMessages = DatabaseStore.findAll(Message, {threadId: message.threadId})
     if message.threadId is MessageStore.threadId()
       getMessages = Promise.resolve(MessageStore.items())


mixpanel: log new terminal open

mixpanel: log new terminal open

--- client/Terminal/AppView.coffee
+++ client/Terminal/AppView.coffee
@@ -31,6 +31,9 @@
     @on 'PlusHandleClicked', @bound 'handlePlusClick'
     @on 'WebTermConnected',  @bound 'updateSessions'
     @on 'TerminalClosed',    @bound 'removeSession'
+
+    @on 'TerminalStarted', ->
+      KD.mixpanel "Open new Webterm tab, success"

     {vmController} = KD.singletons
     vmController.on 'vm.progress.error', => @notify cssClass : 'error'


 Merge branch 'master.terminal.hidemessagepane' into 'master'

terminal: hide message pane when running and started timeout callbacks are invoked

--- client/Terminal/AppView.coffee
+++ client/Terminal/AppView.coffee
@@ -102,12 +102,14 @@
         return @setMessage "It seems you don't have a VM to use with Terminal."

       WebTermView.setTerminalTimeout vmName, 15000
-      , => @restoreTabs vmName
-      , (->)
+      , =>
+        @restoreTabs vmName
+        @messagePane.hide()
+      , =>
+        @messagePane.hide()
       , =>
         KD.mixpanel "Open Webterm, fail", {vmName}
         KD.logToExternalWithTime "oskite: Can't open Webterm", vmName
-

         @emit 'message', """

Couldn't connect to your VM.

@@ -246,10 +248,14 @@

     {vmName} = options

-    WebTermView.setTerminalTimeout vmName, 15000, ->
+    WebTermView.setTerminalTimeout vmName, 15000
+    , =>
       terminalView.connectToTerminal()
+      @messagePane.hide()
     , =>
-      KD.utils.defer => @addNewTab vmName
+      KD.utils.defer =>
+        @addNewTab vmName
+        @messagePane.hide()
     , =>
       KD.mixpanel "Open Webterm, fail", {vmName}
       KD.logToExternalWithTime "oskite: Can't open Webterm", vmName


grr merge

grr merge

--- client/code/view/titles.coffee
+++ client/code/view/titles.coffee
@@ -102,43 +102,3 @@
     """
     @$el.html tpl
     @
-
-<<<<<<< HEAD
-=======
-  nameClicked: (e) ->
-    e.preventDefault()
-    $a = @$el.find('.editable')
-    @$el.find('input').val(@model.get 'displayName').css('width', $a.width() + 20).show 0, ->
-      @focus()
-    $a.hide()
-
-  editableNameBlurred: ->
-    $label = @$el.find('.editable')
-    $input = @$el.find('input')
-    @newName = $.trim($input.val())
-    @oldName = $label.text()
-    if @newName == '' or @newName == $label.text()
-      $label.show().next().hide()
-    else
-      $input.hide()
-      $label.text(@newName).show()
-      @model.set 'displayName', @newName
-      @dataset().save {},
-        success: =>
-          $label.addClass 'saved'
-          setTimeout ->
-            $label.removeClass 'saved'
-          , 1000
-        error: (e) =>
-          $label.text @oldName
-          @model.set 'displayName', @oldName
-          console.warn 'error saving new name', e
-
-  editableNameEscaped: (e) ->
-    e.preventDefault()
-    @$el.find('.editable').show().next().hide()
-
-  keypressOnEditableName: (event) ->
-    @editableNameBlurred() if event.keyCode is 13
-    @editableNameEscaped() if event.keyCode is 27
->>>>>>> 17d994a3ead0471d0ab2db3d9acccbf1e8933b16


FSFile: split out the fetchContents into fetchRawContents which provides you a base64 content of the file

--- client/finder/fs/fsfile.coffee
+++ client/finder/fs/fsfile.coffee
@@ -25,25 +25,23 @@
   fetchContentsBinary: (callback)->
     @fetchContents no, callback

+  fetchRawContents: (callback)->
+
+    kite = @getKite()
+    kite.vmOn().then =>
+      kite.fsReadFile path: FSHelper.plainPath @path
+    .nodeify callback
+
   fetchContents: (useEncoding, callback)->
     [callback, useEncoding] = [useEncoding, callback]  unless callback

-    useEncoding = yes
+    useEncoding ?= yes

     @emit "fs.job.started"

-    kite = @getKite()
-
-    ok = kite.vmOn()
-    .then =>
-
-      kite.fsReadFile {
-        path: FSHelper.plainPath @path
-      }
-
-    .then (response) =>
+    @fetchRawContents().then (response)=>
+
       content = atob response.content
-
       content = KD.utils.utf8Decode content  if useEncoding # Convert to String

       KD.mixpanel "Fetch contents, success"


payment: update includes to reflect refactoring

--- client/landing/includes.coffee
+++ client/landing/includes.coffee
@@ -148,9 +148,9 @@

   # PAYMENT
   "app/MainApp/payment/paymentcontroller.coffee",
-  "app/MainApp/payment/billingmethodchoice.coffee",
-  "app/MainApp/payment/billingform.coffee",
-  "app/MainApp/payment/billingformmodal.coffee",
+  "app/MainApp/payment/paymentmethodchoice.coffee",
+  "app/MainApp/payment/paymentform.coffee",
+  "app/MainApp/payment/paymentformmodal.coffee",
   "app/MainApp/payment/paymentconfirmationmodal.coffee",
   "app/MainApp/payment/paymentdeleteconfirmationmodal.coffee",
   "app/MainApp/payment/vmproductview.coffee",
@@ -571,8 +571,8 @@

   # Group Dashboard Views:
   "app/Applications/Dashboard.kdapplication/Views/paymentmodalview.coffee",
-  "app/Applications/Dashboard.kdapplication/views/billingmethodview.coffee",
-  "app/Applications/Dashboard.kdapplication/views/linkablebillingmethodview.coffee",
+  "app/Applications/Dashboard.kdapplication/views/paymentmethodview.coffee",
+  "app/Applications/Dashboard.kdapplication/views/linkablepaymentmethodview.coffee",
   "app/Applications/Dashboard.kdapplication/views/paymentsettingsview.coffee",


Apps: Removed unused script includes.

Apps: Removed unused script includes.

--- client/landing/includes.coffee
+++ client/landing/includes.coffee
@@ -21,7 +21,7 @@
   # core
   "Framework/core/KDObject.coffee",
   "Framework/core/KDView.coffee",
-  "Framework/core/KDOverlayView.coffee",
+  "Framework/core/KDOverlayView.coffee",
   "Framework/core/JView.coffee",
   "Framework/core/KDCustomHTMLView.coffee",
   "Framework/core/KDScrollView.coffee",
@@ -614,8 +614,8 @@
   "app/Applications/Apps.kdapplication/Views/AppSubmission.coffee",
   "app/Applications/Apps.kdapplication/Views/appinfoview.coffee",
   "app/Applications/Apps.kdapplication/Views/appview.coffee",
-  "app/Applications/Apps.kdapplication/Views/appscreenshotlistitem.coffee",
-  "app/Applications/Apps.kdapplication/Views/appscreenshotsview.coffee",
+  # "app/Applications/Apps.kdapplication/Views/appscreenshotlistitem.coffee",
+  # "app/Applications/Apps.kdapplication/Views/appscreenshotsview.coffee",
   "app/Applications/Apps.kdapplication/Views/appdetailsview.coffee",

   "app/Applications/Apps.kdapplication/ContentDisplays/controller.coffee",


EPT-394 - isAvailable should return true (set the button disabled false), if rest api product attribute "forSale" returns true

--- public/models/product.coffee
+++ public/models/product.coffee
@@ -99,7 +99,7 @@
   isAvailable: ->
     $.getJSON @url()
       .done (response) =>
-        if response.forSale && response.availability != "OutStock"
+        if response.forSale
           $(".epages-shop-overlay-buy-button").prop('disabled', false)

     return "disabled"


account for spelling entry with two or more words

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -32,7 +32,9 @@
     entry = random_question(data)
     update_keyHandler()
     words = entry.name.split(" ").length
-    size = entry.name.length
+    size = 0
+    for word in words
+      size += word.length
     $("dd#hint_stat").empty().append("This word is " + size + " letters long.")
     $("dd#hint_define").empty().append(entry['hint'])
     $("dd#hint_sentence").empty().append(entry['example'])


fix #16

fix #16

--- lib/autohide-tree-view.coffee
+++ lib/autohide-tree-view.coffee
@@ -89,7 +89,7 @@
       @treeViewEl.style.position = 'relative'
       @treeViewEl.parentNode?.style?.width = ''
     else
-      @treeViewEl.style.position = 'absolute'
+      @treeViewEl.style.position = 'fixed'
       @treeViewEl.parentNode?.style?.width = "#{@conf.hiddenWidth}px"

   show: (noDelay = false) ->


chore: remove old script

chore: remove old script

--- transmogrifyArray.coffee
+++ transmogrifyArray.coffee
@@ -1,98 +0,0 @@
-# Start the timer
-console.time 'doneAfter'
-# Import FileSystem
-fs = require 'fs'
-
-# Read the JSON files (I used SQLiteStudio to export these from fuzzwork's SQLite conversions)
-mapSolarSystems = JSON.parse fs.readFileSync './mapSolarSystems.json', 'utf-8'
-mapSolarSystemJumps = JSON.parse fs.readFileSync './mapSolarSystemJumps.json', 'utf-8'
-mapRegions = JSON.parse fs.readFileSync './mapRegions.json', 'utf-8'
-
-# Create temporary lookup object for faster parsing later on
-regions = {}
-for mapRegion in mapRegions
-  regions[mapRegion.regionID] = mapRegion.regionName
-# Define the output object
-output = (
-  nodes: []
-  edges: []
-)
-
-
-# Process nodes
-for node in mapSolarSystems
-  # If it's not a W-Space system
-  if node.solarSystemID < 31000000
-    # Grab the data we want
-    newNode = {}
-    newNode.name = node.solarSystemName
-    newNode.id = node.solarSystemID
-    newNode.security = node.security
-    newNode.region = regions[node.regionID]
-    newNode.x = node.x
-    newNode.y = node.y
-    newNode.z = node.z
-    # Add it to the nodes object
-    output.nodes.push newNode
-
-
-# Process edges
-for edge in mapSolarSystemJumps
-  # Grab the data we want
-  newEdge = {}
-  newEdge.from = edge.fromSolarSystemID
-  newEdge.to = edge.toSolarSystemID
-  # Add it to the edges array
-  output.edges.push newEdge
-
-
-# Normalize coordinates
-coords = (
-  x: (
-    max: Number.NEGATIVE_INFINITY
-    min: Number.POSITIVE_INFINITY
-  )
-  y: (
-    max: Number.NEGATIVE_INFINITY
-    min: Number.POSITIVE_INFINITY
-  )
-  z: (
-    max: Number.NEGATIVE_INFINITY
-    min: Number.POSITIVE_INFINITY
-  )
-)
-# Find highest and lowest values for each axis
-for node in output.nodes
-  if node.x < coords.x.min then coords.x.min = node.x
-  if node.x > coords.x.max then coords.x.max = node.x
-  if node.y < coords.y.min then coords.y.min = node.y
-  if node.y > coords.y.max then coords.y.max = node.y
-  if node.z < coords.z.min then coords.z.min = node.z
-  if node.z > coords.z.max then coords.z.max = node.z
-# Store the absolute min values
-coords.x.minAbs = Math.abs(coords.x.min)
-coords.y.minAbs = Math.abs(coords.y.min)
-coords.z.minAbs = Math.abs(coords.z.min)
-# Offset all the coordinates
-for node in output.nodes
-  node.x += coords.x.minAbs
-  node.y += coords.y.minAbs
-  node.z += coords.z.minAbs
-# Also offset the max values
-coords.x.max += coords.x.minAbs
-coords.y.max += coords.y.minAbs
-coords.z.max += coords.z.minAbs
-# Convert to ratio of max
-for node in output.nodes
-  node.x = node.x/coords.x.max
-  node.y = node.y/coords.y.max
-  node.z = node.z/coords.z.max
-
-
-# Write to file
-fs.writeFileSync './universe_pretty.json', JSON.stringify(output, undefined, 2)
-
-# Report time taken
-console.timeEnd 'doneAfter'
-# Report status
-console.log 'Done processing map data.'


oops.

oops.

--- lib/closure-conversion.coffee
+++ lib/closure-conversion.coffee
@@ -157,7 +157,7 @@
                         n
                 else
                         if n.id?.name?
-                                global_name = util.genGlobalFunctionName n.id.name
+                                global_name = genGlobalFunctionName n.id.name
                                 @currentMapping()[n.id.name] = global_name
                         else
                                 global_name = genAnonymousFunctionName()


remove semicolon

remove semicolon

--- public/js/model/nicetohave/privilege.coffee
+++ public/js/model/nicetohave/privilege.coffee
@@ -28,7 +28,7 @@
         console.log("Error: #{m}")
       ,
       scope: level.trelloScope
-    });
+    })

 window.nicetohave.Privilege = Privilege


fix #27 #28 : add defense code on controller completion

--- lib/controllerProvider.coffee
+++ lib/controllerProvider.coffee
@@ -58,20 +58,21 @@
           curTagName = viewAutoProvider.getPreviousTag(sourceEditor,item.range.start);
           item.stop();

-        if curTagName
+        if curTagName && @completions.tags[curTagName]
           apiName = @completions.tags[curTagName].apiName
           curTagObject = @completions.types[apiName]
-          _.each curTagObject.functions, (value)->
-            completions.push
-              type: 'function'
-              text: value
-              rightLabel: "<#{curTagName}>"
-
-          _.each curTagObject.properties, (value)->
-            completions.push
-              type: 'attribute'
-              text: value
-              rightLabel: "<#{curTagName}>"
+          if curTagObject
+            _.each curTagObject.functions, (value)->
+              completions.push
+                type: 'function'
+                text: value
+                rightLabel: "<#{curTagName}>"
+
+            _.each curTagObject.properties, (value)->
+              completions.push
+                type: 'attribute'
+                text: value
+                rightLabel: "<#{curTagName}>"
     else
       completions = @getPropertyNameCompletions(request)


Enable stub POST /schedule

Enable stub POST /schedule

--- api/resources/schedule.coffee
+++ api/resources/schedule.coffee
@@ -2,7 +2,7 @@

 exports.register = (server, baseRoute) ->
   server.get "#{baseRoute}/schedule", get
-#  server.post "#{baseRoute}/schedule", post
+  server.post "#{baseRoute}/schedule", post

 responseType = '404'

@@ -57,3 +57,12 @@
         responseType = '404'

   next()
+
+post = (req, res, next) ->
+  if not req.header 'Authorization'
+    res.send new restify.InvalidCredentialsError "Not so fast."
+
+  else
+    res.send 201
+
+  next()


FIX: The typo in the rollover teaching period

--- src/app/teaching-periods/modals/rollover-teaching-period-modal/rollover-teaching-period-modal.coffee
+++ src/app/teaching-periods/modals/rollover-teaching-period-modal/rollover-teaching-period-modal.coffee
@@ -23,7 +23,7 @@

     TeachingPeriod.rollover.create { existing_teaching_period_id: $scope.teachingperiod.id, new_teaching_period_id: $scope.rolloverTo },
       (createdTeachingPeriod) ->
-        $scope.teachingperiods.loadedPeriods.push(createdTeachingPeriod)
+        $scope.teachingPeriods.loadedPeriods.push(createdTeachingPeriod)
         alertService.add("success", "Teaching Period created.", 2000)
       (response) ->
         alertService.add("danger", response.data.error, 6000)


mark sku as required

mark sku as required

--- client/skr/models/Event.coffee
+++ client/skr/models/Event.coffee
@@ -17,6 +17,6 @@

     associations:
         photo:         { model: "Lanes.Models.Asset" }
-        sku:           { model: "Sku" }
+        sku:           { model: "Sku", required: true }
         invoices:      { collection: "Invoice" }
         invoice_lines: { collection: "InvLine" }


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- source/javascripts/_test/string_extensions.coffee
+++ source/javascripts/_test/string_extensions.coffee
@@ -55,7 +55,7 @@

   equals "F".toInt(16), 15

-  equals "-1."
+  equals "-1.02".toInt()

 test "#underscore", ->
   equals "Pro-tip".underscore(), "pro_tip"


it's not hint, but definition

it's not hint, but definition

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -41,7 +41,7 @@
     else
       last = "word"
     $("dd#hint_stat").empty().append("This entry is " + size + " letters long and " + words.length + " " + last + " long.")
-    $("dd#hint_define").empty().append(entry['hint'])
+    $("dd#hint_define").empty().append(entry['definition'])
     $("dd#hint_sentence").empty().append(entry['example'])

   emptyInput = () ->


add new function emptyInput

add new function emptyInput

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -29,6 +29,8 @@
     $("p#hint_define").empty().append(word['hint'])
     $("p#hint_sentence").empty().append(word['example'])

+  emptyInput = () ->
+    $("input#input_answer").val("")

   update_keyHandler = () ->
     $("#input_answer").off()
@@ -46,5 +48,5 @@
           else
             last = " time."
           $(".alert").empty().append("You have failed " + @fail + last)
-          $("input#input_answer").val("")
-    )
+          emptyInput()
+      )


append to reveal

append to reveal

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -69,6 +69,7 @@
             last = " time."
           if @fail % 2 == 0
             randomLetter()
+            $("#revealed").empty().append(@reveal)
           $(".alert").empty().append("You have failed " + @fail + last)
           emptyInput()
       )


realized that string are immutable

realized that string are immutable

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -45,7 +45,7 @@
     while breaking < 500
       breaking += 1
       if @reveal[n] == "_"
-        @reveal[n] = @current[n]
+        @reveal = @reveal.substring(0,n) + @current[n] + @reveal.substring(n + 1)
         return

   update_keyHandler = () ->


intermediate commit - this returns true

intermediate commit - this returns true

--- app/assets/javascripts/hanuman/models/question.js.coffee
+++ app/assets/javascripts/hanuman/models/question.js.coffee
@@ -22,4 +22,11 @@
     if @.get('ancestry')?
       Array(@.get('ancestry').split('/').length + 1).join("==> ")
   ).property('ancestry')
+
+  isContainer: (->
+    if @.get('answerType').get('name') is 'section'
+      return true
+    else
+      return false
+  ).property('answerType')
 })


intermittently, tinyMCE would not init on editing discussion

test plan: load edit/create announcement/disucssion
page, make sure the rich text editor inits every time

make sure you try on IE8

Change-Id: Ife8beafb6d0e932cfd49c08377a30329f4a28734
fixes: #10126
Reviewed-on: https://gerrit.instructure.com/13190
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Ryan Shaw <ea3cd978650417470535f3a4725b6b5042a6ab59@instructure.com>

--- app/coffeescripts/views/DiscussionTopics/EditView.coffee
+++ app/coffeescripts/views/DiscussionTopics/EditView.coffee
@@ -42,7 +42,10 @@
       unless wikiSidebar.inited
         wikiSidebar.init()
         $.scrollSidebar()
-      wikiSidebar.attachToEditor @$('textarea[name=message]').attr('id', _.uniqueId('discussion-topic-message')).editorBox()
+      $textarea = @$('textarea[name=message]').attr('id', _.uniqueId('discussion-topic-message'))
+      _.defer -> $textarea.editorBox()
+      wikiSidebar.attachToEditor $textarea
+
       wikiSidebar.show()

       if @assignmentGroupCollection


mark temporary packs with a last_checked date in the far future

they do not need to be checked for archiving

--- app/coffee/PackManager.coffee
+++ app/coffee/PackManager.coffee
@@ -95,6 +95,7 @@
 			temporary: temporary
 		if temporary
 			newPack.expiresAt = new Date(Date.now() + 7 * DAYS)
+			newPack.last_checked = new Date(Date.now() + 30 * DAYS) # never check temporary packs
 		logger.log {project_id, doc_id, newUpdates}, "inserting updates into new pack"
 		db.docHistory.save newPack, (err, result) ->
 			return callback(err) if err?


add pass nodes to test SGFs for marking stones

--- app/coffee/tests/tests.coffee
+++ app/coffee/tests/tests.coffee
@@ -433,7 +433,7 @@
   assert.ok isPointDame($point), "still dame after click"

 test "mixed scoring board", (assert) ->
-  setInputSgf '(;SZ[3];B[ba];W[bb];B[ab];W[cb];B[cc];W[bc])'
+  setInputSgf '(;SZ[3];B[ba];W[bb];B[ab];W[cb];B[cc];W[bc];B[];W[])'
   # .b.
   # bww
   # .w.
@@ -448,7 +448,7 @@

 test "clicking live stones makes them dead, " + \
      "clicking again brings them back", (assert) ->
-  setInputSgf '(;SZ[3];B[aa];W[ab];B[bb];W[ca];B[bc];W[cc];B[ac])'
+  setInputSgf '(;SZ[3];B[aa];W[ab];B[bb];W[ca];B[bc];W[cc];B[ac];W[];B[])'
   # b.w
   # .b.  (white captured at ab)
   # bbw
@@ -486,7 +486,7 @@

 test "killing stones revives neighbouring enemy groups " + \
      "automatically", (assert) ->
-  setInputSgf '(;SZ[3];AB[aa][ca][bb][ac][bc]AW[cc])'
+  setInputSgf '(;SZ[3];AB[aa][ca][bb][ac][bc]AW[cc];B[];W[])'
   # b.b
   # .b.
   # bbw
@@ -511,7 +511,7 @@
   assert.ok isPointWhiteScore($pointAt 1, 1), "(1, 1) scores for White"

 test "Form is updated with current dead stones", (assert) ->
-  setInputSgf '(;SZ[3];AB[aa][ca][bb][ac][ab]AW[cb])'
+  setInputSgf '(;SZ[3];AB[aa][ca][bb][ac][ab]AW[cb];B[];W[])'
   # b.b
   # .b.
   # bbw


add routes for category management

add routes for category management

--- app/controllers/routes.coffee
+++ app/controllers/routes.coffee
@@ -128,7 +128,7 @@
     #     `Get posts by Category`
     #    ----------------------------
     #
-    app.get '/blog/Category/:name/posts.json', (req, res) ->
+    app.get '/blog/category/:name/posts.json', (req, res) ->
       Entry.find {'category': req.params.name}, (err, data) ->
         if err
           res.json result: 'error'
@@ -136,7 +136,70 @@
           res.json
             result: 'OK'
             entries: data
-
+    #
+    #     `Get Categories`
+    #    ----------------------------
+    #
+    app.get '/blog/categories.json', (req, res) ->
+      Category.find {}, (err, data) ->
+        if err
+          res.json result: 'error'
+        else
+          res.json
+            result: 'OK'
+            entries: data
+
+    #
+    #     `Get Categories`
+    #    ----------------------------
+    #
+    app.get '/blog/category/:name.json', (req, res) ->
+      Category.find {name: req.params.name}, (err, data) ->
+        if err
+          res.json result: 'error'
+        else
+          res.json
+            result: 'OK'
+            entries: data
+
+    #
+    #     `Post Categories`
+    #    ----------------------------
+    #
+    app.post '/blog/category/:name.json', (req, res) ->
+      newCategory = new Category(
+        name: req.body.name,
+        description: req.body.description
+      )
+
+      newCategory.save (err) ->
+        unless err
+          res.json result: 'OK'
+        else
+          res.json
+            result: 'error'
+            err: err
+
+    app.put '/blog/category/:name.json', (req, res) ->
+      Category.findOneAndUpdate 'name': req.params.name,
+        req.body,
+        new: true,
+          (err, data) ->
+            unless err
+              res.json result: 'OK'
+            else
+              res.json
+                result: 'error'
+                err: err
+
+    app.del '/blog/category/:name.json', (req, res) ->
+      Category.remove 'name': req.params.name, (err, data) ->
+        if data > 0 and not err
+          res.json result: 'OK'
+        else
+          res.json
+            result: 'error'
+            err: err

   site: (app) ->


Remove extra hide. Fix focus scope call.

--- lib/simple-flake8-view.coffee
+++ lib/simple-flake8-view.coffee
@@ -59,18 +59,14 @@
     ignoreErrors: ""
     mcCabeComplexityThreshold: ""
     validateOnSave: true
+  keyBindings: null

   @activate: ->
     view = new SimpleFlake8View
     @disposable = atom.commands.add 'atom-workspace', 'simple-flake8:toggle', -> view.toggle()

-  # @activate: ->
-  #   new SimpleFlake8View
-
   @deactivate: ->
     @disposable.dispose()
-
-  cancelled: -> @hide()

   activate: (state) ->
     atom.workspaceView.command 'simple-flake8:toggle', => @toggle()
@@ -86,6 +82,8 @@
   getFilterKey: ->
     'message'

+  cancelled: -> @hide()
+
   toggle: ->
     if @panel?.isVisible()
     # if @hasParent()
@@ -93,15 +91,11 @@
     else
       @attach()

-  hide: ->
-    @panel?.hide()
-
-  # attach: ->
   show: ->
-
-    # editor = atom.workspace.getActiveEditor()
-    # return unless editor?
-    # return unless editor.getGrammar().name == 'Python'
+    # Get out of here unless this is a python file
+    editor = atom.workspace.getActiveEditor()
+    return unless editor?
+    return unless editor.getGrammar().name == 'Python'

     @panel ?= atom.workspace.addModalPanel(item: this)
@@ -109,16 +103,13 @@

     @storeFocusedElement()

+    @setLoading('Running Flake8 Linter...')
+
     if @previouslyFocusedElement[0] and @previouslyFocusedElement[0] isnt document.body
       @eventElement = @previouslyFocusedElement[0]
     else
       @eventElement = atom.views.getView(atom.workspace)
-    @keyBindings = atom.keymap.findKeyBindings(target: @eventElement)

-    @setLoading('Running Flake8 Linter...')
-
-    # atom.workspaceView.append(this)
-    # @focusFilterEditor()
     filePath = editor.getPath()

     flake filePath, (errors) =>
@@ -135,7 +126,7 @@
         error_list.push(error)
       @setItems(error_list)

-      @focusFilterEditor()
+    @focusFilterEditor()

   hide: ->
     @panel?.hide()


Modified bootstrap, but not enough.  To figure out what's going on with tags quickly, I should just get to work on the tagbox.

--- server/bootstrap.coffee
+++ server/bootstrap.coffee
@@ -109,7 +109,16 @@
         author_id: 1
         content: 'I\'m a child.'
         parent_id: 0
-        tags:{}
+        tags: {
+          'one': {
+            users: [1, 2]
+            weight: 2
+          }
+          'red': {
+            users: [1]
+            weight: 3
+          }
+        }
         votes:{
             'up': {
               users: []


Fix shortcuts help menu

Fix shortcuts help menu

--- app/js/views/shortcuts.coffee
+++ app/js/views/shortcuts.coffee
@@ -1,5 +1,4 @@
 class app.Views.Shortcuts extends Backbone.View
-  el: document.body # otherwise it won't capture all the shortcuts
   template: JST['app/templates/shortcuts.us']

   shortcuts:
@@ -40,6 +39,8 @@
     'ctrl+`': 'toggleDevelopmentMode'

   initialize: (options) ->
+    @setElement document.body # otherwise it won't capture all the shortcuts
+
     @repositories = options.repositories
     @notifications = options.notifications


nudge mask check by annotation radius

nudge mask check by annotation radius

--- app/lib/tutorial_steps.coffee
+++ app/lib/tutorial_steps.coffee
@@ -76,7 +76,7 @@
       'mouseup svg.primary': (e, tutorial, step) ->
         position = $('.current .image').position()
         x = e.pageX - position.left
-        y = e.pageY - position.top
+        y = e.pageY - position.top - 10 # Subtract the radius 10 from the coordinate

         pixel = step.ctx.getImageData(x, y, 1, 1)
         mask = pixel.data[3]


payments: plumbing

payments: plumbing

add some essential plumbing to payment settings view

--- client/landing/app/Applications/Dashboard.kdapplication/views/paymentsettingsview.coffee
+++ client/landing/app/Applications/Dashboard.kdapplication/views/paymentsettingsview.coffee
@@ -37,7 +37,6 @@
         billing             :
           label             : "Billing Method"
           itemClass         : BillingMethodView
-          data              : group
         history             :
           label             : "Payment History"
           tagName           : "a"
@@ -113,6 +112,8 @@

     @settingsForm = new KDFormViewWithFields formOptions, group

+    @forwardEvent @settingsForm.inputs.billing, 'BillingEditRequested'
+
     group.fetchBundle (err, bundle)=>
       if err or not bundle
         return
@@ -136,4 +137,8 @@
         inputs.overagePolicy.setOn()
         fields.approval.show()

+  setBillingInfo: (billingInfo) ->
+    { billing: billingView } = @settingsForm.inputs
+    billingView.setBillingInfo billingInfo
+
   pistachio:-> "{{> @settingsForm}}"


accountpopup: fetch and update payment label when view appended

--- client/app/lib/avatararea/accountpopup.coffee
+++ client/app/lib/avatararea/accountpopup.coffee
@@ -61,6 +61,11 @@
         return  unless success
         dashboardLink.show()
         adminLink.show()
+
+
+  viewAppended: ->
+
+    @updatePaymentTitle()

   updatePaymentTitle: ->


dance dance dance

dance dance dance

--- roles/hubot/files/scripts/dealwithit.coffee
+++ roles/hubot/files/scripts/dealwithit.coffee
@@ -16,7 +16,8 @@
   "http://i.imgur.com/GxYr5.gif",
   "http://i.imgur.com/UHww5.gif",
   "http://i.imgur.com/qp0eh.gif",
-  "http://i.imgur.com/JnhJU.png"
+  "http://i.imgur.com/JnhJU.png",
+  "http://i.imgur.com/RZ66LoS.gif"
 ]

 module.exports = (robot) ->


fix: reloads the page when a guess is correct

--- src/app/rounds/rounds.coffee
+++ src/app/rounds/rounds.coffee
@@ -33,9 +33,9 @@
 ]

 class RoundsCtrl
-  @$inject = ['$scope', 'RoundsRes', '$route']
+  @$inject = ['$scope', 'RoundsRes', '$state']

-  constructor: ($scope, RoundsRes, $route) ->
+  constructor: ($scope, RoundsRes, $state) ->
     $scope.type = 'series'
     all_tags = {
       'series': ["adventure time", "supernatural", "doctor who", "lotr", "harry potter"],
@@ -55,7 +55,7 @@
       $scope.correct = $scope.tag_regex.test(guess)

     $scope.$watch "correct", (correct) ->
-      $route.reload if correct
+      $state.go($state.$current, null, { reload: true }) if correct

 rounds.controller 'RoundsCtrl', RoundsCtrl


collaboration: controller fix quit calls for leave

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -595,8 +595,6 @@

     if origin is nick()
       switch type
-        when 'ParticipantWantsToLeave'
-          return @quit()
         when 'ParticipantKicked'
           return @handleParticipantKicked data.target
         else return
@@ -1011,10 +1009,10 @@
       options = channelId: @socialChannel.getId()
       kd.singletons.socialapi.channel.leave options, (err) =>
         return showError err  if err
-        @setMachineUser [nick()], no, kd.noop
-        # remove the leaving participant's info from the collaborative doc
-        @removeParticipant nick()
-        @quit()
+        @setMachineUser [nick()], no, =>
+          # remove the leaving participant's info from the collaborative doc
+          @removeParticipant nick()
+          @quit()

   throwError: throwError = (err, args...) ->