       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")

{readFileSync} = fs
{join} = require 'path'
temp = require 'temp'
TextBuffer = require '../src/text-buffer'
SampleText = readFileSync(join(__dirname, 'fixtures', 'sample.js'), 'utf8')

describe "TextBuffer", ->
  buffer = null

  describe "construction", ->
    it "can be constructed empty", ->


      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"
