diff --git a/spec/display-marker-layer-spec.coffee b/spec/display-marker-layer-spec.coffee deleted file mode 100644 index 2761975c07..0000000000 --- a/spec/display-marker-layer-spec.coffee +++ /dev/null @@ -1,413 +0,0 @@ -TextBuffer = require '../src/text-buffer' -Point = require '../src/point' -Range = require '../src/range' -SampleText = require './helpers/sample-text' - -describe "DisplayMarkerLayer", -> - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - - it "allows DisplayMarkers to be created and manipulated in screen coordinates", -> - buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - - marker = markerLayer.markScreenRange([[3, 4], [4, 2]]) - expect(marker.getScreenRange()).toEqual [[3, 4], [4, 2]] - expect(marker.getBufferRange()).toEqual [[3, 2], [4, 2]] - - markerChangeEvents = [] - marker.onDidChange (change) -> markerChangeEvents.push(change) - - marker.setScreenRange([[3, 8], [4, 3]]) - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 3]]) - expect(marker.getScreenRange()).toEqual([[3, 8], [4, 3]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 2] - newHeadBufferPosition: [4, 3] - oldTailBufferPosition: [3, 2] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [4, 2] - newHeadScreenPosition: [4, 3] - oldTailScreenPosition: [3, 4] - newTailScreenPosition: [3, 8] - wasValid: true - isValid: true - textChanged: false - } - - markerChangeEvents = [] - buffer.insert([4, 0], '\t') - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) - expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 3] - newHeadBufferPosition: [4, 4] - oldTailBufferPosition: [3, 4] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [4, 3] - newHeadScreenPosition: [4, 7] - oldTailScreenPosition: [3, 8] - newTailScreenPosition: [3, 8] - wasValid: true - isValid: true - textChanged: true - } - - expect(markerLayer.getMarker(marker.id)).toBe marker - - markerChangeEvents = [] - foldId = displayLayer.foldBufferRange([[0, 2], [2, 2]]) - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) - expect(marker.getScreenRange()).toEqual([[1, 8], [2, 7]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 4] - newHeadBufferPosition: [4, 4] - oldTailBufferPosition: [3, 4] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [4, 7] - newHeadScreenPosition: [2, 7] - oldTailScreenPosition: [3, 8] - newTailScreenPosition: [1, 8] - wasValid: true - isValid: true - textChanged: false - } - - markerChangeEvents = [] - displayLayer.destroyFold(foldId) - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) - expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 4] - newHeadBufferPosition: [4, 4] - oldTailBufferPosition: [3, 4] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [2, 7] - newHeadScreenPosition: [4, 7] - oldTailScreenPosition: [1, 8] - newTailScreenPosition: [3, 8] - wasValid: true - isValid: true - textChanged: false - } - - markerChangeEvents = [] - displayLayer.reset({tabLength: 3}) - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) - expect(marker.getScreenRange()).toEqual([[3, 6], [4, 6]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 4] - newHeadBufferPosition: [4, 4] - oldTailBufferPosition: [3, 4] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [4, 7] - newHeadScreenPosition: [4, 6] - oldTailScreenPosition: [3, 8] - newTailScreenPosition: [3, 6] - wasValid: true - isValid: true - textChanged: false - } - - it "does not create duplicate DisplayMarkers when it has onDidCreateMarker observers (regression)", -> - buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - - emittedMarker = null - markerLayer.onDidCreateMarker (marker) -> - emittedMarker = marker - - createdMarker = markerLayer.markBufferRange([[0, 1], [2, 3]]) - expect(createdMarker).toBe(emittedMarker) - - it "emits events when markers are created and destroyed", -> - buffer = new TextBuffer(text: 'hello world') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - createdMarkers = [] - markerLayer.onDidCreateMarker (m) -> createdMarkers.push(m) - marker = markerLayer.markScreenRange([[0, 4], [1, 4]]) - - expect(createdMarkers).toEqual [marker] - - destroyEventCount = 0 - marker.onDidDestroy -> destroyEventCount++ - - marker.destroy() - expect(destroyEventCount).toBe 1 - - it "emits update events when markers are created, updated directly, updated indirectly, or destroyed", (done) -> - buffer = new TextBuffer(text: 'hello world') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - marker = null - - updateEventCount = 0 - markerLayer.onDidUpdate -> - updateEventCount++ - if updateEventCount is 1 - marker.setScreenRange([[0, 5], [1, 0]]) - else if updateEventCount is 2 - buffer.insert([0, 0], '\t') - else if updateEventCount is 3 - marker.destroy() - else if updateEventCount is 4 - done() - - buffer.transact -> - marker = markerLayer.markScreenRange([[0, 4], [1, 4]]) - - it "allows markers to be copied", -> - buffer = new TextBuffer(text: '\ta\tbc\tdef\tg\n\th') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - - markerA = markerLayer.markScreenRange([[0, 4], [1, 4]], a: 1, b: 2) - markerB = markerA.copy(b: 3, c: 4) - - expect(markerB.id).not.toBe(markerA.id) - expect(markerB.getProperties()).toEqual({a: 1, b: 3, c: 4}) - expect(markerB.getScreenRange()).toEqual(markerA.getScreenRange()) - - describe "::destroy()", -> - it "only destroys the underlying buffer MarkerLayer if the DisplayMarkerLayer was created by calling addMarkerLayer on its parent DisplayLayer", -> - buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') - displayLayer1 = buffer.addDisplayLayer(tabLength: 2) - displayLayer2 = buffer.addDisplayLayer(tabLength: 4) - bufferMarkerLayer = buffer.addMarkerLayer() - bufferMarker1 = bufferMarkerLayer.markRange [[2, 1], [2, 2]] - displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) - displayMarker1 = displayMarkerLayer1.markBufferRange [[1, 0], [1, 2]] - displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) - displayMarker2 = displayMarkerLayer2.markBufferRange [[2, 0], [2, 1]] - displayMarkerLayer3 = displayLayer2.addMarkerLayer() - displayMarker3 = displayMarkerLayer3.markBufferRange [[0, 0], [0, 0]] - - displayMarkerLayer1DestroyEventCount = 0 - displayMarkerLayer1.onDidDestroy -> displayMarkerLayer1DestroyEventCount++ - displayMarkerLayer2DestroyEventCount = 0 - displayMarkerLayer2.onDidDestroy -> displayMarkerLayer2DestroyEventCount++ - displayMarkerLayer3DestroyEventCount = 0 - displayMarkerLayer3.onDidDestroy -> displayMarkerLayer3DestroyEventCount++ - - displayMarkerLayer1.destroy() - expect(bufferMarkerLayer.isDestroyed()).toBe(false) - expect(displayMarkerLayer1.isDestroyed()).toBe(true) - expect(displayMarkerLayer1DestroyEventCount).toBe(1) - expect(bufferMarker1.isDestroyed()).toBe(false) - expect(displayMarker1.isDestroyed()).toBe(true) - expect(displayMarker2.isDestroyed()).toBe(false) - expect(displayMarker3.isDestroyed()).toBe(false) - - displayMarkerLayer2.destroy() - expect(bufferMarkerLayer.isDestroyed()).toBe(false) - expect(displayMarkerLayer2.isDestroyed()).toBe(true) - expect(displayMarkerLayer2DestroyEventCount).toBe(1) - expect(bufferMarker1.isDestroyed()).toBe(false) - expect(displayMarker1.isDestroyed()).toBe(true) - expect(displayMarker2.isDestroyed()).toBe(true) - expect(displayMarker3.isDestroyed()).toBe(false) - - bufferMarkerLayer.destroy() - expect(bufferMarkerLayer.isDestroyed()).toBe(true) - expect(displayMarkerLayer1DestroyEventCount).toBe(1) - expect(displayMarkerLayer2DestroyEventCount).toBe(1) - expect(bufferMarker1.isDestroyed()).toBe(true) - expect(displayMarker1.isDestroyed()).toBe(true) - expect(displayMarker2.isDestroyed()).toBe(true) - expect(displayMarker3.isDestroyed()).toBe(false) - - displayMarkerLayer3.destroy() - expect(displayMarkerLayer3.bufferMarkerLayer.isDestroyed()).toBe(true) - expect(displayMarkerLayer3.isDestroyed()).toBe(true) - expect(displayMarkerLayer3DestroyEventCount).toBe(1) - expect(displayMarker3.isDestroyed()).toBe(true) - - it "destroys the layer's markers", -> - buffer = new TextBuffer() - displayLayer = buffer.addDisplayLayer() - displayMarkerLayer = displayLayer.addMarkerLayer() - - marker1 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]) - marker2 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]) - - destroyListener = jasmine.createSpy('onDidDestroy listener') - marker1.onDidDestroy(destroyListener) - - displayMarkerLayer.destroy() - - expect(destroyListener).toHaveBeenCalled() - expect(marker1.isDestroyed()).toBe(true) - - # Markers states are updated regardless of whether they have an - # ::onDidDestroy listener - expect(marker2.isDestroyed()).toBe(true) - - it "destroys display markers when their underlying buffer markers are destroyed", -> - buffer = new TextBuffer(text: '\tabc') - displayLayer1 = buffer.addDisplayLayer(tabLength: 2) - displayLayer2 = buffer.addDisplayLayer(tabLength: 4) - bufferMarkerLayer = buffer.addMarkerLayer() - displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) - displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) - - bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]) - - displayMarker1 = displayMarkerLayer1.getMarker(bufferMarker.id) - displayMarker2 = displayMarkerLayer2.getMarker(bufferMarker.id) - expect(displayMarker1.getScreenRange()).toEqual([[0, 2], [0, 3]]) - expect(displayMarker2.getScreenRange()).toEqual([[0, 4], [0, 5]]) - - displayMarker1DestroyCount = 0 - displayMarker2DestroyCount = 0 - displayMarker1.onDidDestroy -> displayMarker1DestroyCount++ - displayMarker2.onDidDestroy -> displayMarker2DestroyCount++ - - bufferMarker.destroy() - expect(displayMarker1DestroyCount).toBe(1) - expect(displayMarker2DestroyCount).toBe(1) - - it "does not throw exceptions when buffer markers are destroyed that don't have corresponding display markers", -> - buffer = new TextBuffer(text: '\tabc') - displayLayer1 = buffer.addDisplayLayer(tabLength: 2) - displayLayer2 = buffer.addDisplayLayer(tabLength: 4) - bufferMarkerLayer = buffer.addMarkerLayer() - displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) - displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) - - bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]) - bufferMarker.destroy() - - it "destroys itself when the underlying buffer marker layer is destroyed", -> - buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') - displayLayer1 = buffer.addDisplayLayer(tabLength: 2) - displayLayer2 = buffer.addDisplayLayer(tabLength: 4) - - bufferMarkerLayer = buffer.addMarkerLayer() - displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) - displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) - displayMarkerLayer1DestroyEventCount = 0 - displayMarkerLayer1.onDidDestroy -> displayMarkerLayer1DestroyEventCount++ - displayMarkerLayer2DestroyEventCount = 0 - displayMarkerLayer2.onDidDestroy -> displayMarkerLayer2DestroyEventCount++ - - bufferMarkerLayer.destroy() - expect(displayMarkerLayer1.isDestroyed()).toBe(true) - expect(displayMarkerLayer1DestroyEventCount).toBe(1) - expect(displayMarkerLayer2.isDestroyed()).toBe(true) - expect(displayMarkerLayer2DestroyEventCount).toBe(1) - - describe "findMarkers(params)", -> - [markerLayer, displayLayer] = [] - - beforeEach -> - buffer = new TextBuffer(text: SampleText) - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - - it "allows the startBufferRow and endBufferRow to be specified", -> - marker1 = markerLayer.markBufferRange([[0, 0], [3, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[0, 0], [5, 0]], class: 'a') - marker3 = markerLayer.markBufferRange([[9, 0], [10, 0]], class: 'b') - - expect(markerLayer.findMarkers(class: 'a', startBufferRow: 0)).toEqual [marker2, marker1] - expect(markerLayer.findMarkers(class: 'a', startBufferRow: 0, endBufferRow: 3)).toEqual [marker1] - expect(markerLayer.findMarkers(endBufferRow: 10)).toEqual [marker3] - - it "allows the startScreenRow and endScreenRow to be specified", -> - marker1 = markerLayer.markBufferRange([[6, 0], [7, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[9, 0], [10, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', startScreenRow: 6, endScreenRow: 7)).toEqual [marker2] - - displayLayer.destroyFoldsIntersectingBufferRange([[4, 0], [7, 0]]) - displayLayer.foldBufferRange([[0, 20], [12, 2]]) - marker3 = markerLayer.markBufferRange([[12, 0], [12, 0]], class: 'a') - expect(markerLayer.findMarkers(class: 'a', startScreenRow: 0)).toEqual [marker1, marker2, marker3] - expect(markerLayer.findMarkers(class: 'a', endScreenRow: 0)).toEqual [marker1, marker2, marker3] - - it "allows the startsInBufferRange/endsInBufferRange and startsInScreenRange/endsInScreenRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 2], [5, 4]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 2]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', startsInBufferRange: [[5, 1], [5, 3]])).toEqual [marker1] - expect(markerLayer.findMarkers(class: 'a', endsInBufferRange: [[8, 1], [8, 3]])).toEqual [marker2] - expect(markerLayer.findMarkers(class: 'a', startsInScreenRange: [[4, 0], [4, 1]])).toEqual [marker1] - expect(markerLayer.findMarkers(class: 'a', endsInScreenRange: [[5, 1], [5, 3]])).toEqual [marker2] - - it "allows intersectsBufferRowRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', intersectsBufferRowRange: [5, 6])).toEqual [marker1] - - it "allows intersectsScreenRowRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 10])).toEqual [marker2] - - displayLayer.destroyAllFolds() - displayLayer.foldBufferRange([[0, 20], [12, 2]]) - expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [0, 0])).toEqual [marker1, marker2] - - displayLayer.destroyAllFolds() - displayLayer.reset({softWrapColumn: 10}) - marker1.setHeadScreenPosition([6, 5]) - marker2.setHeadScreenPosition([9, 2]) - expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 7])).toEqual [marker1] - - it "allows containedInScreenRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containedInScreenRange: [[5, 0], [7, 0]])).toEqual [marker2] - - it "allows intersectsBufferRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', intersectsBufferRange: [[5, 0], [6, 0]])).toEqual [marker1] - - it "allows intersectsScreenRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', intersectsScreenRange: [[5, 0], [10, 0]])).toEqual [marker2] - - it "allows containsBufferPosition to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containsBufferPosition: [8, 0])).toEqual [marker2] - - it "allows containsScreenPosition to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containsScreenPosition: [5, 0])).toEqual [marker2] - - it "allows containsBufferRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containsBufferRange: [[8, 2], [8, 4]])).toEqual [marker2] - - it "allows containsScreenRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containsScreenRange: [[5, 2], [5, 4]])).toEqual [marker2] - - it "works when used from within a Marker.onDidDestroy callback (regression)", -> - displayMarker = markerLayer.markBufferRange([[0, 3], [0, 6]]) - displayMarker.onDidDestroy -> - expect(markerLayer.findMarkers({containsBufferPosition: [0, 4]})).not.toContain(displayMarker) - displayMarker.destroy() diff --git a/spec/display-marker-layer-spec.js b/spec/display-marker-layer-spec.js new file mode 100644 index 0000000000..8baeea4967 --- /dev/null +++ b/spec/display-marker-layer-spec.js @@ -0,0 +1,438 @@ +const TextBuffer = require('../src/text-buffer'); +const Point = require('../src/point'); +const Range = require('../src/range'); +const SampleText = require('./helpers/sample-text'); + +describe("DisplayMarkerLayer", function() { + beforeEach(() => jasmine.addCustomEqualityTester(require("underscore-plus").isEqual)); + + it("allows DisplayMarkers to be created and manipulated in screen coordinates", function() { + const buffer = new TextBuffer({text: 'abc\ndef\nghi\nj\tk\tl\nmno'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + + const marker = markerLayer.markScreenRange([[3, 4], [4, 2]]); + expect(marker.getScreenRange()).toEqual([[3, 4], [4, 2]]); + expect(marker.getBufferRange()).toEqual([[3, 2], [4, 2]]); + + let markerChangeEvents = []; + marker.onDidChange(change => markerChangeEvents.push(change)); + + marker.setScreenRange([[3, 8], [4, 3]]); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 3]]); + expect(marker.getScreenRange()).toEqual([[3, 8], [4, 3]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 2], + newHeadBufferPosition: [4, 3], + oldTailBufferPosition: [3, 2], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [4, 2], + newHeadScreenPosition: [4, 3], + oldTailScreenPosition: [3, 4], + newTailScreenPosition: [3, 8], + wasValid: true, + isValid: true, + textChanged: false + }); + + markerChangeEvents = []; + buffer.insert([4, 0], '\t'); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]); + expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 3], + newHeadBufferPosition: [4, 4], + oldTailBufferPosition: [3, 4], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [4, 3], + newHeadScreenPosition: [4, 7], + oldTailScreenPosition: [3, 8], + newTailScreenPosition: [3, 8], + wasValid: true, + isValid: true, + textChanged: true + }); + + expect(markerLayer.getMarker(marker.id)).toBe(marker); + + markerChangeEvents = []; + const foldId = displayLayer.foldBufferRange([[0, 2], [2, 2]]); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]); + expect(marker.getScreenRange()).toEqual([[1, 8], [2, 7]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 4], + newHeadBufferPosition: [4, 4], + oldTailBufferPosition: [3, 4], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [4, 7], + newHeadScreenPosition: [2, 7], + oldTailScreenPosition: [3, 8], + newTailScreenPosition: [1, 8], + wasValid: true, + isValid: true, + textChanged: false + }); + + markerChangeEvents = []; + displayLayer.destroyFold(foldId); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]); + expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 4], + newHeadBufferPosition: [4, 4], + oldTailBufferPosition: [3, 4], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [2, 7], + newHeadScreenPosition: [4, 7], + oldTailScreenPosition: [1, 8], + newTailScreenPosition: [3, 8], + wasValid: true, + isValid: true, + textChanged: false + }); + + markerChangeEvents = []; + displayLayer.reset({tabLength: 3}); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]); + expect(marker.getScreenRange()).toEqual([[3, 6], [4, 6]]); + return expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 4], + newHeadBufferPosition: [4, 4], + oldTailBufferPosition: [3, 4], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [4, 7], + newHeadScreenPosition: [4, 6], + oldTailScreenPosition: [3, 8], + newTailScreenPosition: [3, 6], + wasValid: true, + isValid: true, + textChanged: false + }); +}); + + it("does not create duplicate DisplayMarkers when it has onDidCreateMarker observers (regression)", function() { + const buffer = new TextBuffer({text: 'abc\ndef\nghi\nj\tk\tl\nmno'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + + let emittedMarker = null; + markerLayer.onDidCreateMarker(marker => emittedMarker = marker); + + const createdMarker = markerLayer.markBufferRange([[0, 1], [2, 3]]); + return expect(createdMarker).toBe(emittedMarker); + }); + + it("emits events when markers are created and destroyed", function() { + const buffer = new TextBuffer({text: 'hello world'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + const createdMarkers = []; + markerLayer.onDidCreateMarker(m => createdMarkers.push(m)); + const marker = markerLayer.markScreenRange([[0, 4], [1, 4]]); + + expect(createdMarkers).toEqual([marker]); + + let destroyEventCount = 0; + marker.onDidDestroy(() => destroyEventCount++); + + marker.destroy(); + return expect(destroyEventCount).toBe(1); + }); + + it("emits update events when markers are created, updated directly, updated indirectly, or destroyed", function(done) { + const buffer = new TextBuffer({text: 'hello world'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + let marker = null; + + let updateEventCount = 0; + markerLayer.onDidUpdate(function() { + updateEventCount++; + if (updateEventCount === 1) { + return marker.setScreenRange([[0, 5], [1, 0]]); + } else if (updateEventCount === 2) { + return buffer.insert([0, 0], '\t'); + } else if (updateEventCount === 3) { + return marker.destroy(); + } else if (updateEventCount === 4) { + return done(); + } + }); + + return buffer.transact(() => marker = markerLayer.markScreenRange([[0, 4], [1, 4]])); + }); + + it("allows markers to be copied", function() { + const buffer = new TextBuffer({text: '\ta\tbc\tdef\tg\n\th'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + + const markerA = markerLayer.markScreenRange([[0, 4], [1, 4]], {a: 1, b: 2}); + const markerB = markerA.copy({b: 3, c: 4}); + + expect(markerB.id).not.toBe(markerA.id); + expect(markerB.getProperties()).toEqual({a: 1, b: 3, c: 4}); + return expect(markerB.getScreenRange()).toEqual(markerA.getScreenRange()); + }); + + describe("::destroy()", function() { + it("only destroys the underlying buffer MarkerLayer if the DisplayMarkerLayer was created by calling addMarkerLayer on its parent DisplayLayer", function() { + const buffer = new TextBuffer({text: 'abc\ndef\nghi\nj\tk\tl\nmno'}); + const displayLayer1 = buffer.addDisplayLayer({tabLength: 2}); + const displayLayer2 = buffer.addDisplayLayer({tabLength: 4}); + const bufferMarkerLayer = buffer.addMarkerLayer(); + const bufferMarker1 = bufferMarkerLayer.markRange([[2, 1], [2, 2]]); + const displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id); + const displayMarker1 = displayMarkerLayer1.markBufferRange([[1, 0], [1, 2]]); + const displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id); + const displayMarker2 = displayMarkerLayer2.markBufferRange([[2, 0], [2, 1]]); + const displayMarkerLayer3 = displayLayer2.addMarkerLayer(); + const displayMarker3 = displayMarkerLayer3.markBufferRange([[0, 0], [0, 0]]); + + let displayMarkerLayer1DestroyEventCount = 0; + displayMarkerLayer1.onDidDestroy(() => displayMarkerLayer1DestroyEventCount++); + let displayMarkerLayer2DestroyEventCount = 0; + displayMarkerLayer2.onDidDestroy(() => displayMarkerLayer2DestroyEventCount++); + let displayMarkerLayer3DestroyEventCount = 0; + displayMarkerLayer3.onDidDestroy(() => displayMarkerLayer3DestroyEventCount++); + + displayMarkerLayer1.destroy(); + expect(bufferMarkerLayer.isDestroyed()).toBe(false); + expect(displayMarkerLayer1.isDestroyed()).toBe(true); + expect(displayMarkerLayer1DestroyEventCount).toBe(1); + expect(bufferMarker1.isDestroyed()).toBe(false); + expect(displayMarker1.isDestroyed()).toBe(true); + expect(displayMarker2.isDestroyed()).toBe(false); + expect(displayMarker3.isDestroyed()).toBe(false); + + displayMarkerLayer2.destroy(); + expect(bufferMarkerLayer.isDestroyed()).toBe(false); + expect(displayMarkerLayer2.isDestroyed()).toBe(true); + expect(displayMarkerLayer2DestroyEventCount).toBe(1); + expect(bufferMarker1.isDestroyed()).toBe(false); + expect(displayMarker1.isDestroyed()).toBe(true); + expect(displayMarker2.isDestroyed()).toBe(true); + expect(displayMarker3.isDestroyed()).toBe(false); + + bufferMarkerLayer.destroy(); + expect(bufferMarkerLayer.isDestroyed()).toBe(true); + expect(displayMarkerLayer1DestroyEventCount).toBe(1); + expect(displayMarkerLayer2DestroyEventCount).toBe(1); + expect(bufferMarker1.isDestroyed()).toBe(true); + expect(displayMarker1.isDestroyed()).toBe(true); + expect(displayMarker2.isDestroyed()).toBe(true); + expect(displayMarker3.isDestroyed()).toBe(false); + + displayMarkerLayer3.destroy(); + expect(displayMarkerLayer3.bufferMarkerLayer.isDestroyed()).toBe(true); + expect(displayMarkerLayer3.isDestroyed()).toBe(true); + expect(displayMarkerLayer3DestroyEventCount).toBe(1); + return expect(displayMarker3.isDestroyed()).toBe(true); + }); + + return it("destroys the layer's markers", function() { + const buffer = new TextBuffer(); + const displayLayer = buffer.addDisplayLayer(); + const displayMarkerLayer = displayLayer.addMarkerLayer(); + + const marker1 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]); + const marker2 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]); + + const destroyListener = jasmine.createSpy('onDidDestroy listener'); + marker1.onDidDestroy(destroyListener); + + displayMarkerLayer.destroy(); + + expect(destroyListener).toHaveBeenCalled(); + expect(marker1.isDestroyed()).toBe(true); + + // Markers states are updated regardless of whether they have an + // ::onDidDestroy listener + return expect(marker2.isDestroyed()).toBe(true); + }); + }); + + it("destroys display markers when their underlying buffer markers are destroyed", function() { + const buffer = new TextBuffer({text: '\tabc'}); + const displayLayer1 = buffer.addDisplayLayer({tabLength: 2}); + const displayLayer2 = buffer.addDisplayLayer({tabLength: 4}); + const bufferMarkerLayer = buffer.addMarkerLayer(); + const displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id); + const displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id); + + const bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]); + + const displayMarker1 = displayMarkerLayer1.getMarker(bufferMarker.id); + const displayMarker2 = displayMarkerLayer2.getMarker(bufferMarker.id); + expect(displayMarker1.getScreenRange()).toEqual([[0, 2], [0, 3]]); + expect(displayMarker2.getScreenRange()).toEqual([[0, 4], [0, 5]]); + + let displayMarker1DestroyCount = 0; + let displayMarker2DestroyCount = 0; + displayMarker1.onDidDestroy(() => displayMarker1DestroyCount++); + displayMarker2.onDidDestroy(() => displayMarker2DestroyCount++); + + bufferMarker.destroy(); + expect(displayMarker1DestroyCount).toBe(1); + return expect(displayMarker2DestroyCount).toBe(1); + }); + + it("does not throw exceptions when buffer markers are destroyed that don't have corresponding display markers", function() { + const buffer = new TextBuffer({text: '\tabc'}); + const displayLayer1 = buffer.addDisplayLayer({tabLength: 2}); + const displayLayer2 = buffer.addDisplayLayer({tabLength: 4}); + const bufferMarkerLayer = buffer.addMarkerLayer(); + const displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id); + const displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id); + + const bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]); + return bufferMarker.destroy(); + }); + + it("destroys itself when the underlying buffer marker layer is destroyed", function() { + const buffer = new TextBuffer({text: 'abc\ndef\nghi\nj\tk\tl\nmno'}); + const displayLayer1 = buffer.addDisplayLayer({tabLength: 2}); + const displayLayer2 = buffer.addDisplayLayer({tabLength: 4}); + + const bufferMarkerLayer = buffer.addMarkerLayer(); + const displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id); + const displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id); + let displayMarkerLayer1DestroyEventCount = 0; + displayMarkerLayer1.onDidDestroy(() => displayMarkerLayer1DestroyEventCount++); + let displayMarkerLayer2DestroyEventCount = 0; + displayMarkerLayer2.onDidDestroy(() => displayMarkerLayer2DestroyEventCount++); + + bufferMarkerLayer.destroy(); + expect(displayMarkerLayer1.isDestroyed()).toBe(true); + expect(displayMarkerLayer1DestroyEventCount).toBe(1); + expect(displayMarkerLayer2.isDestroyed()).toBe(true); + return expect(displayMarkerLayer2DestroyEventCount).toBe(1); + }); + + return describe("findMarkers(params)", function() { + let [markerLayer, displayLayer] = Array.from([]); + + beforeEach(function() { + const buffer = new TextBuffer({text: SampleText}); + displayLayer = buffer.addDisplayLayer({tabLength: 4}); + return markerLayer = displayLayer.addMarkerLayer(); + }); + + it("allows the startBufferRow and endBufferRow to be specified", function() { + const marker1 = markerLayer.markBufferRange([[0, 0], [3, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[0, 0], [5, 0]], {class: 'a'}); + const marker3 = markerLayer.markBufferRange([[9, 0], [10, 0]], {class: 'b'}); + + expect(markerLayer.findMarkers({class: 'a', startBufferRow: 0})).toEqual([marker2, marker1]); + expect(markerLayer.findMarkers({class: 'a', startBufferRow: 0, endBufferRow: 3})).toEqual([marker1]); + return expect(markerLayer.findMarkers({endBufferRow: 10})).toEqual([marker3]); + }); + + it("allows the startScreenRow and endScreenRow to be specified", function() { + const marker1 = markerLayer.markBufferRange([[6, 0], [7, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[9, 0], [10, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', startScreenRow: 6, endScreenRow: 7})).toEqual([marker2]); + + displayLayer.destroyFoldsIntersectingBufferRange([[4, 0], [7, 0]]); + displayLayer.foldBufferRange([[0, 20], [12, 2]]); + const marker3 = markerLayer.markBufferRange([[12, 0], [12, 0]], {class: 'a'}); + expect(markerLayer.findMarkers({class: 'a', startScreenRow: 0})).toEqual([marker1, marker2, marker3]); + return expect(markerLayer.findMarkers({class: 'a', endScreenRow: 0})).toEqual([marker1, marker2, marker3]); + }); + + it("allows the startsInBufferRange/endsInBufferRange and startsInScreenRange/endsInScreenRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 2], [5, 4]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 2]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', startsInBufferRange: [[5, 1], [5, 3]]})).toEqual([marker1]); + expect(markerLayer.findMarkers({class: 'a', endsInBufferRange: [[8, 1], [8, 3]]})).toEqual([marker2]); + expect(markerLayer.findMarkers({class: 'a', startsInScreenRange: [[4, 0], [4, 1]]})).toEqual([marker1]); + return expect(markerLayer.findMarkers({class: 'a', endsInScreenRange: [[5, 1], [5, 3]]})).toEqual([marker2]); + }); + + it("allows intersectsBufferRowRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + return expect(markerLayer.findMarkers({class: 'a', intersectsBufferRowRange: [5, 6]})).toEqual([marker1]); + }); + + it("allows intersectsScreenRowRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', intersectsScreenRowRange: [5, 10]})).toEqual([marker2]); + + displayLayer.destroyAllFolds(); + displayLayer.foldBufferRange([[0, 20], [12, 2]]); + expect(markerLayer.findMarkers({class: 'a', intersectsScreenRowRange: [0, 0]})).toEqual([marker1, marker2]); + + displayLayer.destroyAllFolds(); + displayLayer.reset({softWrapColumn: 10}); + marker1.setHeadScreenPosition([6, 5]); + marker2.setHeadScreenPosition([9, 2]); + return expect(markerLayer.findMarkers({class: 'a', intersectsScreenRowRange: [5, 7]})).toEqual([marker1]); + }); + + it("allows containedInScreenRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + return expect(markerLayer.findMarkers({class: 'a', containedInScreenRange: [[5, 0], [7, 0]]})).toEqual([marker2]); + }); + + it("allows intersectsBufferRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + return expect(markerLayer.findMarkers({class: 'a', intersectsBufferRange: [[5, 0], [6, 0]]})).toEqual([marker1]); + }); + + it("allows intersectsScreenRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + return expect(markerLayer.findMarkers({class: 'a', intersectsScreenRange: [[5, 0], [10, 0]]})).toEqual([marker2]); + }); + + it("allows containsBufferPosition to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + return expect(markerLayer.findMarkers({class: 'a', containsBufferPosition: [8, 0]})).toEqual([marker2]); + }); + + it("allows containsScreenPosition to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + return expect(markerLayer.findMarkers({class: 'a', containsScreenPosition: [5, 0]})).toEqual([marker2]); + }); + + it("allows containsBufferRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + return expect(markerLayer.findMarkers({class: 'a', containsBufferRange: [[8, 2], [8, 4]]})).toEqual([marker2]); + }); + + it("allows containsScreenRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + return expect(markerLayer.findMarkers({class: 'a', containsScreenRange: [[5, 2], [5, 4]]})).toEqual([marker2]); + }); + + return it("works when used from within a Marker.onDidDestroy callback (regression)", function() { + const displayMarker = markerLayer.markBufferRange([[0, 3], [0, 6]]); + displayMarker.onDidDestroy(() => expect(markerLayer.findMarkers({containsBufferPosition: [0, 4]})).not.toContain(displayMarker)); + return displayMarker.destroy(); + }); + }); +}); diff --git a/spec/marker-layer-spec.coffee b/spec/marker-layer-spec.coffee deleted file mode 100644 index faeabbf597..0000000000 --- a/spec/marker-layer-spec.coffee +++ /dev/null @@ -1,368 +0,0 @@ -{uniq, times} = require 'underscore-plus' -TextBuffer = require '../src/text-buffer' - -describe "MarkerLayer", -> - [buffer, layer1, layer2] = [] - - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - buffer = new TextBuffer(text: "abcdefghijklmnopqrstuvwxyz") - layer1 = buffer.addMarkerLayer() - layer2 = buffer.addMarkerLayer() - - it "ensures that marker ids are unique across layers", -> - times 5, -> - buffer.markRange([[0, 3], [0, 6]]) - layer1.markRange([[0, 4], [0, 7]]) - layer2.markRange([[0, 5], [0, 8]]) - - ids = buffer.getMarkers() - .concat(layer1.getMarkers()) - .concat(layer2.getMarkers()) - .map (marker) -> marker.id - - expect(uniq(ids).length).toEqual ids.length - - it "updates each layer's markers when the text changes", -> - defaultMarker = buffer.markRange([[0, 3], [0, 6]]) - layer1Marker = layer1.markRange([[0, 4], [0, 7]]) - layer2Marker = layer2.markRange([[0, 5], [0, 8]]) - - buffer.setTextInRange([[0, 1], [0, 2]], "BBB") - expect(defaultMarker.getRange()).toEqual [[0, 5], [0, 8]] - expect(layer1Marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(layer2Marker.getRange()).toEqual [[0, 7], [0, 10]] - - layer2.destroy() - expect(layer2.isAlive()).toBe false - expect(layer2.isDestroyed()).toBe true - - expect(layer1.isAlive()).toBe true - expect(layer1.isDestroyed()).toBe false - - buffer.undo() - expect(defaultMarker.getRange()).toEqual [[0, 3], [0, 6]] - expect(layer1Marker.getRange()).toEqual [[0, 4], [0, 7]] - - expect(layer2Marker.isDestroyed()).toBe true - expect(layer2Marker.getRange()).toEqual [[0, 0], [0, 0]] - - it "emits onDidCreateMarker events synchronously when markers are created", -> - createdMarkers = [] - layer1.onDidCreateMarker (marker) -> createdMarkers.push(marker) - marker = layer1.markRange([[0, 1], [2, 3]]) - expect(createdMarkers).toEqual [marker] - - it "does not emit marker events on the TextBuffer for non-default layers", -> - createEventCount = updateEventCount = 0 - buffer.onDidCreateMarker -> createEventCount++ - buffer.onDidUpdateMarkers -> updateEventCount++ - - marker1 = buffer.markRange([[0, 1], [0, 2]]) - marker1.setRange([[0, 1], [0, 3]]) - - expect(createEventCount).toBe 1 - expect(updateEventCount).toBe 2 - - marker2 = layer1.markRange([[0, 1], [0, 2]]) - marker2.setRange([[0, 1], [0, 3]]) - - expect(createEventCount).toBe 1 - expect(updateEventCount).toBe 2 - - describe "when destroyInvalidatedMarkers is enabled for the layer", -> - it "destroys markers when they are invalidated via a splice", -> - layer3 = buffer.addMarkerLayer(destroyInvalidatedMarkers: true) - - marker1 = layer3.markRange([[0, 0], [0, 3]], invalidate: 'inside') - marker2 = layer3.markRange([[0, 2], [0, 6]], invalidate: 'inside') - - destroyedMarkers = [] - marker1.onDidDestroy -> destroyedMarkers.push(marker1) - marker2.onDidDestroy -> destroyedMarkers.push(marker2) - - buffer.insert([0, 5], 'x') - - expect(destroyedMarkers).toEqual [marker2] - expect(marker2.isDestroyed()).toBe true - expect(marker1.isDestroyed()).toBe false - - describe "when maintainHistory is enabled for the layer", -> - layer3 = null - - beforeEach -> - layer3 = buffer.addMarkerLayer(maintainHistory: true) - - it "restores the state of all markers in the layer on undo and redo", -> - buffer.setText('') - buffer.transact -> buffer.append('foo') - layer3 = buffer.addMarkerLayer(maintainHistory: true) - - marker1 = layer3.markRange([[0, 0], [0, 0]], a: 'b', invalidate: 'never') - marker2 = layer3.markRange([[0, 0], [0, 0]], c: 'd', invalidate: 'never') - - marker2ChangeCount = 0 - marker2.onDidChange -> marker2ChangeCount++ - - buffer.transact -> - buffer.append('\n') - buffer.append('bar') - - marker1.destroy() - marker2.setRange([[0, 2], [0, 3]]) - marker3 = layer3.markRange([[0, 0], [0, 3]], e: 'f', invalidate: 'never') - marker4 = layer3.markRange([[1, 0], [1, 3]], g: 'h', invalidate: 'never') - expect(marker2ChangeCount).toBe(1) - - createdMarker = null - layer3.onDidCreateMarker((m) -> createdMarker = m) - buffer.undo() - - expect(buffer.getText()).toBe 'foo' - expect(marker1.isDestroyed()).toBe false - expect(createdMarker).toBe(marker1) - markers = layer3.findMarkers({}) - expect(markers.length).toBe 2 - expect(markers[0]).toBe marker1 - expect(markers[0].getProperties()).toEqual {a: 'b'} - expect(markers[0].getRange()).toEqual [[0, 0], [0, 0]] - expect(markers[1].getProperties()).toEqual {c: 'd'} - expect(markers[1].getRange()).toEqual [[0, 0], [0, 0]] - expect(marker2ChangeCount).toBe(2) - - buffer.redo() - - expect(buffer.getText()).toBe 'foo\nbar' - markers = layer3.findMarkers({}) - expect(markers.length).toBe 3 - expect(markers[0].getProperties()).toEqual {e: 'f'} - expect(markers[0].getRange()).toEqual [[0, 0], [0, 3]] - expect(markers[1].getProperties()).toEqual {c: 'd'} - expect(markers[1].getRange()).toEqual [[0, 2], [0, 3]] - expect(markers[2].getProperties()).toEqual {g: 'h'} - expect(markers[2].getRange()).toEqual [[1, 0], [1, 3]] - - it "does not undo marker manipulations that aren't associated with text changes", -> - marker = layer3.markRange([[0, 6], [0, 9]]) - - # Can't undo changes in a transaction without other buffer changes - buffer.transact -> marker.setRange([[0, 4], [0, 20]]) - buffer.undo() - expect(marker.getRange()).toEqual [[0, 4], [0, 20]] - - # Can undo changes in a transaction with other buffer changes - buffer.transact -> - marker.setRange([[0, 5], [0, 9]]) - buffer.setTextInRange([[0, 2], [0, 3]], 'XYZ') - marker.setRange([[0, 8], [0, 12]]) - - buffer.undo() - expect(marker.getRange()).toEqual [[0, 4], [0, 20]] - - buffer.redo() - expect(marker.getRange()).toEqual [[0, 8], [0, 12]] - - it "ignores snapshot references to marker layers that no longer exist", -> - layer3.markRange([[0, 6], [0, 9]]) - buffer.append("stuff") - layer3.destroy() - - # Should not throw an exception - buffer.undo() - - describe "when a role is provided for the layer", -> - it "getRole() returns its role and keeps track of ids of 'selections' role", -> - expect(buffer.selectionsMarkerLayerIds.size).toBe 0 - - selectionsMarkerLayer1 = buffer.addMarkerLayer(role: "selections") - expect(selectionsMarkerLayer1.getRole()).toBe "selections" - - expect(buffer.addMarkerLayer(role: "role-1").getRole()).toBe "role-1" - expect(buffer.addMarkerLayer().getRole()).toBe undefined - - expect(buffer.selectionsMarkerLayerIds.size).toBe 1 - expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe true - - selectionsMarkerLayer2 = buffer.addMarkerLayer(role: "selections") - expect(selectionsMarkerLayer2.getRole()).toBe "selections" - - expect(buffer.selectionsMarkerLayerIds.size).toBe 2 - expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe true - - selectionsMarkerLayer1.destroy() - selectionsMarkerLayer2.destroy() - expect(buffer.selectionsMarkerLayerIds.size).toBe 2 - expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe true - expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe true - - describe "::findMarkers(params)", -> - it "does not find markers from other layers", -> - defaultMarker = buffer.markRange([[0, 3], [0, 6]]) - layer1Marker = layer1.markRange([[0, 3], [0, 6]]) - layer2Marker = layer2.markRange([[0, 3], [0, 6]]) - - expect(buffer.findMarkers(containsPoint: [0, 4])).toEqual [defaultMarker] - expect(layer1.findMarkers(containsPoint: [0, 4])).toEqual [layer1Marker] - expect(layer2.findMarkers(containsPoint: [0, 4])).toEqual [layer2Marker] - - describe "::onDidUpdate", -> - it "notifies observers at the end of the outermost transaction when markers are created, updated, or destroyed", -> - [marker1, marker2] = [] - - displayLayer = buffer.addDisplayLayer() - displayLayerDidChange = false - - changeCount = 0 - buffer.onDidChange -> - changeCount++ - - updateCount = 0 - layer1.onDidUpdate -> - updateCount++ - if updateCount is 1 - expect(changeCount).toBe(0) - buffer.transact -> - marker1.setRange([[1, 2], [3, 4]]) - marker2.setRange([[4, 5], [6, 7]]) - else if updateCount is 2 - expect(changeCount).toBe(0) - buffer.transact -> - buffer.insert([0, 1], "xxx") - buffer.insert([0, 1], "yyy") - else if updateCount is 3 - expect(changeCount).toBe(1) - marker1.destroy() - marker2.destroy() - else if updateCount is 7 - expect(changeCount).toBe(2) - expect(displayLayerDidChange).toBe(true, 'Display layer was updated after marker layer.') - - buffer.transact -> - buffer.transact -> - marker1 = layer1.markRange([[0, 2], [0, 4]]) - marker2 = layer1.markRange([[0, 6], [0, 8]]) - - expect(updateCount).toBe(5) - - # update events happen immediately when there is no parent transaction - layer1.markRange([[0, 2], [0, 4]]) - expect(updateCount).toBe(6) - - # update events happen after updating display layers when there is no parent transaction. - displayLayer.onDidChange -> - displayLayerDidChange = true - buffer.undo() - expect(updateCount).toBe(7) - - describe "::clear()", -> - it "destroys all of the layer's markers", (done) -> - buffer = new TextBuffer(text: 'abc') - displayLayer = buffer.addDisplayLayer() - markerLayer = buffer.addMarkerLayer() - displayMarkerLayer = displayLayer.getMarkerLayer(markerLayer.id) - marker1 = markerLayer.markRange([[0, 1], [0, 2]]) - marker2 = markerLayer.markRange([[0, 1], [0, 2]]) - marker3 = markerLayer.markRange([[0, 1], [0, 2]]) - displayMarker1 = displayMarkerLayer.getMarker(marker1.id) - # intentionally omit a display marker for marker2 just to cover that case - displayMarker3 = displayMarkerLayer.getMarker(marker3.id) - - marker1DestroyCount = 0 - marker2DestroyCount = 0 - displayMarker1DestroyCount = 0 - displayMarker3DestroyCount = 0 - markerLayerUpdateCount = 0 - displayMarkerLayerUpdateCount = 0 - marker1.onDidDestroy -> marker1DestroyCount++ - marker2.onDidDestroy -> marker2DestroyCount++ - displayMarker1.onDidDestroy -> displayMarker1DestroyCount++ - displayMarker3.onDidDestroy -> displayMarker3DestroyCount++ - markerLayer.onDidUpdate -> - markerLayerUpdateCount++ - done() if markerLayerUpdateCount is 1 and displayMarkerLayerUpdateCount is 1 - displayMarkerLayer.onDidUpdate -> - displayMarkerLayerUpdateCount++ - done() if markerLayerUpdateCount is 1 and displayMarkerLayerUpdateCount is 1 - - markerLayer.clear() - expect(marker1.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(true) - expect(marker3.isDestroyed()).toBe(true) - expect(displayMarker1.isDestroyed()).toBe(true) - expect(displayMarker3.isDestroyed()).toBe(true) - expect(marker1DestroyCount).toBe(1) - expect(marker2DestroyCount).toBe(1) - expect(displayMarker1DestroyCount).toBe(1) - expect(displayMarker3DestroyCount).toBe(1) - expect(markerLayer.getMarkers()).toEqual([]) - expect(displayMarkerLayer.getMarkers()).toEqual([]) - expect(displayMarkerLayer.getMarker(displayMarker3.id)).toBeUndefined() - - describe "::copy", -> - it "creates a new marker layer with markers in the same states", -> - originalLayer = buffer.addMarkerLayer(maintainHistory: true) - originalLayer.markRange([[0, 1], [0, 3]], a: 'b') - originalLayer.markPosition([0, 2]) - - copy = originalLayer.copy() - expect(copy).not.toBe originalLayer - - markers = copy.getMarkers() - expect(markers.length).toBe 2 - expect(markers[0].getRange()).toEqual [[0, 1], [0, 3]] - expect(markers[0].getProperties()).toEqual {a: 'b'} - expect(markers[1].getRange()).toEqual [[0, 2], [0, 2]] - expect(markers[1].hasTail()).toBe false - - it "copies the marker layer role", -> - originalLayer = buffer.addMarkerLayer(maintainHistory: true, role: "selections") - copy = originalLayer.copy() - expect(copy).not.toBe originalLayer - expect(copy.getRole()).toBe("selections") - expect(buffer.selectionsMarkerLayerIds.has(originalLayer.id)).toBe true - expect(buffer.selectionsMarkerLayerIds.has(copy.id)).toBe true - expect(buffer.selectionsMarkerLayerIds.size).toBe 2 - - describe "::destroy", -> - it "destroys the layer's markers", -> - buffer = new TextBuffer() - markerLayer = buffer.addMarkerLayer() - - marker1 = markerLayer.markRange([[0, 0], [0, 0]]) - marker2 = markerLayer.markRange([[0, 0], [0, 0]]) - - destroyListener = jasmine.createSpy('onDidDestroy listener') - marker1.onDidDestroy(destroyListener) - - markerLayer.destroy() - - expect(destroyListener).toHaveBeenCalled() - expect(marker1.isDestroyed()).toBe(true) - - # Markers states are updated regardless of whether they have an - # ::onDidDestroy listener - expect(marker2.isDestroyed()).toBe(true) - - describe "trackDestructionInOnDidCreateMarkerCallbacks", -> - it "stores a stack trace when destroy is called during onDidCreateMarker callbacks", -> - layer1.onDidCreateMarker (m) -> m.destroy() if destroyInCreateCallback - - layer1.trackDestructionInOnDidCreateMarkerCallbacks = true - destroyInCreateCallback = true - marker1 = layer1.markPosition([0, 0]) - expect(marker1.isDestroyed()).toBe(true) - expect(marker1.destroyStackTrace).toBeDefined() - - destroyInCreateCallback = false - marker2 = layer1.markPosition([0, 0]) - expect(marker2.isDestroyed()).toBe(false) - expect(marker2.destroyStackTrace).toBeUndefined() - marker2.destroy() - expect(marker2.isDestroyed()).toBe(true) - expect(marker2.destroyStackTrace).toBeUndefined() - - destroyInCreateCallback = true - layer1.trackDestructionInOnDidCreateMarkerCallbacks = false - marker3 = layer1.markPosition([0, 0]) - expect(marker3.isDestroyed()).toBe(true) - expect(marker3.destroyStackTrace).toBeUndefined() diff --git a/spec/marker-layer-spec.js b/spec/marker-layer-spec.js new file mode 100644 index 0000000000..813eec4e55 --- /dev/null +++ b/spec/marker-layer-spec.js @@ -0,0 +1,388 @@ +const {uniq, times} = require('underscore-plus'); +const TextBuffer = require('../src/text-buffer'); + +describe("MarkerLayer", function() { + let [buffer, layer1, layer2] = Array.from([]); + + beforeEach(function() { + jasmine.addCustomEqualityTester(require("underscore-plus").isEqual); + buffer = new TextBuffer({text: "abcdefghijklmnopqrstuvwxyz"}); + layer1 = buffer.addMarkerLayer(); + return layer2 = buffer.addMarkerLayer(); + }); + + it("ensures that marker ids are unique across layers", function() { + times(5, function() { + buffer.markRange([[0, 3], [0, 6]]); + layer1.markRange([[0, 4], [0, 7]]); + return layer2.markRange([[0, 5], [0, 8]]); + }); + + const ids = buffer.getMarkers() + .concat(layer1.getMarkers()) + .concat(layer2.getMarkers()) + .map(marker => marker.id); + + return expect(uniq(ids).length).toEqual(ids.length); + }); + + it("updates each layer's markers when the text changes", function() { + const defaultMarker = buffer.markRange([[0, 3], [0, 6]]); + const layer1Marker = layer1.markRange([[0, 4], [0, 7]]); + const layer2Marker = layer2.markRange([[0, 5], [0, 8]]); + + buffer.setTextInRange([[0, 1], [0, 2]], "BBB"); + expect(defaultMarker.getRange()).toEqual([[0, 5], [0, 8]]); + expect(layer1Marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(layer2Marker.getRange()).toEqual([[0, 7], [0, 10]]); + + layer2.destroy(); + expect(layer2.isAlive()).toBe(false); + expect(layer2.isDestroyed()).toBe(true); + + expect(layer1.isAlive()).toBe(true); + expect(layer1.isDestroyed()).toBe(false); + + buffer.undo(); + expect(defaultMarker.getRange()).toEqual([[0, 3], [0, 6]]); + expect(layer1Marker.getRange()).toEqual([[0, 4], [0, 7]]); + + expect(layer2Marker.isDestroyed()).toBe(true); + return expect(layer2Marker.getRange()).toEqual([[0, 0], [0, 0]]); +}); + + it("emits onDidCreateMarker events synchronously when markers are created", function() { + const createdMarkers = []; + layer1.onDidCreateMarker(marker => createdMarkers.push(marker)); + const marker = layer1.markRange([[0, 1], [2, 3]]); + return expect(createdMarkers).toEqual([marker]); +}); + + it("does not emit marker events on the TextBuffer for non-default layers", function() { + let updateEventCount; + let createEventCount = (updateEventCount = 0); + buffer.onDidCreateMarker(() => createEventCount++); + buffer.onDidUpdateMarkers(() => updateEventCount++); + + const marker1 = buffer.markRange([[0, 1], [0, 2]]); + marker1.setRange([[0, 1], [0, 3]]); + + expect(createEventCount).toBe(1); + expect(updateEventCount).toBe(2); + + const marker2 = layer1.markRange([[0, 1], [0, 2]]); + marker2.setRange([[0, 1], [0, 3]]); + + expect(createEventCount).toBe(1); + return expect(updateEventCount).toBe(2); + }); + + describe("when destroyInvalidatedMarkers is enabled for the layer", () => it("destroys markers when they are invalidated via a splice", function() { + const layer3 = buffer.addMarkerLayer({destroyInvalidatedMarkers: true}); + + const marker1 = layer3.markRange([[0, 0], [0, 3]], {invalidate: 'inside'}); + const marker2 = layer3.markRange([[0, 2], [0, 6]], {invalidate: 'inside'}); + + const destroyedMarkers = []; + marker1.onDidDestroy(() => destroyedMarkers.push(marker1)); + marker2.onDidDestroy(() => destroyedMarkers.push(marker2)); + + buffer.insert([0, 5], 'x'); + + expect(destroyedMarkers).toEqual([marker2]); + expect(marker2.isDestroyed()).toBe(true); + return expect(marker1.isDestroyed()).toBe(false); + })); + + describe("when maintainHistory is enabled for the layer", function() { + let layer3 = null; + + beforeEach(() => layer3 = buffer.addMarkerLayer({maintainHistory: true})); + + it("restores the state of all markers in the layer on undo and redo", function() { + buffer.setText(''); + buffer.transact(() => buffer.append('foo')); + layer3 = buffer.addMarkerLayer({maintainHistory: true}); + + const marker1 = layer3.markRange([[0, 0], [0, 0]], {a: 'b', invalidate: 'never'}); + const marker2 = layer3.markRange([[0, 0], [0, 0]], {c: 'd', invalidate: 'never'}); + + let marker2ChangeCount = 0; + marker2.onDidChange(() => marker2ChangeCount++); + + buffer.transact(function() { + buffer.append('\n'); + buffer.append('bar'); + + marker1.destroy(); + marker2.setRange([[0, 2], [0, 3]]); + const marker3 = layer3.markRange([[0, 0], [0, 3]], {e: 'f', invalidate: 'never'}); + const marker4 = layer3.markRange([[1, 0], [1, 3]], {g: 'h', invalidate: 'never'}); + return expect(marker2ChangeCount).toBe(1); + }); + + let createdMarker = null; + layer3.onDidCreateMarker(m => createdMarker = m); + buffer.undo(); + + expect(buffer.getText()).toBe('foo'); + expect(marker1.isDestroyed()).toBe(false); + expect(createdMarker).toBe(marker1); + let markers = layer3.findMarkers({}); + expect(markers.length).toBe(2); + expect(markers[0]).toBe(marker1); + expect(markers[0].getProperties()).toEqual({a: 'b'}); + expect(markers[0].getRange()).toEqual([[0, 0], [0, 0]]); + expect(markers[1].getProperties()).toEqual({c: 'd'}); + expect(markers[1].getRange()).toEqual([[0, 0], [0, 0]]); + expect(marker2ChangeCount).toBe(2); + + buffer.redo(); + + expect(buffer.getText()).toBe('foo\nbar'); + markers = layer3.findMarkers({}); + expect(markers.length).toBe(3); + expect(markers[0].getProperties()).toEqual({e: 'f'}); + expect(markers[0].getRange()).toEqual([[0, 0], [0, 3]]); + expect(markers[1].getProperties()).toEqual({c: 'd'}); + expect(markers[1].getRange()).toEqual([[0, 2], [0, 3]]); + expect(markers[2].getProperties()).toEqual({g: 'h'}); + return expect(markers[2].getRange()).toEqual([[1, 0], [1, 3]]); + }); + + it("does not undo marker manipulations that aren't associated with text changes", function() { + const marker = layer3.markRange([[0, 6], [0, 9]]); + + // Can't undo changes in a transaction without other buffer changes + buffer.transact(() => marker.setRange([[0, 4], [0, 20]])); + buffer.undo(); + expect(marker.getRange()).toEqual([[0, 4], [0, 20]]); + + // Can undo changes in a transaction with other buffer changes + buffer.transact(function() { + marker.setRange([[0, 5], [0, 9]]); + buffer.setTextInRange([[0, 2], [0, 3]], 'XYZ'); + return marker.setRange([[0, 8], [0, 12]]); + }); + + buffer.undo(); + expect(marker.getRange()).toEqual([[0, 4], [0, 20]]); + + buffer.redo(); + return expect(marker.getRange()).toEqual([[0, 8], [0, 12]]); + }); + + return it("ignores snapshot references to marker layers that no longer exist", function() { + layer3.markRange([[0, 6], [0, 9]]); + buffer.append("stuff"); + layer3.destroy(); + + // Should not throw an exception + return buffer.undo(); + }); + }); + + describe("when a role is provided for the layer", () => it("getRole() returns its role and keeps track of ids of 'selections' role", function() { + expect(buffer.selectionsMarkerLayerIds.size).toBe(0); + + const selectionsMarkerLayer1 = buffer.addMarkerLayer({role: "selections"}); + expect(selectionsMarkerLayer1.getRole()).toBe("selections"); + + expect(buffer.addMarkerLayer({role: "role-1"}).getRole()).toBe("role-1"); + expect(buffer.addMarkerLayer().getRole()).toBe(undefined); + + expect(buffer.selectionsMarkerLayerIds.size).toBe(1); + expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe(true); + + const selectionsMarkerLayer2 = buffer.addMarkerLayer({role: "selections"}); + expect(selectionsMarkerLayer2.getRole()).toBe("selections"); + + expect(buffer.selectionsMarkerLayerIds.size).toBe(2); + expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe(true); + + selectionsMarkerLayer1.destroy(); + selectionsMarkerLayer2.destroy(); + expect(buffer.selectionsMarkerLayerIds.size).toBe(2); + expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe(true); + return expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe(true); + })); + + describe("::findMarkers(params)", () => it("does not find markers from other layers", function() { + const defaultMarker = buffer.markRange([[0, 3], [0, 6]]); + const layer1Marker = layer1.markRange([[0, 3], [0, 6]]); + const layer2Marker = layer2.markRange([[0, 3], [0, 6]]); + + expect(buffer.findMarkers({containsPoint: [0, 4]})).toEqual([defaultMarker]); + expect(layer1.findMarkers({containsPoint: [0, 4]})).toEqual([layer1Marker]); + return expect(layer2.findMarkers({containsPoint: [0, 4]})).toEqual([layer2Marker]); +})); + + describe("::onDidUpdate", () => it("notifies observers at the end of the outermost transaction when markers are created, updated, or destroyed", function() { + let [marker1, marker2] = Array.from([]); + + const displayLayer = buffer.addDisplayLayer(); + let displayLayerDidChange = false; + + let changeCount = 0; + buffer.onDidChange(() => changeCount++); + + let updateCount = 0; + layer1.onDidUpdate(function() { + updateCount++; + if (updateCount === 1) { + expect(changeCount).toBe(0); + return buffer.transact(function() { + marker1.setRange([[1, 2], [3, 4]]); + return marker2.setRange([[4, 5], [6, 7]]); + }); + } else if (updateCount === 2) { + expect(changeCount).toBe(0); + return buffer.transact(function() { + buffer.insert([0, 1], "xxx"); + return buffer.insert([0, 1], "yyy"); + }); + } else if (updateCount === 3) { + expect(changeCount).toBe(1); + marker1.destroy(); + return marker2.destroy(); + } else if (updateCount === 7) { + expect(changeCount).toBe(2); + return expect(displayLayerDidChange).toBe(true, 'Display layer was updated after marker layer.'); + } + }); + + buffer.transact(() => buffer.transact(function() { + marker1 = layer1.markRange([[0, 2], [0, 4]]); + return marker2 = layer1.markRange([[0, 6], [0, 8]]); + })); + + expect(updateCount).toBe(5); + + // update events happen immediately when there is no parent transaction + layer1.markRange([[0, 2], [0, 4]]); + expect(updateCount).toBe(6); + + // update events happen after updating display layers when there is no parent transaction. + displayLayer.onDidChange(() => displayLayerDidChange = true); + buffer.undo(); + return expect(updateCount).toBe(7); + })); + + describe("::clear()", () => it("destroys all of the layer's markers", function(done) { + buffer = new TextBuffer({text: 'abc'}); + const displayLayer = buffer.addDisplayLayer(); + const markerLayer = buffer.addMarkerLayer(); + const displayMarkerLayer = displayLayer.getMarkerLayer(markerLayer.id); + const marker1 = markerLayer.markRange([[0, 1], [0, 2]]); + const marker2 = markerLayer.markRange([[0, 1], [0, 2]]); + const marker3 = markerLayer.markRange([[0, 1], [0, 2]]); + const displayMarker1 = displayMarkerLayer.getMarker(marker1.id); + // intentionally omit a display marker for marker2 just to cover that case + const displayMarker3 = displayMarkerLayer.getMarker(marker3.id); + + let marker1DestroyCount = 0; + let marker2DestroyCount = 0; + let displayMarker1DestroyCount = 0; + let displayMarker3DestroyCount = 0; + let markerLayerUpdateCount = 0; + let displayMarkerLayerUpdateCount = 0; + marker1.onDidDestroy(() => marker1DestroyCount++); + marker2.onDidDestroy(() => marker2DestroyCount++); + displayMarker1.onDidDestroy(() => displayMarker1DestroyCount++); + displayMarker3.onDidDestroy(() => displayMarker3DestroyCount++); + markerLayer.onDidUpdate(function() { + markerLayerUpdateCount++; + if ((markerLayerUpdateCount === 1) && (displayMarkerLayerUpdateCount === 1)) { return done(); } + }); + displayMarkerLayer.onDidUpdate(function() { + displayMarkerLayerUpdateCount++; + if ((markerLayerUpdateCount === 1) && (displayMarkerLayerUpdateCount === 1)) { return done(); } + }); + + markerLayer.clear(); + expect(marker1.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(true); + expect(marker3.isDestroyed()).toBe(true); + expect(displayMarker1.isDestroyed()).toBe(true); + expect(displayMarker3.isDestroyed()).toBe(true); + expect(marker1DestroyCount).toBe(1); + expect(marker2DestroyCount).toBe(1); + expect(displayMarker1DestroyCount).toBe(1); + expect(displayMarker3DestroyCount).toBe(1); + expect(markerLayer.getMarkers()).toEqual([]); + expect(displayMarkerLayer.getMarkers()).toEqual([]); + return expect(displayMarkerLayer.getMarker(displayMarker3.id)).toBeUndefined(); + })); + + describe("::copy", function() { + it("creates a new marker layer with markers in the same states", function() { + const originalLayer = buffer.addMarkerLayer({maintainHistory: true}); + originalLayer.markRange([[0, 1], [0, 3]], {a: 'b'}); + originalLayer.markPosition([0, 2]); + + const copy = originalLayer.copy(); + expect(copy).not.toBe(originalLayer); + + const markers = copy.getMarkers(); + expect(markers.length).toBe(2); + expect(markers[0].getRange()).toEqual([[0, 1], [0, 3]]); + expect(markers[0].getProperties()).toEqual({a: 'b'}); + expect(markers[1].getRange()).toEqual([[0, 2], [0, 2]]); + return expect(markers[1].hasTail()).toBe(false); + }); + + return it("copies the marker layer role", function() { + const originalLayer = buffer.addMarkerLayer({maintainHistory: true, role: "selections"}); + const copy = originalLayer.copy(); + expect(copy).not.toBe(originalLayer); + expect(copy.getRole()).toBe("selections"); + expect(buffer.selectionsMarkerLayerIds.has(originalLayer.id)).toBe(true); + expect(buffer.selectionsMarkerLayerIds.has(copy.id)).toBe(true); + return expect(buffer.selectionsMarkerLayerIds.size).toBe(2); + }); + }); + + describe("::destroy", () => it("destroys the layer's markers", function() { + buffer = new TextBuffer(); + const markerLayer = buffer.addMarkerLayer(); + + const marker1 = markerLayer.markRange([[0, 0], [0, 0]]); + const marker2 = markerLayer.markRange([[0, 0], [0, 0]]); + + const destroyListener = jasmine.createSpy('onDidDestroy listener'); + marker1.onDidDestroy(destroyListener); + + markerLayer.destroy(); + + expect(destroyListener).toHaveBeenCalled(); + expect(marker1.isDestroyed()).toBe(true); + + // Markers states are updated regardless of whether they have an + // ::onDidDestroy listener + return expect(marker2.isDestroyed()).toBe(true); + })); + + return describe("trackDestructionInOnDidCreateMarkerCallbacks", () => it("stores a stack trace when destroy is called during onDidCreateMarker callbacks", function() { + layer1.onDidCreateMarker(function(m) { if (destroyInCreateCallback) { return m.destroy(); } }); + + layer1.trackDestructionInOnDidCreateMarkerCallbacks = true; + var destroyInCreateCallback = true; + const marker1 = layer1.markPosition([0, 0]); + expect(marker1.isDestroyed()).toBe(true); + expect(marker1.destroyStackTrace).toBeDefined(); + + destroyInCreateCallback = false; + const marker2 = layer1.markPosition([0, 0]); + expect(marker2.isDestroyed()).toBe(false); + expect(marker2.destroyStackTrace).toBeUndefined(); + marker2.destroy(); + expect(marker2.isDestroyed()).toBe(true); + expect(marker2.destroyStackTrace).toBeUndefined(); + + destroyInCreateCallback = true; + layer1.trackDestructionInOnDidCreateMarkerCallbacks = false; + const marker3 = layer1.markPosition([0, 0]); + expect(marker3.isDestroyed()).toBe(true); + return expect(marker3.destroyStackTrace).toBeUndefined(); + })); +}); diff --git a/spec/marker-spec.coffee b/spec/marker-spec.coffee deleted file mode 100644 index a3ea821bca..0000000000 --- a/spec/marker-spec.coffee +++ /dev/null @@ -1,765 +0,0 @@ -{difference, times, uniq} = require 'underscore-plus' -TextBuffer = require '../src/text-buffer' - -describe "Marker", -> - [buffer, markerCreations, markersUpdatedCount] = [] - - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - buffer = new TextBuffer(text: "abcdefghijklmnopqrstuvwxyz") - markerCreations = [] - buffer.onDidCreateMarker (marker) -> markerCreations.push(marker) - markersUpdatedCount = 0 - buffer.onDidUpdateMarkers -> markersUpdatedCount++ - - describe "creation", -> - describe "TextBuffer::markRange(range, properties)", -> - it "creates a marker for the given range with the given properties", -> - marker = buffer.markRange([[0, 3], [0, 6]]) - expect(marker.getRange()).toEqual [[0, 3], [0, 6]] - expect(marker.getHeadPosition()).toEqual [0, 6] - expect(marker.getTailPosition()).toEqual [0, 3] - expect(marker.isReversed()).toBe false - expect(marker.hasTail()).toBe true - expect(markerCreations).toEqual [marker] - expect(markersUpdatedCount).toBe 1 - - it "allows a reversed marker to be created", -> - marker = buffer.markRange([[0, 3], [0, 6]], reversed: true) - expect(marker.getRange()).toEqual [[0, 3], [0, 6]] - expect(marker.getHeadPosition()).toEqual [0, 3] - expect(marker.getTailPosition()).toEqual [0, 6] - expect(marker.isReversed()).toBe true - expect(marker.hasTail()).toBe true - - it "allows an invalidation strategy to be assigned", -> - marker = buffer.markRange([[0, 3], [0, 6]], invalidate: 'inside') - expect(marker.getInvalidationStrategy()).toBe 'inside' - - it "allows an exclusive marker to be created independently of its invalidation strategy", -> - layer = buffer.addMarkerLayer({maintainHistory: true}) - marker1 = layer.markRange([[0, 3], [0, 6]], invalidate: 'overlap', exclusive: true) - marker2 = marker1.copy() - marker3 = marker1.copy(exclusive: false) - marker4 = marker1.copy(exclusive: null, invalidate: 'inside') - - buffer.insert([0, 3], 'something') - - expect(marker1.getStartPosition()).toEqual [0, 12] - expect(marker1.isExclusive()).toBe true - expect(marker2.getStartPosition()).toEqual [0, 12] - expect(marker2.isExclusive()).toBe true - expect(marker3.getStartPosition()).toEqual [0, 3] - expect(marker3.isExclusive()).toBe false - expect(marker4.getStartPosition()).toEqual [0, 12] - expect(marker4.isExclusive()).toBe true - - it "allows custom state to be assigned", -> - marker = buffer.markRange([[0, 3], [0, 6]], foo: 1, bar: 2) - expect(marker.getProperties()).toEqual {foo: 1, bar: 2} - - it "clips the range before creating a marker with it", -> - marker = buffer.markRange([[-100, -100], [100, 100]]) - expect(marker.getRange()).toEqual [[0, 0], [0, 26]] - - it "throws an error if an invalid point is given", -> - marker1 = buffer.markRange([[0, 1], [0, 2]]) - - expect -> buffer.markRange([[0, NaN], [0, 2]]) - .toThrowError "Invalid Point: (0, NaN)" - expect -> buffer.markRange([[0, 1], [0, NaN]]) - .toThrowError "Invalid Point: (0, NaN)" - - expect(buffer.findMarkers({})).toEqual [marker1] - expect(buffer.getMarkers()).toEqual [marker1] - - it "allows arbitrary properties to be assigned", -> - marker = buffer.markRange([[0, 6], [0, 8]], foo: 'bar') - expect(marker.getProperties()).toEqual({foo: 'bar'}) - - describe "TextBuffer::markPosition(position, properties)", -> - it "creates a tail-less marker at the given position", -> - marker = buffer.markPosition([0, 6]) - expect(marker.getRange()).toEqual [[0, 6], [0, 6]] - expect(marker.getHeadPosition()).toEqual [0, 6] - expect(marker.getTailPosition()).toEqual [0, 6] - expect(marker.isReversed()).toBe false - expect(marker.hasTail()).toBe false - expect(markerCreations).toEqual [marker] - - it "allows an invalidation strategy to be assigned", -> - marker = buffer.markPosition([0, 3], invalidate: 'inside') - expect(marker.getInvalidationStrategy()).toBe 'inside' - - it "throws an error if an invalid point is given", -> - marker1 = buffer.markPosition([0, 1]) - - expect -> buffer.markPosition([0, NaN]) - .toThrowError "Invalid Point: (0, NaN)" - - expect(buffer.findMarkers({})).toEqual [marker1] - expect(buffer.getMarkers()).toEqual [marker1] - - it "allows arbitrary properties to be assigned", -> - marker = buffer.markPosition([0, 6], foo: 'bar') - expect(marker.getProperties()).toEqual({foo: 'bar'}) - - describe "direct updates", -> - [marker, changes] = [] - - beforeEach -> - marker = buffer.markRange([[0, 6], [0, 9]]) - changes = [] - markersUpdatedCount = 0 - marker.onDidChange (change) -> changes.push(change) - - describe "::setHeadPosition(position, state)", -> - it "sets the head position of the marker, flipping its orientation if necessary", -> - marker.setHeadPosition([0, 12]) - expect(marker.getRange()).toEqual [[0, 6], [0, 12]] - expect(marker.isReversed()).toBe false - expect(markersUpdatedCount).toBe 1 - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 12] - oldTailPosition: [0, 6], newTailPosition: [0, 6] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setHeadPosition([0, 3]) - expect(markersUpdatedCount).toBe 2 - expect(marker.getRange()).toEqual [[0, 3], [0, 6]] - expect(marker.isReversed()).toBe true - expect(changes).toEqual [{ - oldHeadPosition: [0, 12], newHeadPosition: [0, 3] - oldTailPosition: [0, 6], newTailPosition: [0, 6] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setHeadPosition([0, 9]) - expect(markersUpdatedCount).toBe 3 - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isReversed()).toBe false - expect(changes).toEqual [{ - oldHeadPosition: [0, 3], newHeadPosition: [0, 9] - oldTailPosition: [0, 6], newTailPosition: [0, 6] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - it "does not give the marker a tail if it doesn't have one already", -> - marker.clearTail() - expect(marker.hasTail()).toBe false - marker.setHeadPosition([0, 15]) - expect(marker.hasTail()).toBe false - expect(marker.getRange()).toEqual [[0, 15], [0, 15]] - - it "does not notify ::onDidChange observers and returns false if the position isn't actually changed", -> - expect(marker.setHeadPosition(marker.getHeadPosition())).toBe false - expect(markersUpdatedCount).toBe 0 - expect(changes.length).toBe 0 - - it "clips the assigned position", -> - marker.setHeadPosition([100, 100]) - expect(marker.getHeadPosition()).toEqual [0, 26] - - describe "::setTailPosition(position, state)", -> - it "sets the head position of the marker, flipping its orientation if necessary", -> - marker.setTailPosition([0, 3]) - expect(marker.getRange()).toEqual [[0, 3], [0, 9]] - expect(marker.isReversed()).toBe false - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 9] - oldTailPosition: [0, 6], newTailPosition: [0, 3] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setTailPosition([0, 12]) - expect(marker.getRange()).toEqual [[0, 9], [0, 12]] - expect(marker.isReversed()).toBe true - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 9] - oldTailPosition: [0, 3], newTailPosition: [0, 12] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setTailPosition([0, 6]) - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isReversed()).toBe false - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 9] - oldTailPosition: [0, 12], newTailPosition: [0, 6] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - it "plants the tail of the marker if it does not have a tail", -> - marker.clearTail() - expect(marker.hasTail()).toBe false - marker.setTailPosition([0, 0]) - expect(marker.hasTail()).toBe true - expect(marker.getRange()).toEqual [[0, 0], [0, 9]] - - it "does not notify ::onDidChange observers and returns false if the position isn't actually changed", -> - expect(marker.setTailPosition(marker.getTailPosition())).toBe false - expect(changes.length).toBe 0 - - it "clips the assigned position", -> - marker.setTailPosition([100, 100]) - expect(marker.getTailPosition()).toEqual [0, 26] - - describe "::setRange(range, options)", -> - it "sets the head and tail position simultaneously, flipping the orientation if the 'isReversed' option is true", -> - marker.setRange([[0, 8], [0, 12]]) - expect(marker.getRange()).toEqual [[0, 8], [0, 12]] - expect(marker.isReversed()).toBe false - expect(marker.getHeadPosition()).toEqual [0, 12] - expect(marker.getTailPosition()).toEqual [0, 8] - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 12] - oldTailPosition: [0, 6], newTailPosition: [0, 8] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setRange([[0, 3], [0, 9]], reversed: true) - expect(marker.getRange()).toEqual [[0, 3], [0, 9]] - expect(marker.isReversed()).toBe true - expect(marker.getHeadPosition()).toEqual [0, 3] - expect(marker.getTailPosition()).toEqual [0, 9] - expect(changes).toEqual [{ - oldHeadPosition: [0, 12], newHeadPosition: [0, 3] - oldTailPosition: [0, 8], newTailPosition: [0, 9] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - it "plants the tail of the marker if it does not have a tail", -> - marker.clearTail() - expect(marker.hasTail()).toBe false - marker.setRange([[0, 1], [0, 10]]) - expect(marker.hasTail()).toBe true - expect(marker.getRange()).toEqual [[0, 1], [0, 10]] - - it "clips the assigned range", -> - marker.setRange([[-100, -100], [100, 100]]) - expect(marker.getRange()).toEqual [[0, 0], [0, 26]] - - it "emits the right events when called inside of an ::onDidChange handler", -> - marker.onDidChange (change) -> - if marker.getHeadPosition().isEqual([0, 5]) - marker.setHeadPosition([0, 6]) - - marker.setHeadPosition([0, 5]) - - headPositions = for {oldHeadPosition, newHeadPosition} in changes - {old: oldHeadPosition, new: newHeadPosition} - - expect(headPositions).toEqual [ - {old: [0, 9], new: [0, 5]} - {old: [0, 5], new: [0, 6]} - ] - - it "throws an error if an invalid range is given", -> - expect -> marker.setRange([[0, NaN], [0, 12]]) - .toThrowError "Invalid Point: (0, NaN)" - - expect(buffer.findMarkers({})).toEqual [marker] - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - - describe "::clearTail() / ::plantTail()", -> - it "clears the tail / plants the tail at the current head position", -> - marker.setRange([[0, 6], [0, 9]], reversed: true) - - changes = [] - marker.clearTail() - expect(marker.getRange()).toEqual [[0, 6], [0, 6]] - expect(marker.hasTail()).toBe false - expect(marker.isReversed()).toBe false - - expect(changes).toEqual [{ - oldHeadPosition: [0, 6], newHeadPosition: [0, 6] - oldTailPosition: [0, 9], newTailPosition: [0, 6] - hadTail: true, hasTail: false - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setHeadPosition([0, 12]) - expect(marker.getRange()).toEqual [[0, 12], [0, 12]] - expect(changes).toEqual [{ - oldHeadPosition: [0, 6], newHeadPosition: [0, 12] - oldTailPosition: [0, 6], newTailPosition: [0, 12] - hadTail: false, hasTail: false - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.plantTail() - expect(marker.hasTail()).toBe true - expect(marker.isReversed()).toBe false - expect(marker.getRange()).toEqual [[0, 12], [0, 12]] - expect(changes).toEqual [{ - oldHeadPosition: [0, 12], newHeadPosition: [0, 12] - oldTailPosition: [0, 12], newTailPosition: [0, 12] - hadTail: false, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setHeadPosition([0, 15]) - expect(marker.getRange()).toEqual [[0, 12], [0, 15]] - expect(changes).toEqual [{ - oldHeadPosition: [0, 12], newHeadPosition: [0, 15] - oldTailPosition: [0, 12], newTailPosition: [0, 12] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.plantTail() - expect(marker.getRange()).toEqual [[0, 12], [0, 15]] - expect(changes).toEqual [] - - describe "::setProperties(properties)", -> - it "merges the given properties into the current properties", -> - marker.setProperties(foo: 1) - expect(marker.getProperties()).toEqual {foo: 1} - marker.setProperties(bar: 2) - expect(marker.getProperties()).toEqual {foo: 1, bar: 2} - expect(markersUpdatedCount).toBe 2 - - describe "indirect updates (due to buffer changes)", -> - [allStrategies, neverMarker, surroundMarker, overlapMarker, insideMarker, touchMarker] = [] - - beforeEach -> - overlapMarker = buffer.markRange([[0, 6], [0, 9]], invalidate: 'overlap') - neverMarker = overlapMarker.copy(invalidate: 'never') - surroundMarker = overlapMarker.copy(invalidate: 'surround') - insideMarker = overlapMarker.copy(invalidate: 'inside') - touchMarker = overlapMarker.copy(invalidate: 'touch') - allStrategies = [neverMarker, surroundMarker, overlapMarker, insideMarker, touchMarker] - markersUpdatedCount = 0 - - it "defers notifying Marker::onDidChange observers until after notifying Buffer::onDidChange observers", -> - for marker in allStrategies - do (marker) -> - marker.changes = [] - marker.onDidChange (change) -> - marker.changes.push(change) - - changedCount = 0 - changeSubscription = - buffer.onDidChange (change) -> - changedCount++ - expect(markersUpdatedCount).toBe 0 - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 8], [0, 11]] - expect(marker.isValid()).toBe true - expect(marker.changes.length).toBe 0 - - buffer.setTextInRange([[0, 1], [0, 2]], "ABC") - - expect(changedCount).toBe 1 - - for marker in allStrategies - expect(marker.changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 11] - oldTailPosition: [0, 6], newTailPosition: [0, 8] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: true - }] - expect(markersUpdatedCount).toBe 1 - - marker.changes = [] for marker in allStrategies - changeSubscription.dispose() - changedCount = 0 - markersUpdatedCount = 0 - buffer.onDidChange (change) -> - changedCount++ - expect(markersUpdatedCount).toBe 0 - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isValid()).toBe true - expect(marker.changes.length).toBe 0 - - it "notifies ::onDidUpdateMarkers observers even if there are no Marker::onDidChange observers", -> - expect(markersUpdatedCount).toBe 0 - buffer.insert([0, 0], "123") - expect(markersUpdatedCount).toBe 1 - overlapMarker.setRange([[0, 1], [0, 2]]) - expect(markersUpdatedCount).toBe 2 - - it "emits onDidChange events when undoing/redoing text changes that move the marker", -> - marker = buffer.markRange([[0, 4], [0, 8]]) - buffer.insert([0, 0], 'ABCD') - - changes = [] - marker.onDidChange (change) -> changes.push(change) - buffer.undo() - expect(changes.length).toBe 1 - expect(changes[0].newHeadPosition).toEqual [0, 8] - buffer.redo() - expect(changes.length).toBe 2 - expect(changes[1].newHeadPosition).toEqual [0, 12] - - describe "when a change precedes a marker", -> - it "shifts the marker based on the characters inserted or removed by the change", -> - buffer.setTextInRange([[0, 1], [0, 2]], "ABC") - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 8], [0, 11]] - expect(marker.isValid()).toBe true - - buffer.setTextInRange([[0, 1], [0, 1]], '\nDEF') - for marker in allStrategies - expect(marker.getRange()).toEqual [[1, 10], [1, 13]] - expect(marker.isValid()).toBe true - - describe "when a change follows a marker", -> - it "does not shift the marker", -> - buffer.setTextInRange([[0, 10], [0, 12]], "ABC") - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isValid()).toBe true - - describe "when a change starts at a marker's start position", -> - describe "when the marker has a tail", -> - it "interprets the change as being inside the marker for all invalidation strategies", -> - buffer.setTextInRange([[0, 6], [0, 7]], "ABC") - - for marker in difference(allStrategies, [insideMarker, touchMarker]) - expect(marker.getRange()).toEqual [[0, 6], [0, 11]] - expect(marker.isValid()).toBe true - - expect(insideMarker.getRange()).toEqual [[0, 9], [0, 11]] - expect(insideMarker.isValid()).toBe false - expect(touchMarker.getRange()).toEqual [[0, 6], [0, 11]] - expect(touchMarker.isValid()).toBe false - - describe "when the marker has no tail", -> - it "interprets the change as being outside the marker for all invalidation strategies", -> - for marker in allStrategies - marker.setRange([[0, 6], [0, 11]], reversed: true) - marker.clearTail() - expect(marker.getRange()).toEqual [[0, 6], [0, 6]] - - buffer.setTextInRange([[0, 6], [0, 6]], "ABC") - - for marker in difference(allStrategies, [touchMarker]) - expect(marker.getRange()).toEqual [[0, 9], [0, 9]] - expect(marker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 9], [0, 9]] - expect(touchMarker.isValid()).toBe false - - buffer.setTextInRange([[0, 9], [0, 9]], "DEF") - - for marker in difference(allStrategies, [touchMarker]) - expect(marker.getRange()).toEqual [[0, 12], [0, 12]] - expect(marker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 12], [0, 12]] - expect(touchMarker.isValid()).toBe false - - describe "when a change ends at a marker's start position but starts before it", -> - it "interprets the change as being outside the marker for all invalidation strategies", -> - buffer.setTextInRange([[0, 4], [0, 6]], "ABC") - - for marker in difference(allStrategies, [touchMarker]) - expect(marker.getRange()).toEqual [[0, 7], [0, 10]] - expect(marker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 7], [0, 10]] - expect(touchMarker.isValid()).toBe false - - describe "when a change starts and ends at a marker's start position", -> - it "interprets the change as being inside the marker for all invalidation strategies except 'inside'", -> - buffer.insert([0, 6], "ABC") - - for marker in difference(allStrategies, [insideMarker, touchMarker]) - expect(marker.getRange()).toEqual [[0, 6], [0, 12]] - expect(marker.isValid()).toBe true - - expect(insideMarker.getRange()).toEqual [[0, 9], [0, 12]] - expect(insideMarker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 6], [0, 12]] - expect(touchMarker.isValid()).toBe false - - describe "when a change starts at a marker's end position", -> - describe "when the change is an insertion", -> - it "interprets the change as being inside the marker for all invalidation strategies except 'inside'", -> - buffer.setTextInRange([[0, 9], [0, 9]], "ABC") - - for marker in difference(allStrategies, [insideMarker, touchMarker]) - expect(marker.getRange()).toEqual [[0, 6], [0, 12]] - expect(marker.isValid()).toBe true - - expect(insideMarker.getRange()).toEqual [[0, 6], [0, 9]] - expect(insideMarker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 6], [0, 12]] - expect(touchMarker.isValid()).toBe false - - describe "when the change replaces some existing text", -> - it "interprets the change as being outside the marker for all invalidation strategies", -> - buffer.setTextInRange([[0, 9], [0, 11]], "ABC") - - for marker in difference(allStrategies, [touchMarker]) - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 6], [0, 9]] - expect(touchMarker.isValid()).toBe false - - describe "when a change surrounds a marker", -> - it "truncates the marker to the end of the change and invalidates every invalidation strategy except 'never'", -> - buffer.setTextInRange([[0, 5], [0, 10]], "ABC") - - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 8], [0, 8]] - - for marker in difference(allStrategies, [neverMarker]) - expect(marker.isValid()).toBe false - - expect(neverMarker.isValid()).toBe true - - describe "when a change is inside a marker", -> - it "adjusts the marker's end position and invalidates markers with an 'inside' or 'touch' strategy", -> - buffer.setTextInRange([[0, 7], [0, 8]], "AB") - - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 6], [0, 10]] - - for marker in difference(allStrategies, [insideMarker, touchMarker]) - expect(marker.isValid()).toBe true - - expect(insideMarker.isValid()).toBe false - expect(touchMarker.isValid()).toBe false - - describe "when a change overlaps the start of a marker", -> - it "moves the start of the marker to the end of the change and invalidates the marker if its stategy is 'overlap', 'inside', or 'touch'", -> - buffer.setTextInRange([[0, 5], [0, 7]], "ABC") - - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 8], [0, 10]] - - expect(neverMarker.isValid()).toBe true - expect(surroundMarker.isValid()).toBe true - expect(overlapMarker.isValid()).toBe false - expect(insideMarker.isValid()).toBe false - expect(touchMarker.isValid()).toBe false - - describe "when a change overlaps the end of a marker", -> - it "moves the end of the marker to the end of the change and invalidates the marker if its stategy is 'overlap', 'inside', or 'touch'", -> - buffer.setTextInRange([[0, 8], [0, 10]], "ABC") - - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 6], [0, 11]] - - expect(neverMarker.isValid()).toBe true - expect(surroundMarker.isValid()).toBe true - expect(overlapMarker.isValid()).toBe false - expect(insideMarker.isValid()).toBe false - expect(touchMarker.isValid()).toBe false - - describe "when multiple changes occur in a transaction", -> - it "emits one change event for each marker that was indirectly updated", -> - for marker in allStrategies - do (marker) -> - marker.changes = [] - marker.onDidChange (change) -> - marker.changes.push(change) - - buffer.transact -> - buffer.insert([0, 7], ".") - buffer.append("!") - - for marker in allStrategies - expect(marker.changes.length).toBe 0 - - neverMarker.setRange([[0, 0], [0, 1]]) - - expect(neverMarker.changes).toEqual [{ - oldHeadPosition: [0, 9] - newHeadPosition: [0, 1] - oldTailPosition: [0, 6] - newTailPosition: [0, 0] - wasValid: true - isValid: true - hadTail: true - hasTail: true - oldProperties: {} - newProperties: {} - textChanged: false - }] - - expect(insideMarker.changes).toEqual [{ - oldHeadPosition: [0, 9] - newHeadPosition: [0, 10] - oldTailPosition: [0, 6] - newTailPosition: [0, 6] - wasValid: true - isValid: false - hadTail: true - hasTail: true - oldProperties: {} - newProperties: {} - textChanged: true - }] - - describe "destruction", -> - it "removes the marker from the buffer, marks it destroyed and invalid, and notifies ::onDidDestroy observers", -> - marker = buffer.markRange([[0, 3], [0, 6]]) - expect(buffer.getMarker(marker.id)).toBe marker - marker.onDidDestroy destroyedHandler = jasmine.createSpy("destroyedHandler") - - marker.destroy() - - expect(destroyedHandler.calls.count()).toBe 1 - expect(buffer.getMarker(marker.id)).toBeUndefined() - expect(marker.isDestroyed()).toBe true - expect(marker.isValid()).toBe false - expect(marker.getRange()).toEqual [[0, 0], [0, 0]] - - it "handles markers deleted in event handlers", -> - marker1 = buffer.markRange([[0, 3], [0, 6]]) - marker2 = marker1.copy() - marker3 = marker1.copy() - - marker1.onDidChange -> - marker1.destroy() - marker2.destroy() - marker3.destroy() - - # doesn't blow up. - buffer.insert([0, 0], "!") - - marker1 = buffer.markRange([[0, 3], [0, 6]]) - marker2 = marker1.copy() - marker3 = marker1.copy() - - marker1.onDidChange -> - marker1.destroy() - marker2.destroy() - marker3.destroy() - - # doesn't blow up. - buffer.undo() - - it "does not reinsert the marker if its range is later updated", -> - marker = buffer.markRange([[0, 3], [0, 6]]) - marker.destroy() - expect(buffer.findMarkers(intersectsRow: 0)).toEqual [] - marker.setRange([[0, 0], [0, 9]]) - expect(buffer.findMarkers(intersectsRow: 0)).toEqual [] - - it "does not blow up when destroy is called twice", -> - marker = buffer.markRange([[0, 3], [0, 6]]) - marker.destroy() - marker.destroy() - - describe "TextBuffer::findMarkers(properties)", -> - [marker1, marker2, marker3, marker4] = [] - - beforeEach -> - marker1 = buffer.markRange([[0, 0], [0, 3]], class: 'a') - marker2 = buffer.markRange([[0, 0], [0, 5]], class: 'a', invalidate: 'surround') - marker3 = buffer.markRange([[0, 4], [0, 7]], class: 'a') - marker4 = buffer.markRange([[0, 0], [0, 7]], class: 'b', invalidate: 'never') - - it "can find markers based on custom properties", -> - expect(buffer.findMarkers(class: 'a')).toEqual [marker2, marker1, marker3] - expect(buffer.findMarkers(class: 'b')).toEqual [marker4] - - it "can find markers based on their invalidation strategy", -> - expect(buffer.findMarkers(invalidate: 'overlap')).toEqual [marker1, marker3] - expect(buffer.findMarkers(invalidate: 'surround')).toEqual [marker2] - expect(buffer.findMarkers(invalidate: 'never')).toEqual [marker4] - - it "can find markers that start or end at a given position", -> - expect(buffer.findMarkers(startPosition: [0, 0])).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(startPosition: [0, 0], class: 'a')).toEqual [marker2, marker1] - expect(buffer.findMarkers(startPosition: [0, 0], endPosition: [0, 3], class: 'a')).toEqual [marker1] - expect(buffer.findMarkers(startPosition: [0, 4], endPosition: [0, 7])).toEqual [marker3] - expect(buffer.findMarkers(endPosition: [0, 7])).toEqual [marker4, marker3] - expect(buffer.findMarkers(endPosition: [0, 7], class: 'b')).toEqual [marker4] - - it "can find markers that start or end at a given range", -> - expect(buffer.findMarkers(startsInRange: [[0, 0], [0, 4]])).toEqual [marker4, marker2, marker1, marker3] - expect(buffer.findMarkers(startsInRange: [[0, 0], [0, 4]], class: 'a')).toEqual [marker2, marker1, marker3] - expect(buffer.findMarkers(startsInRange: [[0, 0], [0, 4]], endsInRange: [[0, 3], [0, 6]])).toEqual [marker2, marker1] - expect(buffer.findMarkers(endsInRange: [[0, 5], [0, 7]])).toEqual [marker4, marker2, marker3] - - it "can find markers that contain a given point", -> - expect(buffer.findMarkers(containsPosition: [0, 0])).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(containsPoint: [0, 0])).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(containsPoint: [0, 1], class: 'a')).toEqual [marker2, marker1] - expect(buffer.findMarkers(containsPoint: [0, 4])).toEqual [marker4, marker2, marker3] - - it "can find markers that contain a given range", -> - expect(buffer.findMarkers(containsRange: [[0, 1], [0, 4]])).toEqual [marker4, marker2] - expect(buffer.findMarkers(containsRange: [[0, 4], [0, 1]])).toEqual [marker4, marker2] - expect(buffer.findMarkers(containsRange: [[0, 1], [0, 3]])).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(containsRange: [[0, 6], [0, 7]])).toEqual [marker4, marker3] - - it "can find markers that intersect a given range", -> - expect(buffer.findMarkers(intersectsRange: [[0, 4], [0, 6]])).toEqual [marker4, marker2, marker3] - expect(buffer.findMarkers(intersectsRange: [[0, 0], [0, 2]])).toEqual [marker4, marker2, marker1] - - it "can find markers that start or end at a given row", -> - buffer.setTextInRange([[0, 7], [0, 7]], '\n') - buffer.setTextInRange([[0, 3], [0, 4]], ' \n') - expect(buffer.findMarkers(startRow: 0)).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(startRow: 1)).toEqual [marker3] - expect(buffer.findMarkers(endRow: 2)).toEqual [marker4, marker3] - expect(buffer.findMarkers(startRow: 0, endRow: 2)).toEqual [marker4] - - it "can find markers that intersect a given row", -> - buffer.setTextInRange([[0, 7], [0, 7]], '\n') - buffer.setTextInRange([[0, 3], [0, 4]], ' \n') - expect(buffer.findMarkers(intersectsRow: 0)).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(intersectsRow: 1)).toEqual [marker4, marker2, marker3] - - it "can find markers that intersect a given range", -> - buffer.setTextInRange([[0, 7], [0, 7]], '\n') - buffer.setTextInRange([[0, 3], [0, 4]], ' \n') - expect(buffer.findMarkers(intersectsRowRange: [1, 2])).toEqual [marker4, marker2, marker3] - - it "can find markers that are contained within a certain range, inclusive", -> - expect(buffer.findMarkers(containedInRange: [[0, 0], [0, 6]])).toEqual [marker2, marker1] - expect(buffer.findMarkers(containedInRange: [[0, 4], [0, 7]])).toEqual [marker3] diff --git a/spec/marker-spec.js b/spec/marker-spec.js new file mode 100644 index 0000000000..275abb2282 --- /dev/null +++ b/spec/marker-spec.js @@ -0,0 +1,883 @@ +const {difference, times, uniq} = require('underscore-plus'); +const TextBuffer = require('../src/text-buffer'); + +describe("Marker", function() { + let [buffer, markerCreations, markersUpdatedCount] = Array.from([]); + + beforeEach(function() { + jasmine.addCustomEqualityTester(require("underscore-plus").isEqual); + buffer = new TextBuffer({text: "abcdefghijklmnopqrstuvwxyz"}); + markerCreations = []; + buffer.onDidCreateMarker(marker => markerCreations.push(marker)); + markersUpdatedCount = 0; + return buffer.onDidUpdateMarkers(() => markersUpdatedCount++); + }); + + describe("creation", function() { + describe("TextBuffer::markRange(range, properties)", function() { + it("creates a marker for the given range with the given properties", function() { + const marker = buffer.markRange([[0, 3], [0, 6]]); + expect(marker.getRange()).toEqual([[0, 3], [0, 6]]); + expect(marker.getHeadPosition()).toEqual([0, 6]); + expect(marker.getTailPosition()).toEqual([0, 3]); + expect(marker.isReversed()).toBe(false); + expect(marker.hasTail()).toBe(true); + expect(markerCreations).toEqual([marker]); + return expect(markersUpdatedCount).toBe(1); + }); + + it("allows a reversed marker to be created", function() { + const marker = buffer.markRange([[0, 3], [0, 6]], {reversed: true}); + expect(marker.getRange()).toEqual([[0, 3], [0, 6]]); + expect(marker.getHeadPosition()).toEqual([0, 3]); + expect(marker.getTailPosition()).toEqual([0, 6]); + expect(marker.isReversed()).toBe(true); + return expect(marker.hasTail()).toBe(true); + }); + + it("allows an invalidation strategy to be assigned", function() { + const marker = buffer.markRange([[0, 3], [0, 6]], {invalidate: 'inside'}); + return expect(marker.getInvalidationStrategy()).toBe('inside'); + }); + + it("allows an exclusive marker to be created independently of its invalidation strategy", function() { + const layer = buffer.addMarkerLayer({maintainHistory: true}); + const marker1 = layer.markRange([[0, 3], [0, 6]], {invalidate: 'overlap', exclusive: true}); + const marker2 = marker1.copy(); + const marker3 = marker1.copy({exclusive: false}); + const marker4 = marker1.copy({exclusive: null, invalidate: 'inside'}); + + buffer.insert([0, 3], 'something'); + + expect(marker1.getStartPosition()).toEqual([0, 12]); + expect(marker1.isExclusive()).toBe(true); + expect(marker2.getStartPosition()).toEqual([0, 12]); + expect(marker2.isExclusive()).toBe(true); + expect(marker3.getStartPosition()).toEqual([0, 3]); + expect(marker3.isExclusive()).toBe(false); + expect(marker4.getStartPosition()).toEqual([0, 12]); + return expect(marker4.isExclusive()).toBe(true); + }); + + it("allows custom state to be assigned", function() { + const marker = buffer.markRange([[0, 3], [0, 6]], {foo: 1, bar: 2}); + return expect(marker.getProperties()).toEqual({foo: 1, bar: 2}); + }); + + it("clips the range before creating a marker with it", function() { + const marker = buffer.markRange([[-100, -100], [100, 100]]); + return expect(marker.getRange()).toEqual([[0, 0], [0, 26]]); + }); + + it("throws an error if an invalid point is given", function() { + const marker1 = buffer.markRange([[0, 1], [0, 2]]); + + expect(() => buffer.markRange([[0, NaN], [0, 2]])) + .toThrowError("Invalid Point: (0, NaN)"); + expect(() => buffer.markRange([[0, 1], [0, NaN]])) + .toThrowError("Invalid Point: (0, NaN)"); + + expect(buffer.findMarkers({})).toEqual([marker1]); + return expect(buffer.getMarkers()).toEqual([marker1]); + }); + + return it("allows arbitrary properties to be assigned", function() { + const marker = buffer.markRange([[0, 6], [0, 8]], {foo: 'bar'}); + return expect(marker.getProperties()).toEqual({foo: 'bar'}); + }); + }); + + return describe("TextBuffer::markPosition(position, properties)", function() { + it("creates a tail-less marker at the given position", function() { + const marker = buffer.markPosition([0, 6]); + expect(marker.getRange()).toEqual([[0, 6], [0, 6]]); + expect(marker.getHeadPosition()).toEqual([0, 6]); + expect(marker.getTailPosition()).toEqual([0, 6]); + expect(marker.isReversed()).toBe(false); + expect(marker.hasTail()).toBe(false); + return expect(markerCreations).toEqual([marker]); + }); + + it("allows an invalidation strategy to be assigned", function() { + const marker = buffer.markPosition([0, 3], {invalidate: 'inside'}); + return expect(marker.getInvalidationStrategy()).toBe('inside'); + }); + + it("throws an error if an invalid point is given", function() { + const marker1 = buffer.markPosition([0, 1]); + + expect(() => buffer.markPosition([0, NaN])) + .toThrowError("Invalid Point: (0, NaN)"); + + expect(buffer.findMarkers({})).toEqual([marker1]); + return expect(buffer.getMarkers()).toEqual([marker1]); + }); + + return it("allows arbitrary properties to be assigned", function() { + const marker = buffer.markPosition([0, 6], {foo: 'bar'}); + return expect(marker.getProperties()).toEqual({foo: 'bar'}); + }); + }); + }); + + describe("direct updates", function() { + let [marker, changes] = Array.from([]); + + beforeEach(function() { + marker = buffer.markRange([[0, 6], [0, 9]]); + changes = []; + markersUpdatedCount = 0; + return marker.onDidChange(change => changes.push(change)); + }); + + describe("::setHeadPosition(position, state)", function() { + it("sets the head position of the marker, flipping its orientation if necessary", function() { + marker.setHeadPosition([0, 12]); + expect(marker.getRange()).toEqual([[0, 6], [0, 12]]); + expect(marker.isReversed()).toBe(false); + expect(markersUpdatedCount).toBe(1); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 12], + oldTailPosition: [0, 6], newTailPosition: [0, 6], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setHeadPosition([0, 3]); + expect(markersUpdatedCount).toBe(2); + expect(marker.getRange()).toEqual([[0, 3], [0, 6]]); + expect(marker.isReversed()).toBe(true); + expect(changes).toEqual([{ + oldHeadPosition: [0, 12], newHeadPosition: [0, 3], + oldTailPosition: [0, 6], newTailPosition: [0, 6], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setHeadPosition([0, 9]); + expect(markersUpdatedCount).toBe(3); + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isReversed()).toBe(false); + return expect(changes).toEqual([{ + oldHeadPosition: [0, 3], newHeadPosition: [0, 9], + oldTailPosition: [0, 6], newTailPosition: [0, 6], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + }); + + it("does not give the marker a tail if it doesn't have one already", function() { + marker.clearTail(); + expect(marker.hasTail()).toBe(false); + marker.setHeadPosition([0, 15]); + expect(marker.hasTail()).toBe(false); + return expect(marker.getRange()).toEqual([[0, 15], [0, 15]]); + }); + + it("does not notify ::onDidChange observers and returns false if the position isn't actually changed", function() { + expect(marker.setHeadPosition(marker.getHeadPosition())).toBe(false); + expect(markersUpdatedCount).toBe(0); + return expect(changes.length).toBe(0); + }); + + return it("clips the assigned position", function() { + marker.setHeadPosition([100, 100]); + return expect(marker.getHeadPosition()).toEqual([0, 26]); + }); + }); + + describe("::setTailPosition(position, state)", function() { + it("sets the head position of the marker, flipping its orientation if necessary", function() { + marker.setTailPosition([0, 3]); + expect(marker.getRange()).toEqual([[0, 3], [0, 9]]); + expect(marker.isReversed()).toBe(false); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 9], + oldTailPosition: [0, 6], newTailPosition: [0, 3], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setTailPosition([0, 12]); + expect(marker.getRange()).toEqual([[0, 9], [0, 12]]); + expect(marker.isReversed()).toBe(true); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 9], + oldTailPosition: [0, 3], newTailPosition: [0, 12], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setTailPosition([0, 6]); + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isReversed()).toBe(false); + return expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 9], + oldTailPosition: [0, 12], newTailPosition: [0, 6], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + }); + + it("plants the tail of the marker if it does not have a tail", function() { + marker.clearTail(); + expect(marker.hasTail()).toBe(false); + marker.setTailPosition([0, 0]); + expect(marker.hasTail()).toBe(true); + return expect(marker.getRange()).toEqual([[0, 0], [0, 9]]); + }); + + it("does not notify ::onDidChange observers and returns false if the position isn't actually changed", function() { + expect(marker.setTailPosition(marker.getTailPosition())).toBe(false); + return expect(changes.length).toBe(0); + }); + + return it("clips the assigned position", function() { + marker.setTailPosition([100, 100]); + return expect(marker.getTailPosition()).toEqual([0, 26]); + }); + }); + + describe("::setRange(range, options)", function() { + it("sets the head and tail position simultaneously, flipping the orientation if the 'isReversed' option is true", function() { + marker.setRange([[0, 8], [0, 12]]); + expect(marker.getRange()).toEqual([[0, 8], [0, 12]]); + expect(marker.isReversed()).toBe(false); + expect(marker.getHeadPosition()).toEqual([0, 12]); + expect(marker.getTailPosition()).toEqual([0, 8]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 12], + oldTailPosition: [0, 6], newTailPosition: [0, 8], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setRange([[0, 3], [0, 9]], {reversed: true}); + expect(marker.getRange()).toEqual([[0, 3], [0, 9]]); + expect(marker.isReversed()).toBe(true); + expect(marker.getHeadPosition()).toEqual([0, 3]); + expect(marker.getTailPosition()).toEqual([0, 9]); + return expect(changes).toEqual([{ + oldHeadPosition: [0, 12], newHeadPosition: [0, 3], + oldTailPosition: [0, 8], newTailPosition: [0, 9], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + }); + + it("plants the tail of the marker if it does not have a tail", function() { + marker.clearTail(); + expect(marker.hasTail()).toBe(false); + marker.setRange([[0, 1], [0, 10]]); + expect(marker.hasTail()).toBe(true); + return expect(marker.getRange()).toEqual([[0, 1], [0, 10]]); + }); + + it("clips the assigned range", function() { + marker.setRange([[-100, -100], [100, 100]]); + return expect(marker.getRange()).toEqual([[0, 0], [0, 26]]); + }); + + it("emits the right events when called inside of an ::onDidChange handler", function() { + marker.onDidChange(function(change) { + if (marker.getHeadPosition().isEqual([0, 5])) { + return marker.setHeadPosition([0, 6]); + } + }); + + marker.setHeadPosition([0, 5]); + + const headPositions = (() => { + const result = []; + for (let {oldHeadPosition, newHeadPosition} of Array.from(changes)) { + result.push({old: oldHeadPosition, new: newHeadPosition}); + } + return result; + })(); + + return expect(headPositions).toEqual([ + {old: [0, 9], new: [0, 5]}, + {old: [0, 5], new: [0, 6]} + ]); + }); + + return it("throws an error if an invalid range is given", function() { + expect(() => marker.setRange([[0, NaN], [0, 12]])) + .toThrowError("Invalid Point: (0, NaN)"); + + expect(buffer.findMarkers({})).toEqual([marker]); + return expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + }); + }); + + describe("::clearTail() / ::plantTail()", () => it("clears the tail / plants the tail at the current head position", function() { + marker.setRange([[0, 6], [0, 9]], {reversed: true}); + + changes = []; + marker.clearTail(); + expect(marker.getRange()).toEqual([[0, 6], [0, 6]]); + expect(marker.hasTail()).toBe(false); + expect(marker.isReversed()).toBe(false); + + expect(changes).toEqual([{ + oldHeadPosition: [0, 6], newHeadPosition: [0, 6], + oldTailPosition: [0, 9], newTailPosition: [0, 6], + hadTail: true, hasTail: false, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setHeadPosition([0, 12]); + expect(marker.getRange()).toEqual([[0, 12], [0, 12]]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 6], newHeadPosition: [0, 12], + oldTailPosition: [0, 6], newTailPosition: [0, 12], + hadTail: false, hasTail: false, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.plantTail(); + expect(marker.hasTail()).toBe(true); + expect(marker.isReversed()).toBe(false); + expect(marker.getRange()).toEqual([[0, 12], [0, 12]]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 12], newHeadPosition: [0, 12], + oldTailPosition: [0, 12], newTailPosition: [0, 12], + hadTail: false, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setHeadPosition([0, 15]); + expect(marker.getRange()).toEqual([[0, 12], [0, 15]]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 12], newHeadPosition: [0, 15], + oldTailPosition: [0, 12], newTailPosition: [0, 12], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.plantTail(); + expect(marker.getRange()).toEqual([[0, 12], [0, 15]]); + return expect(changes).toEqual([]); + })); + + return describe("::setProperties(properties)", () => it("merges the given properties into the current properties", function() { + marker.setProperties({foo: 1}); + expect(marker.getProperties()).toEqual({foo: 1}); + marker.setProperties({bar: 2}); + expect(marker.getProperties()).toEqual({foo: 1, bar: 2}); + return expect(markersUpdatedCount).toBe(2); + })); + }); + + describe("indirect updates (due to buffer changes)", function() { + let [allStrategies, neverMarker, surroundMarker, overlapMarker, insideMarker, touchMarker] = Array.from([]); + + beforeEach(function() { + overlapMarker = buffer.markRange([[0, 6], [0, 9]], {invalidate: 'overlap'}); + neverMarker = overlapMarker.copy({invalidate: 'never'}); + surroundMarker = overlapMarker.copy({invalidate: 'surround'}); + insideMarker = overlapMarker.copy({invalidate: 'inside'}); + touchMarker = overlapMarker.copy({invalidate: 'touch'}); + allStrategies = [neverMarker, surroundMarker, overlapMarker, insideMarker, touchMarker]; + return markersUpdatedCount = 0; + }); + + it("defers notifying Marker::onDidChange observers until after notifying Buffer::onDidChange observers", function() { + let marker; + for (marker of Array.from(allStrategies)) { + (function(marker) { + marker.changes = []; + return marker.onDidChange(change => marker.changes.push(change)); + })(marker); + } + + let changedCount = 0; + const changeSubscription = + buffer.onDidChange(function(change) { + changedCount++; + expect(markersUpdatedCount).toBe(0); + return (() => { + const result = []; + for (marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[0, 8], [0, 11]]); + expect(marker.isValid()).toBe(true); + result.push(expect(marker.changes.length).toBe(0)); + } + return result; + })(); + }); + + buffer.setTextInRange([[0, 1], [0, 2]], "ABC"); + + expect(changedCount).toBe(1); + + for (marker of Array.from(allStrategies)) { + expect(marker.changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 11], + oldTailPosition: [0, 6], newTailPosition: [0, 8], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: true + }]); + } + expect(markersUpdatedCount).toBe(1); + + for (marker of Array.from(allStrategies)) { marker.changes = []; } + changeSubscription.dispose(); + changedCount = 0; + markersUpdatedCount = 0; + return buffer.onDidChange(function(change) { + changedCount++; + expect(markersUpdatedCount).toBe(0); + return (() => { + const result = []; + for (marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isValid()).toBe(true); + result.push(expect(marker.changes.length).toBe(0)); + } + return result; + })(); + }); + }); + + it("notifies ::onDidUpdateMarkers observers even if there are no Marker::onDidChange observers", function() { + expect(markersUpdatedCount).toBe(0); + buffer.insert([0, 0], "123"); + expect(markersUpdatedCount).toBe(1); + overlapMarker.setRange([[0, 1], [0, 2]]); + return expect(markersUpdatedCount).toBe(2); + }); + + it("emits onDidChange events when undoing/redoing text changes that move the marker", function() { + const marker = buffer.markRange([[0, 4], [0, 8]]); + buffer.insert([0, 0], 'ABCD'); + + const changes = []; + marker.onDidChange(change => changes.push(change)); + buffer.undo(); + expect(changes.length).toBe(1); + expect(changes[0].newHeadPosition).toEqual([0, 8]); + buffer.redo(); + expect(changes.length).toBe(2); + return expect(changes[1].newHeadPosition).toEqual([0, 12]); + }); + + describe("when a change precedes a marker", () => it("shifts the marker based on the characters inserted or removed by the change", function() { + let marker; + buffer.setTextInRange([[0, 1], [0, 2]], "ABC"); + for (marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[0, 8], [0, 11]]); + expect(marker.isValid()).toBe(true); + } + + buffer.setTextInRange([[0, 1], [0, 1]], '\nDEF'); + return (() => { + const result = []; + for (marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[1, 10], [1, 13]]); + result.push(expect(marker.isValid()).toBe(true)); + } + return result; + })(); + })); + + describe("when a change follows a marker", () => it("does not shift the marker", function() { + buffer.setTextInRange([[0, 10], [0, 12]], "ABC"); + return (() => { + const result = []; + for (let marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + result.push(expect(marker.isValid()).toBe(true)); + } + return result; + })(); + })); + + describe("when a change starts at a marker's start position", function() { + describe("when the marker has a tail", () => it("interprets the change as being inside the marker for all invalidation strategies", function() { + buffer.setTextInRange([[0, 6], [0, 7]], "ABC"); + + for (let marker of Array.from(difference(allStrategies, [insideMarker, touchMarker]))) { + expect(marker.getRange()).toEqual([[0, 6], [0, 11]]); + expect(marker.isValid()).toBe(true); + } + + expect(insideMarker.getRange()).toEqual([[0, 9], [0, 11]]); + expect(insideMarker.isValid()).toBe(false); + expect(touchMarker.getRange()).toEqual([[0, 6], [0, 11]]); + return expect(touchMarker.isValid()).toBe(false); + })); + + return describe("when the marker has no tail", () => it("interprets the change as being outside the marker for all invalidation strategies", function() { + let marker; + for (marker of Array.from(allStrategies)) { + marker.setRange([[0, 6], [0, 11]], {reversed: true}); + marker.clearTail(); + expect(marker.getRange()).toEqual([[0, 6], [0, 6]]); + } + + buffer.setTextInRange([[0, 6], [0, 6]], "ABC"); + + for (marker of Array.from(difference(allStrategies, [touchMarker]))) { + expect(marker.getRange()).toEqual([[0, 9], [0, 9]]); + expect(marker.isValid()).toBe(true); + } + + expect(touchMarker.getRange()).toEqual([[0, 9], [0, 9]]); + expect(touchMarker.isValid()).toBe(false); + + buffer.setTextInRange([[0, 9], [0, 9]], "DEF"); + + for (marker of Array.from(difference(allStrategies, [touchMarker]))) { + expect(marker.getRange()).toEqual([[0, 12], [0, 12]]); + expect(marker.isValid()).toBe(true); + } + + expect(touchMarker.getRange()).toEqual([[0, 12], [0, 12]]); + return expect(touchMarker.isValid()).toBe(false); + })); + }); + + describe("when a change ends at a marker's start position but starts before it", () => it("interprets the change as being outside the marker for all invalidation strategies", function() { + buffer.setTextInRange([[0, 4], [0, 6]], "ABC"); + + for (let marker of Array.from(difference(allStrategies, [touchMarker]))) { + expect(marker.getRange()).toEqual([[0, 7], [0, 10]]); + expect(marker.isValid()).toBe(true); + } + + expect(touchMarker.getRange()).toEqual([[0, 7], [0, 10]]); + return expect(touchMarker.isValid()).toBe(false); + })); + + describe("when a change starts and ends at a marker's start position", () => it("interprets the change as being inside the marker for all invalidation strategies except 'inside'", function() { + buffer.insert([0, 6], "ABC"); + + for (let marker of Array.from(difference(allStrategies, [insideMarker, touchMarker]))) { + expect(marker.getRange()).toEqual([[0, 6], [0, 12]]); + expect(marker.isValid()).toBe(true); + } + + expect(insideMarker.getRange()).toEqual([[0, 9], [0, 12]]); + expect(insideMarker.isValid()).toBe(true); + + expect(touchMarker.getRange()).toEqual([[0, 6], [0, 12]]); + return expect(touchMarker.isValid()).toBe(false); + })); + + describe("when a change starts at a marker's end position", function() { + describe("when the change is an insertion", () => it("interprets the change as being inside the marker for all invalidation strategies except 'inside'", function() { + buffer.setTextInRange([[0, 9], [0, 9]], "ABC"); + + for (let marker of Array.from(difference(allStrategies, [insideMarker, touchMarker]))) { + expect(marker.getRange()).toEqual([[0, 6], [0, 12]]); + expect(marker.isValid()).toBe(true); + } + + expect(insideMarker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(insideMarker.isValid()).toBe(true); + + expect(touchMarker.getRange()).toEqual([[0, 6], [0, 12]]); + return expect(touchMarker.isValid()).toBe(false); + })); + + return describe("when the change replaces some existing text", () => it("interprets the change as being outside the marker for all invalidation strategies", function() { + buffer.setTextInRange([[0, 9], [0, 11]], "ABC"); + + for (let marker of Array.from(difference(allStrategies, [touchMarker]))) { + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isValid()).toBe(true); + } + + expect(touchMarker.getRange()).toEqual([[0, 6], [0, 9]]); + return expect(touchMarker.isValid()).toBe(false); + })); + }); + + describe("when a change surrounds a marker", () => it("truncates the marker to the end of the change and invalidates every invalidation strategy except 'never'", function() { + let marker; + buffer.setTextInRange([[0, 5], [0, 10]], "ABC"); + + for (marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[0, 8], [0, 8]]); + } + + for (marker of Array.from(difference(allStrategies, [neverMarker]))) { + expect(marker.isValid()).toBe(false); + } + + return expect(neverMarker.isValid()).toBe(true); + })); + + describe("when a change is inside a marker", () => it("adjusts the marker's end position and invalidates markers with an 'inside' or 'touch' strategy", function() { + let marker; + buffer.setTextInRange([[0, 7], [0, 8]], "AB"); + + for (marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[0, 6], [0, 10]]); + } + + for (marker of Array.from(difference(allStrategies, [insideMarker, touchMarker]))) { + expect(marker.isValid()).toBe(true); + } + + expect(insideMarker.isValid()).toBe(false); + return expect(touchMarker.isValid()).toBe(false); + })); + + describe("when a change overlaps the start of a marker", () => it("moves the start of the marker to the end of the change and invalidates the marker if its stategy is 'overlap', 'inside', or 'touch'", function() { + buffer.setTextInRange([[0, 5], [0, 7]], "ABC"); + + for (let marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[0, 8], [0, 10]]); + } + + expect(neverMarker.isValid()).toBe(true); + expect(surroundMarker.isValid()).toBe(true); + expect(overlapMarker.isValid()).toBe(false); + expect(insideMarker.isValid()).toBe(false); + return expect(touchMarker.isValid()).toBe(false); + })); + + describe("when a change overlaps the end of a marker", () => it("moves the end of the marker to the end of the change and invalidates the marker if its stategy is 'overlap', 'inside', or 'touch'", function() { + buffer.setTextInRange([[0, 8], [0, 10]], "ABC"); + + for (let marker of Array.from(allStrategies)) { + expect(marker.getRange()).toEqual([[0, 6], [0, 11]]); + } + + expect(neverMarker.isValid()).toBe(true); + expect(surroundMarker.isValid()).toBe(true); + expect(overlapMarker.isValid()).toBe(false); + expect(insideMarker.isValid()).toBe(false); + return expect(touchMarker.isValid()).toBe(false); + })); + + return describe("when multiple changes occur in a transaction", () => it("emits one change event for each marker that was indirectly updated", function() { + let marker; + for (marker of Array.from(allStrategies)) { + (function(marker) { + marker.changes = []; + return marker.onDidChange(change => marker.changes.push(change)); + })(marker); + } + + buffer.transact(function() { + buffer.insert([0, 7], "."); + buffer.append("!"); + + for (marker of Array.from(allStrategies)) { + expect(marker.changes.length).toBe(0); + } + + return neverMarker.setRange([[0, 0], [0, 1]]); + }); + + expect(neverMarker.changes).toEqual([{ + oldHeadPosition: [0, 9], + newHeadPosition: [0, 1], + oldTailPosition: [0, 6], + newTailPosition: [0, 0], + wasValid: true, + isValid: true, + hadTail: true, + hasTail: true, + oldProperties: {}, + newProperties: {}, + textChanged: false + }]); + + return expect(insideMarker.changes).toEqual([{ + oldHeadPosition: [0, 9], + newHeadPosition: [0, 10], + oldTailPosition: [0, 6], + newTailPosition: [0, 6], + wasValid: true, + isValid: false, + hadTail: true, + hasTail: true, + oldProperties: {}, + newProperties: {}, + textChanged: true + }]); + })); +}); + + describe("destruction", function() { + it("removes the marker from the buffer, marks it destroyed and invalid, and notifies ::onDidDestroy observers", function() { + let destroyedHandler; + const marker = buffer.markRange([[0, 3], [0, 6]]); + expect(buffer.getMarker(marker.id)).toBe(marker); + marker.onDidDestroy(destroyedHandler = jasmine.createSpy("destroyedHandler")); + + marker.destroy(); + + expect(destroyedHandler.calls.count()).toBe(1); + expect(buffer.getMarker(marker.id)).toBeUndefined(); + expect(marker.isDestroyed()).toBe(true); + expect(marker.isValid()).toBe(false); + return expect(marker.getRange()).toEqual([[0, 0], [0, 0]]); + }); + + it("handles markers deleted in event handlers", function() { + let marker1 = buffer.markRange([[0, 3], [0, 6]]); + let marker2 = marker1.copy(); + let marker3 = marker1.copy(); + + marker1.onDidChange(function() { + marker1.destroy(); + marker2.destroy(); + return marker3.destroy(); + }); + + // doesn't blow up. + buffer.insert([0, 0], "!"); + + marker1 = buffer.markRange([[0, 3], [0, 6]]); + marker2 = marker1.copy(); + marker3 = marker1.copy(); + + marker1.onDidChange(function() { + marker1.destroy(); + marker2.destroy(); + return marker3.destroy(); + }); + + // doesn't blow up. + return buffer.undo(); + }); + + it("does not reinsert the marker if its range is later updated", function() { + const marker = buffer.markRange([[0, 3], [0, 6]]); + marker.destroy(); + expect(buffer.findMarkers({intersectsRow: 0})).toEqual([]); + marker.setRange([[0, 0], [0, 9]]); + return expect(buffer.findMarkers({intersectsRow: 0})).toEqual([]); + }); + + return it("does not blow up when destroy is called twice", function() { + const marker = buffer.markRange([[0, 3], [0, 6]]); + marker.destroy(); + return marker.destroy(); + }); + }); + + return describe("TextBuffer::findMarkers(properties)", function() { + let [marker1, marker2, marker3, marker4] = Array.from([]); + + beforeEach(function() { + marker1 = buffer.markRange([[0, 0], [0, 3]], {class: 'a'}); + marker2 = buffer.markRange([[0, 0], [0, 5]], {class: 'a', invalidate: 'surround'}); + marker3 = buffer.markRange([[0, 4], [0, 7]], {class: 'a'}); + return marker4 = buffer.markRange([[0, 0], [0, 7]], {class: 'b', invalidate: 'never'}); + }); + + it("can find markers based on custom properties", function() { + expect(buffer.findMarkers({class: 'a'})).toEqual([marker2, marker1, marker3]); + return expect(buffer.findMarkers({class: 'b'})).toEqual([marker4]); + }); + + it("can find markers based on their invalidation strategy", function() { + expect(buffer.findMarkers({invalidate: 'overlap'})).toEqual([marker1, marker3]); + expect(buffer.findMarkers({invalidate: 'surround'})).toEqual([marker2]); + return expect(buffer.findMarkers({invalidate: 'never'})).toEqual([marker4]); + }); + + it("can find markers that start or end at a given position", function() { + expect(buffer.findMarkers({startPosition: [0, 0]})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({startPosition: [0, 0], class: 'a'})).toEqual([marker2, marker1]); + expect(buffer.findMarkers({startPosition: [0, 0], endPosition: [0, 3], class: 'a'})).toEqual([marker1]); + expect(buffer.findMarkers({startPosition: [0, 4], endPosition: [0, 7]})).toEqual([marker3]); + expect(buffer.findMarkers({endPosition: [0, 7]})).toEqual([marker4, marker3]); + return expect(buffer.findMarkers({endPosition: [0, 7], class: 'b'})).toEqual([marker4]); + }); + + it("can find markers that start or end at a given range", function() { + expect(buffer.findMarkers({startsInRange: [[0, 0], [0, 4]]})).toEqual([marker4, marker2, marker1, marker3]); + expect(buffer.findMarkers({startsInRange: [[0, 0], [0, 4]], class: 'a'})).toEqual([marker2, marker1, marker3]); + expect(buffer.findMarkers({startsInRange: [[0, 0], [0, 4]], endsInRange: [[0, 3], [0, 6]]})).toEqual([marker2, marker1]); + return expect(buffer.findMarkers({endsInRange: [[0, 5], [0, 7]]})).toEqual([marker4, marker2, marker3]); + }); + + it("can find markers that contain a given point", function() { + expect(buffer.findMarkers({containsPosition: [0, 0]})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({containsPoint: [0, 0]})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({containsPoint: [0, 1], class: 'a'})).toEqual([marker2, marker1]); + return expect(buffer.findMarkers({containsPoint: [0, 4]})).toEqual([marker4, marker2, marker3]); + }); + + it("can find markers that contain a given range", function() { + expect(buffer.findMarkers({containsRange: [[0, 1], [0, 4]]})).toEqual([marker4, marker2]); + expect(buffer.findMarkers({containsRange: [[0, 4], [0, 1]]})).toEqual([marker4, marker2]); + expect(buffer.findMarkers({containsRange: [[0, 1], [0, 3]]})).toEqual([marker4, marker2, marker1]); + return expect(buffer.findMarkers({containsRange: [[0, 6], [0, 7]]})).toEqual([marker4, marker3]); + }); + + it("can find markers that intersect a given range", function() { + expect(buffer.findMarkers({intersectsRange: [[0, 4], [0, 6]]})).toEqual([marker4, marker2, marker3]); + return expect(buffer.findMarkers({intersectsRange: [[0, 0], [0, 2]]})).toEqual([marker4, marker2, marker1]); + }); + + it("can find markers that start or end at a given row", function() { + buffer.setTextInRange([[0, 7], [0, 7]], '\n'); + buffer.setTextInRange([[0, 3], [0, 4]], ' \n'); + expect(buffer.findMarkers({startRow: 0})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({startRow: 1})).toEqual([marker3]); + expect(buffer.findMarkers({endRow: 2})).toEqual([marker4, marker3]); + return expect(buffer.findMarkers({startRow: 0, endRow: 2})).toEqual([marker4]); + }); + + it("can find markers that intersect a given row", function() { + buffer.setTextInRange([[0, 7], [0, 7]], '\n'); + buffer.setTextInRange([[0, 3], [0, 4]], ' \n'); + expect(buffer.findMarkers({intersectsRow: 0})).toEqual([marker4, marker2, marker1]); + return expect(buffer.findMarkers({intersectsRow: 1})).toEqual([marker4, marker2, marker3]); + }); + + it("can find markers that intersect a given range", function() { + buffer.setTextInRange([[0, 7], [0, 7]], '\n'); + buffer.setTextInRange([[0, 3], [0, 4]], ' \n'); + return expect(buffer.findMarkers({intersectsRowRange: [1, 2]})).toEqual([marker4, marker2, marker3]); + }); + + return it("can find markers that are contained within a certain range, inclusive", function() { + expect(buffer.findMarkers({containedInRange: [[0, 0], [0, 6]]})).toEqual([marker2, marker1]); + return expect(buffer.findMarkers({containedInRange: [[0, 4], [0, 7]]})).toEqual([marker3]); + }); +}); +}); diff --git a/spec/point-spec.coffee b/spec/point-spec.coffee deleted file mode 100644 index 317a4d7152..0000000000 --- a/spec/point-spec.coffee +++ /dev/null @@ -1,196 +0,0 @@ -Point = require '../src/point' - -describe "Point", -> - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - - describe "::negate()", -> - it "should negate the row and column", -> - expect(new Point( 0, 0).negate().toString()).toBe "(0, 0)" - expect(new Point( 1, 2).negate().toString()).toBe "(-1, -2)" - expect(new Point(-1, -2).negate().toString()).toBe "(1, 2)" - expect(new Point(-1, 2).negate().toString()).toBe "(1, -2)" - - describe "::fromObject(object, copy)", -> - it "returns a new Point if object is point-compatible array ", -> - expect(Point.fromObject([1, 3])).toEqual Point(1, 3) - expect(Point.fromObject([Infinity, Infinity])).toEqual Point.INFINITY - - it "returns the copy of object if it is an instanceof Point", -> - origin = Point(0, 0) - expect(Point.fromObject(origin, false) is origin).toBe true - expect(Point.fromObject(origin, true) is origin).toBe false - - describe "::copy()", -> - it "returns a copy of the object", -> - expect(Point(3, 4).copy()).toEqual Point(3, 4) - expect(Point.ZERO.copy()).toEqual [0, 0] - - describe "::negate()", -> - it "returns a new point with row and column negated", -> - expect(Point(3, 4).negate()).toEqual Point(-3, -4) - expect(Point.ZERO.negate()).toEqual [0, 0] - - describe "::freeze()", -> - it "makes the Point object immutable", -> - expect(Object.isFrozen(Point(3, 4).freeze())).toBe true - expect(Object.isFrozen(Point.ZERO.freeze())).toBe true - - describe "::compare(other)", -> - it "returns -1 for <, 0 for =, 1 for > comparisions", -> - expect(Point(2, 3).compare(Point(2, 6))).toBe -1 - expect(Point(2, 3).compare(Point(3, 4))).toBe -1 - expect(Point(1, 1).compare(Point(1, 1))).toBe 0 - expect(Point(2, 3).compare(Point(2, 0))).toBe 1 - expect(Point(2, 3).compare(Point(1, 3))).toBe 1 - - expect(Point(2, 3).compare([2, 6])).toBe -1 - expect(Point(2, 3).compare([3, 4])).toBe -1 - expect(Point(1, 1).compare([1, 1])).toBe 0 - expect(Point(2, 3).compare([2, 0])).toBe 1 - expect(Point(2, 3).compare([1, 3])).toBe 1 - - describe "::isLessThan(other)", -> - it "returns a boolean indicating whether a point precedes the given Point ", -> - expect(Point(2, 3).isLessThan(Point(2, 5))).toBe true - expect(Point(2, 3).isLessThan(Point(3, 4))).toBe true - expect(Point(2, 3).isLessThan(Point(2, 3))).toBe false - expect(Point(2, 3).isLessThan(Point(2, 1))).toBe false - expect(Point(2, 3).isLessThan(Point(1, 2))).toBe false - - expect(Point(2, 3).isLessThan([2, 5])).toBe true - expect(Point(2, 3).isLessThan([3, 4])).toBe true - expect(Point(2, 3).isLessThan([2, 3])).toBe false - expect(Point(2, 3).isLessThan([2, 1])).toBe false - expect(Point(2, 3).isLessThan([1, 2])).toBe false - - describe "::isLessThanOrEqual(other)", -> - it "returns a boolean indicating whether a point precedes or equal the given Point ", -> - expect(Point(2, 3).isLessThanOrEqual(Point(2, 5))).toBe true - expect(Point(2, 3).isLessThanOrEqual(Point(3, 4))).toBe true - expect(Point(2, 3).isLessThanOrEqual(Point(2, 3))).toBe true - expect(Point(2, 3).isLessThanOrEqual(Point(2, 1))).toBe false - expect(Point(2, 3).isLessThanOrEqual(Point(1, 2))).toBe false - - expect(Point(2, 3).isLessThanOrEqual([2, 5])).toBe true - expect(Point(2, 3).isLessThanOrEqual([3, 4])).toBe true - expect(Point(2, 3).isLessThanOrEqual([2, 3])).toBe true - expect(Point(2, 3).isLessThanOrEqual([2, 1])).toBe false - expect(Point(2, 3).isLessThanOrEqual([1, 2])).toBe false - - describe "::isGreaterThan(other)", -> - it "returns a boolean indicating whether a point follows the given Point ", -> - expect(Point(2, 3).isGreaterThan(Point(2, 5))).toBe false - expect(Point(2, 3).isGreaterThan(Point(3, 4))).toBe false - expect(Point(2, 3).isGreaterThan(Point(2, 3))).toBe false - expect(Point(2, 3).isGreaterThan(Point(2, 1))).toBe true - expect(Point(2, 3).isGreaterThan(Point(1, 2))).toBe true - - expect(Point(2, 3).isGreaterThan([2, 5])).toBe false - expect(Point(2, 3).isGreaterThan([3, 4])).toBe false - expect(Point(2, 3).isGreaterThan([2, 3])).toBe false - expect(Point(2, 3).isGreaterThan([2, 1])).toBe true - expect(Point(2, 3).isGreaterThan([1, 2])).toBe true - - describe "::isGreaterThanOrEqual(other)", -> - it "returns a boolean indicating whether a point follows or equal the given Point ", -> - expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 5))).toBe false - expect(Point(2, 3).isGreaterThanOrEqual(Point(3, 4))).toBe false - expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 3))).toBe true - expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 1))).toBe true - expect(Point(2, 3).isGreaterThanOrEqual(Point(1, 2))).toBe true - - expect(Point(2, 3).isGreaterThanOrEqual([2, 5])).toBe false - expect(Point(2, 3).isGreaterThanOrEqual([3, 4])).toBe false - expect(Point(2, 3).isGreaterThanOrEqual([2, 3])).toBe true - expect(Point(2, 3).isGreaterThanOrEqual([2, 1])).toBe true - expect(Point(2, 3).isGreaterThanOrEqual([1, 2])).toBe true - - describe "::isEqual()", -> - it "returns if whether two points are equal", -> - expect(Point(1, 1).isEqual(Point(1, 1))).toBe true - expect(Point(1, 1).isEqual([1, 1])).toBe true - expect(Point(1, 2).isEqual(Point(3, 3))).toBe false - expect(Point(1, 2).isEqual([3, 3])).toBe false - - describe "::isPositive()", -> - it "returns true if the point represents a forward traversal", -> - expect(Point(-1, -1).isPositive()).toBe false - expect(Point(-1, 0).isPositive()).toBe false - expect(Point(-1, Infinity).isPositive()).toBe false - expect(Point(0, 0).isPositive()).toBe false - - expect(Point(0, 1).isPositive()).toBe true - expect(Point(5, 0).isPositive()).toBe true - expect(Point(5, -1).isPositive()).toBe true - - describe "::isZero()", -> - it "returns true if the point is zero", -> - expect(Point(1, 1).isZero()).toBe false - expect(Point(0, 1).isZero()).toBe false - expect(Point(1, 0).isZero()).toBe false - expect(Point(0, 0).isZero()).toBe true - - describe "::min(a, b)", -> - it "returns the minimum of two points", -> - expect(Point.min(Point(3, 4), Point(1, 1))).toEqual Point(1, 1) - expect(Point.min(Point(1, 2), Point(5, 6))).toEqual Point(1, 2) - expect(Point.min([3, 4], [1, 1])).toEqual [1, 1] - expect(Point.min([1, 2], [5, 6])).toEqual [1, 2] - - describe "::max(a, b)", -> - it "returns the minimum of two points", -> - expect(Point.max(Point(3, 4), Point(1, 1))).toEqual Point(3, 4) - expect(Point.max(Point(1, 2), Point(5, 6))).toEqual Point(5, 6) - expect(Point.max([3, 4], [1, 1])).toEqual [3, 4] - expect(Point.max([1, 2], [5, 6])).toEqual [5, 6] - - describe "::translate(delta)", -> - it "returns a new point by adding corresponding coordinates", -> - expect(Point(1, 1).translate(Point(2, 3))).toEqual Point(3, 4) - expect(Point.INFINITY.translate(Point(2, 3))).toEqual Point.INFINITY - - expect(Point.ZERO.translate([5, 6])).toEqual [5, 6] - expect(Point(1, 1).translate([3, 4])).toEqual [4, 5] - - describe "::traverse(delta)", -> - it "returns a new point by traversing given rows and columns", -> - expect(Point(2, 3).traverse(Point(0, 3))).toEqual Point(2, 6) - expect(Point(2, 3).traverse([0, 3])).toEqual [2, 6] - - expect(Point(1, 3).traverse(Point(4, 2))).toEqual [5, 2] - expect(Point(1, 3).traverse([5, 4])).toEqual [6, 4] - - describe "::traversalFrom(other)", -> - it "returns a point that other has to traverse to get to given point", -> - expect(Point(2, 5).traversalFrom(Point(2, 3))).toEqual Point(0, 2) - expect(Point(2, 3).traversalFrom(Point(2, 5))).toEqual Point(0, -2) - expect(Point(2, 3).traversalFrom(Point(2, 3))).toEqual Point(0, 0) - - expect(Point(3, 4).traversalFrom(Point(2, 3))).toEqual Point(1, 4) - expect(Point(2, 3).traversalFrom(Point(3, 5))).toEqual Point(-1, 3) - - expect(Point(2, 5).traversalFrom([2, 3])).toEqual [0, 2] - expect(Point(2, 3).traversalFrom([2, 5])).toEqual [0, -2] - expect(Point(2, 3).traversalFrom([2, 3])).toEqual [0, 0] - - expect(Point(3, 4).traversalFrom([2, 3])).toEqual [1, 4] - expect(Point(2, 3).traversalFrom([3, 5])).toEqual [-1, 3] - - describe "::toArray()", -> - it "returns an array of row and column", -> - expect(Point(1, 3).toArray()).toEqual [1, 3] - expect(Point.ZERO.toArray()).toEqual [0, 0] - expect(Point.INFINITY.toArray()).toEqual [Infinity, Infinity] - - describe "::serialize()", -> - it "returns an array of row and column", -> - expect(Point(1, 3).serialize()).toEqual [1, 3] - expect(Point.ZERO.serialize()).toEqual [0, 0] - expect(Point.INFINITY.serialize()).toEqual [Infinity, Infinity] - - describe "::toString()", -> - it "returns string representation of Point", -> - expect(Point(4, 5).toString()).toBe "(4, 5)" - expect(Point.ZERO.toString()).toBe "(0, 0)" - expect(Point.INFINITY.toString()).toBe "(Infinity, Infinity)" diff --git a/spec/point-spec.js b/spec/point-spec.js new file mode 100644 index 0000000000..8b39e7df4d --- /dev/null +++ b/spec/point-spec.js @@ -0,0 +1,199 @@ +const Point = require('../src/point'); + +describe("Point", function() { + beforeEach(() => jasmine.addCustomEqualityTester(require("underscore-plus").isEqual)); + + describe("::negate()", () => it("should negate the row and column", function() { + expect(new Point( 0, 0).negate().toString()).toBe("(0, 0)"); + expect(new Point( 1, 2).negate().toString()).toBe("(-1, -2)"); + expect(new Point(-1, -2).negate().toString()).toBe("(1, 2)"); + return expect(new Point(-1, 2).negate().toString()).toBe("(1, -2)"); + })); + + describe("::fromObject(object, copy)", function() { + it("returns a new Point if object is point-compatible array ", function() { + expect(Point.fromObject([1, 3])).toEqual(Point(1, 3)); + return expect(Point.fromObject([Infinity, Infinity])).toEqual(Point.INFINITY); + }); + + return it("returns the copy of object if it is an instanceof Point", function() { + const origin = Point(0, 0); + expect(Point.fromObject(origin, false) === origin).toBe(true); + return expect(Point.fromObject(origin, true) === origin).toBe(false); + }); + }); + + describe("::copy()", () => it("returns a copy of the object", function() { + expect(Point(3, 4).copy()).toEqual(Point(3, 4)); + return expect(Point.ZERO.copy()).toEqual([0, 0]); +})); + + describe("::negate()", () => it("returns a new point with row and column negated", function() { + expect(Point(3, 4).negate()).toEqual(Point(-3, -4)); + return expect(Point.ZERO.negate()).toEqual([0, 0]); +})); + + describe("::freeze()", () => it("makes the Point object immutable", function() { + expect(Object.isFrozen(Point(3, 4).freeze())).toBe(true); + return expect(Object.isFrozen(Point.ZERO.freeze())).toBe(true); + })); + + describe("::compare(other)", () => it("returns -1 for <, 0 for =, 1 for > comparisions", function() { + expect(Point(2, 3).compare(Point(2, 6))).toBe(-1); + expect(Point(2, 3).compare(Point(3, 4))).toBe(-1); + expect(Point(1, 1).compare(Point(1, 1))).toBe(0); + expect(Point(2, 3).compare(Point(2, 0))).toBe(1); + expect(Point(2, 3).compare(Point(1, 3))).toBe(1); + + expect(Point(2, 3).compare([2, 6])).toBe(-1); + expect(Point(2, 3).compare([3, 4])).toBe(-1); + expect(Point(1, 1).compare([1, 1])).toBe(0); + expect(Point(2, 3).compare([2, 0])).toBe(1); + return expect(Point(2, 3).compare([1, 3])).toBe(1); + })); + + describe("::isLessThan(other)", () => it("returns a boolean indicating whether a point precedes the given Point ", function() { + expect(Point(2, 3).isLessThan(Point(2, 5))).toBe(true); + expect(Point(2, 3).isLessThan(Point(3, 4))).toBe(true); + expect(Point(2, 3).isLessThan(Point(2, 3))).toBe(false); + expect(Point(2, 3).isLessThan(Point(2, 1))).toBe(false); + expect(Point(2, 3).isLessThan(Point(1, 2))).toBe(false); + + expect(Point(2, 3).isLessThan([2, 5])).toBe(true); + expect(Point(2, 3).isLessThan([3, 4])).toBe(true); + expect(Point(2, 3).isLessThan([2, 3])).toBe(false); + expect(Point(2, 3).isLessThan([2, 1])).toBe(false); + return expect(Point(2, 3).isLessThan([1, 2])).toBe(false); + })); + + describe("::isLessThanOrEqual(other)", () => it("returns a boolean indicating whether a point precedes or equal the given Point ", function() { + expect(Point(2, 3).isLessThanOrEqual(Point(2, 5))).toBe(true); + expect(Point(2, 3).isLessThanOrEqual(Point(3, 4))).toBe(true); + expect(Point(2, 3).isLessThanOrEqual(Point(2, 3))).toBe(true); + expect(Point(2, 3).isLessThanOrEqual(Point(2, 1))).toBe(false); + expect(Point(2, 3).isLessThanOrEqual(Point(1, 2))).toBe(false); + + expect(Point(2, 3).isLessThanOrEqual([2, 5])).toBe(true); + expect(Point(2, 3).isLessThanOrEqual([3, 4])).toBe(true); + expect(Point(2, 3).isLessThanOrEqual([2, 3])).toBe(true); + expect(Point(2, 3).isLessThanOrEqual([2, 1])).toBe(false); + return expect(Point(2, 3).isLessThanOrEqual([1, 2])).toBe(false); + })); + + describe("::isGreaterThan(other)", () => it("returns a boolean indicating whether a point follows the given Point ", function() { + expect(Point(2, 3).isGreaterThan(Point(2, 5))).toBe(false); + expect(Point(2, 3).isGreaterThan(Point(3, 4))).toBe(false); + expect(Point(2, 3).isGreaterThan(Point(2, 3))).toBe(false); + expect(Point(2, 3).isGreaterThan(Point(2, 1))).toBe(true); + expect(Point(2, 3).isGreaterThan(Point(1, 2))).toBe(true); + + expect(Point(2, 3).isGreaterThan([2, 5])).toBe(false); + expect(Point(2, 3).isGreaterThan([3, 4])).toBe(false); + expect(Point(2, 3).isGreaterThan([2, 3])).toBe(false); + expect(Point(2, 3).isGreaterThan([2, 1])).toBe(true); + return expect(Point(2, 3).isGreaterThan([1, 2])).toBe(true); + })); + + describe("::isGreaterThanOrEqual(other)", () => it("returns a boolean indicating whether a point follows or equal the given Point ", function() { + expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 5))).toBe(false); + expect(Point(2, 3).isGreaterThanOrEqual(Point(3, 4))).toBe(false); + expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 3))).toBe(true); + expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 1))).toBe(true); + expect(Point(2, 3).isGreaterThanOrEqual(Point(1, 2))).toBe(true); + + expect(Point(2, 3).isGreaterThanOrEqual([2, 5])).toBe(false); + expect(Point(2, 3).isGreaterThanOrEqual([3, 4])).toBe(false); + expect(Point(2, 3).isGreaterThanOrEqual([2, 3])).toBe(true); + expect(Point(2, 3).isGreaterThanOrEqual([2, 1])).toBe(true); + return expect(Point(2, 3).isGreaterThanOrEqual([1, 2])).toBe(true); + })); + + describe("::isEqual()", () => it("returns if whether two points are equal", function() { + expect(Point(1, 1).isEqual(Point(1, 1))).toBe(true); + expect(Point(1, 1).isEqual([1, 1])).toBe(true); + expect(Point(1, 2).isEqual(Point(3, 3))).toBe(false); + return expect(Point(1, 2).isEqual([3, 3])).toBe(false); + })); + + describe("::isPositive()", () => it("returns true if the point represents a forward traversal", function() { + expect(Point(-1, -1).isPositive()).toBe(false); + expect(Point(-1, 0).isPositive()).toBe(false); + expect(Point(-1, Infinity).isPositive()).toBe(false); + expect(Point(0, 0).isPositive()).toBe(false); + + expect(Point(0, 1).isPositive()).toBe(true); + expect(Point(5, 0).isPositive()).toBe(true); + return expect(Point(5, -1).isPositive()).toBe(true); + })); + + describe("::isZero()", () => it("returns true if the point is zero", function() { + expect(Point(1, 1).isZero()).toBe(false); + expect(Point(0, 1).isZero()).toBe(false); + expect(Point(1, 0).isZero()).toBe(false); + return expect(Point(0, 0).isZero()).toBe(true); + })); + + describe("::min(a, b)", () => it("returns the minimum of two points", function() { + expect(Point.min(Point(3, 4), Point(1, 1))).toEqual(Point(1, 1)); + expect(Point.min(Point(1, 2), Point(5, 6))).toEqual(Point(1, 2)); + expect(Point.min([3, 4], [1, 1])).toEqual([1, 1]); + return expect(Point.min([1, 2], [5, 6])).toEqual([1, 2]); +})); + + describe("::max(a, b)", () => it("returns the minimum of two points", function() { + expect(Point.max(Point(3, 4), Point(1, 1))).toEqual(Point(3, 4)); + expect(Point.max(Point(1, 2), Point(5, 6))).toEqual(Point(5, 6)); + expect(Point.max([3, 4], [1, 1])).toEqual([3, 4]); + return expect(Point.max([1, 2], [5, 6])).toEqual([5, 6]); +})); + + describe("::translate(delta)", () => it("returns a new point by adding corresponding coordinates", function() { + expect(Point(1, 1).translate(Point(2, 3))).toEqual(Point(3, 4)); + expect(Point.INFINITY.translate(Point(2, 3))).toEqual(Point.INFINITY); + + expect(Point.ZERO.translate([5, 6])).toEqual([5, 6]); + return expect(Point(1, 1).translate([3, 4])).toEqual([4, 5]); +})); + + describe("::traverse(delta)", () => it("returns a new point by traversing given rows and columns", function() { + expect(Point(2, 3).traverse(Point(0, 3))).toEqual(Point(2, 6)); + expect(Point(2, 3).traverse([0, 3])).toEqual([2, 6]); + + expect(Point(1, 3).traverse(Point(4, 2))).toEqual([5, 2]); + return expect(Point(1, 3).traverse([5, 4])).toEqual([6, 4]); +})); + + describe("::traversalFrom(other)", () => it("returns a point that other has to traverse to get to given point", function() { + expect(Point(2, 5).traversalFrom(Point(2, 3))).toEqual(Point(0, 2)); + expect(Point(2, 3).traversalFrom(Point(2, 5))).toEqual(Point(0, -2)); + expect(Point(2, 3).traversalFrom(Point(2, 3))).toEqual(Point(0, 0)); + + expect(Point(3, 4).traversalFrom(Point(2, 3))).toEqual(Point(1, 4)); + expect(Point(2, 3).traversalFrom(Point(3, 5))).toEqual(Point(-1, 3)); + + expect(Point(2, 5).traversalFrom([2, 3])).toEqual([0, 2]); + expect(Point(2, 3).traversalFrom([2, 5])).toEqual([0, -2]); + expect(Point(2, 3).traversalFrom([2, 3])).toEqual([0, 0]); + + expect(Point(3, 4).traversalFrom([2, 3])).toEqual([1, 4]); + return expect(Point(2, 3).traversalFrom([3, 5])).toEqual([-1, 3]); +})); + + describe("::toArray()", () => it("returns an array of row and column", function() { + expect(Point(1, 3).toArray()).toEqual([1, 3]); + expect(Point.ZERO.toArray()).toEqual([0, 0]); + return expect(Point.INFINITY.toArray()).toEqual([Infinity, Infinity]); +})); + + describe("::serialize()", () => it("returns an array of row and column", function() { + expect(Point(1, 3).serialize()).toEqual([1, 3]); + expect(Point.ZERO.serialize()).toEqual([0, 0]); + return expect(Point.INFINITY.serialize()).toEqual([Infinity, Infinity]); +})); + + return describe("::toString()", () => it("returns string representation of Point", function() { + expect(Point(4, 5).toString()).toBe("(4, 5)"); + expect(Point.ZERO.toString()).toBe("(0, 0)"); + return expect(Point.INFINITY.toString()).toBe("(Infinity, Infinity)"); + })); +}); diff --git a/spec/range-spec.coffee b/spec/range-spec.coffee deleted file mode 100644 index ea4caf07a3..0000000000 --- a/spec/range-spec.coffee +++ /dev/null @@ -1,42 +0,0 @@ -Range = require '../src/range' - -describe "Range", -> - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - - describe "::intersectsWith(other, [exclusive])", -> - intersectsWith = (range1, range2, exclusive) -> - range1 = Range.fromObject(range1) - range2 = Range.fromObject(range2) - range1.intersectsWith(range2, exclusive) - - describe "when the exclusive argument is false (the default)", -> - it "returns true if the ranges intersect, exclusive of their endpoints", -> - expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]])).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]])).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe false - - describe "when the exclusive argument is true", -> - it "returns true if the ranges intersect, exclusive of their endpoints", -> - expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]], true)).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]], true)).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]], true)).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe false - - describe "::negate()", -> - it "should negate the start and end points", -> - expect(new Range([ 0, 0], [ 0, 0]).negate().toString()).toBe "[(0, 0) - (0, 0)]" - expect(new Range([ 1, 2], [ 3, 4]).negate().toString()).toBe "[(-3, -4) - (-1, -2)]" - expect(new Range([-1, -2], [-3, -4]).negate().toString()).toBe "[(1, 2) - (3, 4)]" - expect(new Range([-1, 2], [ 3, -4]).negate().toString()).toBe "[(-3, 4) - (1, -2)]" diff --git a/spec/range-spec.js b/spec/range-spec.js new file mode 100644 index 0000000000..109851f282 --- /dev/null +++ b/spec/range-spec.js @@ -0,0 +1,44 @@ +const Range = require('../src/range'); + +describe("Range", function() { + beforeEach(() => jasmine.addCustomEqualityTester(require("underscore-plus").isEqual)); + + describe("::intersectsWith(other, [exclusive])", function() { + const intersectsWith = function(range1, range2, exclusive) { + range1 = Range.fromObject(range1); + range2 = Range.fromObject(range2); + return range1.intersectsWith(range2, exclusive); + }; + + describe("when the exclusive argument is false (the default)", () => it("returns true if the ranges intersect, exclusive of their endpoints", function() { + expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]])).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]])).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe(false); + return expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe(false); + })); + + return describe("when the exclusive argument is true", () => it("returns true if the ranges intersect, exclusive of their endpoints", function() { + expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]], true)).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]], true)).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]], true)).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe(false); + return expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe(false); + })); + }); + + return describe("::negate()", () => it("should negate the start and end points", function() { + expect(new Range([ 0, 0], [ 0, 0]).negate().toString()).toBe("[(0, 0) - (0, 0)]"); + expect(new Range([ 1, 2], [ 3, 4]).negate().toString()).toBe("[(-3, -4) - (-1, -2)]"); + expect(new Range([-1, -2], [-3, -4]).negate().toString()).toBe("[(1, 2) - (3, 4)]"); + return expect(new Range([-1, 2], [ 3, -4]).negate().toString()).toBe("[(-3, 4) - (1, -2)]"); + })); +}); diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee deleted file mode 100644 index d87c2b3134..0000000000 --- a/spec/text-buffer-spec.coffee +++ /dev/null @@ -1,2994 +0,0 @@ -fs = require 'fs-plus' -{join} = require 'path' -temp = require 'temp' -{File} = require 'pathwatcher' -Random = require 'random-seed' -Point = require '../src/point' -Range = require '../src/range' -DisplayLayer = require '../src/display-layer' -DefaultHistoryProvider = require '../src/default-history-provider' -TextBuffer = require '../src/text-buffer' -SampleText = fs.readFileSync(join(__dirname, 'fixtures', 'sample.js'), 'utf8') -{buildRandomLines, getRandomBufferRange} = require './helpers/random' -NullLanguageMode = require '../src/null-language-mode' - -describe "TextBuffer", -> - buffer = null - - beforeEach -> - temp.track() - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - # When running specs in Atom, setTimeout is spied on by default. - jasmine.useRealClock?() - - afterEach -> - buffer?.destroy() - buffer = null - - describe "construction", -> - it "can be constructed empty", -> - buffer = new TextBuffer - expect(buffer.getLineCount()).toBe 1 - expect(buffer.getText()).toBe '' - expect(buffer.lineForRow(0)).toBe '' - expect(buffer.lineEndingForRow(0)).toBe '' - - it "can be constructed with initial text containing no trailing newline", -> - text = "hello\nworld\r\nhow are you doing?\r\nlast" - buffer = new TextBuffer(text) - expect(buffer.getLineCount()).toBe 4 - expect(buffer.getText()).toBe text - expect(buffer.lineForRow(0)).toBe 'hello' - expect(buffer.lineEndingForRow(0)).toBe '\n' - expect(buffer.lineForRow(1)).toBe 'world' - expect(buffer.lineEndingForRow(1)).toBe '\r\n' - expect(buffer.lineForRow(2)).toBe 'how are you doing?' - expect(buffer.lineEndingForRow(2)).toBe '\r\n' - expect(buffer.lineForRow(3)).toBe 'last' - expect(buffer.lineEndingForRow(3)).toBe '' - - it "can be constructed with initial text containing a trailing newline", -> - text = "first\n" - buffer = new TextBuffer(text) - expect(buffer.getLineCount()).toBe 2 - expect(buffer.getText()).toBe text - expect(buffer.lineForRow(0)).toBe 'first' - expect(buffer.lineEndingForRow(0)).toBe '\n' - expect(buffer.lineForRow(1)).toBe '' - expect(buffer.lineEndingForRow(1)).toBe '' - - it "automatically assigns a unique identifier to new buffers", -> - bufferIds = [0..16].map(-> new TextBuffer().getId()) - uniqueBufferIds = new Set(bufferIds) - - expect(uniqueBufferIds.size).toBe(bufferIds.length) - - describe "::destroy()", -> - it "clears the buffer's state", (done) -> - filePath = temp.openSync('atom').path - buffer = new TextBuffer() - buffer.setPath(filePath) - buffer.append("a") - buffer.append("b") - buffer.destroy() - - expect(buffer.getText()).toBe('') - buffer.undo() - expect(buffer.getText()).toBe('') - buffer.save().catch (error) -> - expect(error.message).toMatch(/Can't save destroyed buffer/) - done() - - describe "::setTextInRange(range, text)", -> - beforeEach -> - buffer = new TextBuffer("hello\nworld\r\nhow are you doing?") - - it "can replace text on a single line with a standard newline", -> - buffer.setTextInRange([[0, 2], [0, 4]], "y y") - expect(buffer.getText()).toEqual "hey yo\nworld\r\nhow are you doing?" - - it "can replace text on a single line with a carriage-return/newline", -> - buffer.setTextInRange([[1, 3], [1, 5]], "ms") - expect(buffer.getText()).toEqual "hello\nworms\r\nhow are you doing?" - - it "can replace text in a region spanning multiple lines, ending on the last line", -> - buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", normalizeLineEndings: false) - expect(buffer.getText()).toEqual "hey there\r\ncat\nwhat are you doing?" - - it "can replace text in a region spanning multiple lines, ending with a carriage-return/newline", -> - buffer.setTextInRange([[0, 2], [1, 3]], "y\nyou're o", normalizeLineEndings: false) - expect(buffer.getText()).toEqual "hey\nyou're old\r\nhow are you doing?" - - describe "after a change", -> - it "notifies, in order: the language mode, display layers, and display layer ::onDidChange observers with the relevant details", -> - buffer = new TextBuffer("hello\nworld\r\nhow are you doing?") - - events = [] - languageMode = { - bufferDidChange: (e) -> events.push({source: 'language-mode', event: e}), - bufferDidFinishTransaction: ->, - onDidChangeHighlighting: -> {dispose: ->} - } - displayLayer1 = buffer.addDisplayLayer() - displayLayer2 = buffer.addDisplayLayer() - spyOn(displayLayer1, 'bufferDidChange').and.callFake (e) -> - events.push({source: 'display-layer-1', event: e}) - DisplayLayer.prototype.bufferDidChange.call(displayLayer1, e) - spyOn(displayLayer2, 'bufferDidChange').and.callFake (e) -> - events.push({source: 'display-layer-2', event: e}) - DisplayLayer.prototype.bufferDidChange.call(displayLayer2, e) - buffer.setLanguageMode(languageMode) - buffer.onDidChange (e) -> events.push({source: 'buffer', event: JSON.parse(JSON.stringify(e))}) - displayLayer1.onDidChange (e) -> events.push({source: 'display-layer-event', event: e}) - - buffer.transact -> - buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", normalizeLineEndings: false) - buffer.setTextInRange([[1, 1], [1, 2]], "abc", normalizeLineEndings: false) - - changeEvent1 = { - oldRange: [[0, 2], [2, 3]], newRange: [[0, 2], [2, 4]] - oldText: "llo\nworld\r\nhow", newText: "y there\r\ncat\nwhat", - } - changeEvent2 = { - oldRange: [[1, 1], [1, 2]], newRange: [[1, 1], [1, 4]] - oldText: "a", newText: "abc", - } - expect(events).toEqual [ - {source: 'language-mode', event: changeEvent1}, - {source: 'display-layer-1', event: changeEvent1}, - {source: 'display-layer-2', event: changeEvent1}, - - {source: 'language-mode', event: changeEvent2}, - {source: 'display-layer-1', event: changeEvent2}, - {source: 'display-layer-2', event: changeEvent2}, - - { - source: 'buffer', - event: { - oldRange: Range(Point(0, 2), Point(2, 3)), - newRange: Range(Point(0, 2), Point(2, 4)), - changes: [ - { - oldRange: Range(Point(0, 2), Point(2, 3)), - newRange: Range(Point(0, 2), Point(2, 4)), - oldText: "llo\nworld\r\nhow", - newText: "y there\r\ncabct\nwhat" - } - ] - } - }, - { - source: 'display-layer-event', - event: [{ - oldRange: Range(Point(0, 0), Point(3, 0)), - newRange: Range(Point(0, 0), Point(3, 0)) - }] - } - ] - - it "returns the newRange of the change", -> - expect(buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat"), normalizeLineEndings: false).toEqual [[0, 2], [2, 4]] - - it "clips the given range", -> - buffer.setTextInRange([[-1, -1], [0, 1]], "y") - buffer.setTextInRange([[0, 10], [0, 100]], "w") - expect(buffer.lineForRow(0)).toBe "yellow" - - it "preserves the line endings of existing lines", -> - buffer.setTextInRange([[0, 1], [0, 2]], 'o') - expect(buffer.lineEndingForRow(0)).toBe '\n' - buffer.setTextInRange([[1, 1], [1, 3]], 'i') - expect(buffer.lineEndingForRow(1)).toBe '\r\n' - - it "freezes change event ranges", -> - changedOldRange = null - changedNewRange = null - buffer.onDidChange ({oldRange, newRange}) -> - oldRange.start = Point(0, 3) - oldRange.start.row = 1 - newRange.start = Point(4, 4) - newRange.end.row = 2 - changedOldRange = oldRange - changedNewRange = newRange - - buffer.setTextInRange(Range(Point(0, 2), Point(0, 4)), "y y") - - expect(changedOldRange).toEqual([[0, 2], [0, 4]]) - expect(changedNewRange).toEqual([[0, 2], [0, 5]]) - - describe "when the undo option is 'skip'", -> - it "replaces the contents of the buffer with the given text", -> - buffer.setTextInRange([[0, 0], [0, 1]], "y") - buffer.setTextInRange([[0, 10], [0, 100]], "w", {undo: 'skip'}) - expect(buffer.lineForRow(0)).toBe "yellow" - - expect(buffer.undo()).toBe true - expect(buffer.lineForRow(0)).toBe "hello" - - it "still emits marker change events (regression)", -> - markerLayer = buffer.addMarkerLayer() - marker = markerLayer.markRange([[0, 0], [0, 3]]) - - markerLayerUpdateEventsCount = 0 - markerChangeEvents = [] - markerLayer.onDidUpdate -> markerLayerUpdateEventsCount++ - marker.onDidChange (event) -> markerChangeEvents.push(event) - - buffer.setTextInRange([[0, 0], [0, 1]], '', {undo: 'skip'}) - expect(markerLayerUpdateEventsCount).toBe(1) - expect(markerChangeEvents).toEqual([{ - wasValid: true, isValid: true, - hadTail: true, hasTail: true, - oldProperties: {}, newProperties: {}, - oldHeadPosition: Point(0, 3), newHeadPosition: Point(0, 2), - oldTailPosition: Point(0, 0), newTailPosition: Point(0, 0), - textChanged: true - }]) - markerChangeEvents.length = 0 - - buffer.transact -> - buffer.setTextInRange([[0, 0], [0, 1]], '', {undo: 'skip'}) - expect(markerLayerUpdateEventsCount).toBe(2) - expect(markerChangeEvents).toEqual([{ - wasValid: true, isValid: true, - hadTail: true, hasTail: true, - oldProperties: {}, newProperties: {}, - oldHeadPosition: Point(0, 2), newHeadPosition: Point(0, 1), - oldTailPosition: Point(0, 0), newTailPosition: Point(0, 0), - textChanged: true - }]) - - it "still emits text change events (regression)", (done) -> - didChangeEvents = [] - buffer.onDidChange (event) -> didChangeEvents.push(event) - - buffer.onDidStopChanging ({changes}) -> - assertChangesEqual(changes, [{ - oldRange: [[0, 0], [0, 1]], - newRange: [[0, 0], [0, 1]], - oldText: 'h', - newText: 'z' - }]) - done() - - buffer.setTextInRange([[0, 0], [0, 1]], 'y', {undo: 'skip'}) - expect(didChangeEvents.length).toBe(1) - assertChangesEqual(didChangeEvents[0].changes, [{ - oldRange: [[0, 0], [0, 1]], - newRange: [[0, 0], [0, 1]], - oldText: 'h', - newText: 'y' - }]) - - buffer.transact -> buffer.setTextInRange([[0, 0], [0, 1]], 'z', {undo: 'skip'}) - expect(didChangeEvents.length).toBe(2) - assertChangesEqual(didChangeEvents[1].changes, [{ - oldRange: [[0, 0], [0, 1]], - newRange: [[0, 0], [0, 1]], - oldText: 'y', - newText: 'z' - }]) - - describe "when the normalizeLineEndings argument is true (the default)", -> - describe "when the range's start row has a line ending", -> - it "normalizes inserted line endings to match the line ending of the range's start row", -> - changeEvents = [] - buffer.onDidChange (e) -> changeEvents.push(e) - - expect(buffer.lineEndingForRow(0)).toBe '\n' - buffer.setTextInRange([[0, 2], [0, 5]], "y\r\nthere\r\ncrazy") - expect(buffer.lineEndingForRow(0)).toBe '\n' - expect(buffer.lineEndingForRow(1)).toBe '\n' - expect(buffer.lineEndingForRow(2)).toBe '\n' - expect(changeEvents[0].newText).toBe "y\nthere\ncrazy" - - expect(buffer.lineEndingForRow(3)).toBe '\r\n' - buffer.setTextInRange([[3, 3], [4, Infinity]], "ms\ndo you\r\nlike\ndirt") - expect(buffer.lineEndingForRow(3)).toBe '\r\n' - expect(buffer.lineEndingForRow(4)).toBe '\r\n' - expect(buffer.lineEndingForRow(5)).toBe '\r\n' - expect(buffer.lineEndingForRow(6)).toBe '' - expect(changeEvents[1].newText).toBe "ms\r\ndo you\r\nlike\r\ndirt" - - buffer.setTextInRange([[5, 1], [5, 3]], '\r') - expect(changeEvents[2].changes).toEqual([{ - oldRange: [[5, 1], [5, 3]], - newRange: [[5, 1], [6, 0]], - oldText: 'ik', - newText: '\r\n' - }]) - - buffer.undo() - expect(changeEvents[3].changes).toEqual([{ - oldRange: [[5, 1], [6, 0]], - newRange: [[5, 1], [5, 3]], - oldText: '\r\n', - newText: 'ik' - }]) - - buffer.redo() - expect(changeEvents[4].changes).toEqual([{ - oldRange: [[5, 1], [5, 3]], - newRange: [[5, 1], [6, 0]], - oldText: 'ik', - newText: '\r\n' - }]) - - describe "when the range's start row has no line ending (because it's the last line of the buffer)", -> - describe "when the buffer contains no newlines", -> - it "honors the newlines in the inserted text", -> - buffer = new TextBuffer("hello") - buffer.setTextInRange([[0, 2], [0, Infinity]], "hey\r\nthere\nworld") - expect(buffer.lineEndingForRow(0)).toBe '\r\n' - expect(buffer.lineEndingForRow(1)).toBe '\n' - expect(buffer.lineEndingForRow(2)).toBe '' - - describe "when the buffer contains newlines", -> - it "normalizes inserted line endings to match the line ending of the penultimate row", -> - expect(buffer.lineEndingForRow(1)).toBe '\r\n' - buffer.setTextInRange([[2, 0], [2, Infinity]], "what\ndo\r\nyou\nwant?") - expect(buffer.lineEndingForRow(2)).toBe '\r\n' - expect(buffer.lineEndingForRow(3)).toBe '\r\n' - expect(buffer.lineEndingForRow(4)).toBe '\r\n' - expect(buffer.lineEndingForRow(5)).toBe '' - - describe "when the normalizeLineEndings argument is false", -> - it "honors the newlines in the inserted text", -> - buffer.setTextInRange([[1, 0], [1, 5]], "moon\norbiting\r\nhappily\nthere", {normalizeLineEndings: false}) - expect(buffer.lineEndingForRow(1)).toBe '\n' - expect(buffer.lineEndingForRow(2)).toBe '\r\n' - expect(buffer.lineEndingForRow(3)).toBe '\n' - expect(buffer.lineEndingForRow(4)).toBe '\r\n' - expect(buffer.lineEndingForRow(5)).toBe '' - - describe "::setText(text)", -> - it "replaces the contents of the buffer with the given text", -> - buffer = new TextBuffer("hello\nworld\r\nyou are cool") - buffer.setText("goodnight\r\nmoon\nit's been good") - expect(buffer.getText()).toBe "goodnight\r\nmoon\nit's been good" - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nyou are cool" - - describe "::insert(position, text, normalizeNewlinesn)", -> - it "inserts text at the given position", -> - buffer = new TextBuffer("hello world") - buffer.insert([0, 5], " there") - expect(buffer.getText()).toBe "hello there world" - - it "honors the normalizeNewlines option", -> - buffer = new TextBuffer("hello\nworld") - buffer.insert([0, 5], "\r\nthere\r\nlittle", normalizeLineEndings: false) - expect(buffer.getText()).toBe "hello\r\nthere\r\nlittle\nworld" - - describe "::append(text, normalizeNewlines)", -> - it "appends text to the end of the buffer", -> - buffer = new TextBuffer("hello world") - buffer.append(", how are you?") - expect(buffer.getText()).toBe "hello world, how are you?" - - it "honors the normalizeNewlines option", -> - buffer = new TextBuffer("hello\nworld") - buffer.append("\r\nhow\r\nare\nyou?", normalizeLineEndings: false) - expect(buffer.getText()).toBe "hello\nworld\r\nhow\r\nare\nyou?" - - describe "::delete(range)", -> - it "deletes text in the given range", -> - buffer = new TextBuffer("hello world") - buffer.delete([[0, 5], [0, 11]]) - expect(buffer.getText()).toBe "hello" - - describe "::deleteRows(startRow, endRow)", -> - beforeEach -> - buffer = new TextBuffer("first\nsecond\nthird\nlast") - - describe "when the endRow is less than the last row of the buffer", -> - it "deletes the specified rows", -> - buffer.deleteRows(1, 2) - expect(buffer.getText()).toBe "first\nlast" - buffer.deleteRows(0, 0) - expect(buffer.getText()).toBe "last" - - describe "when the endRow is the last row of the buffer", -> - it "deletes the specified rows", -> - buffer.deleteRows(2, 3) - expect(buffer.getText()).toBe "first\nsecond" - buffer.deleteRows(0, 1) - expect(buffer.getText()).toBe "" - - it "clips the given row range", -> - buffer.deleteRows(-1, 0) - expect(buffer.getText()).toBe "second\nthird\nlast" - buffer.deleteRows(1, 5) - expect(buffer.getText()).toBe "second" - - buffer.deleteRows(-2, -1) - expect(buffer.getText()).toBe "second" - buffer.deleteRows(1, 2) - expect(buffer.getText()).toBe "second" - - it "handles out of order row ranges", -> - buffer.deleteRows(2, 1) - expect(buffer.getText()).toBe "first\nlast" - - describe "::getText()", -> - it "returns the contents of the buffer as a single string", -> - buffer = new TextBuffer("hello\nworld\r\nhow are you?") - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you?" - buffer.setTextInRange([[1, 0], [1, 5]], "mom") - expect(buffer.getText()).toBe "hello\nmom\r\nhow are you?" - - describe "::undo() and ::redo()", -> - beforeEach -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - - it "undoes and redoes multiple changes", -> - buffer.setTextInRange([[0, 5], [0, 5]], " there") - buffer.setTextInRange([[1, 0], [1, 5]], "friend") - expect(buffer.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello there\nworld\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello there\nworld\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - buffer.redo() - expect(buffer.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - - it "clears the redo stack upon a fresh change", -> - buffer.setTextInRange([[0, 5], [0, 5]], " there") - buffer.setTextInRange([[1, 0], [1, 5]], "friend") - expect(buffer.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello there\nworld\r\nhow are you doing?" - - buffer.setTextInRange([[1, 3], [1, 5]], "m") - expect(buffer.getText()).toBe "hello there\nworm\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello there\nworm\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello there\nworld\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "does not allow the undo stack to grow without bound", -> - buffer = new TextBuffer(maxUndoEntries: 12) - - # Each transaction is treated as a single undo entry. We can undo up - # to 12 of them. - buffer.setText("") - buffer.clearUndoStack() - for i in [0...13] - buffer.transact -> - buffer.append(String(i)) - buffer.append("\n") - expect(buffer.getLineCount()).toBe 14 - - undoCount = 0 - undoCount++ while buffer.undo() - expect(undoCount).toBe 12 - expect(buffer.getText()).toBe '0\n' - - describe "::createMarkerSnapshot", -> - markerLayers = null - - beforeEach -> - buffer = new TextBuffer - - markerLayers = [ - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - buffer.addMarkerLayer(maintainHistory: true) - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - buffer.addMarkerLayer(maintainHistory: true) - ] - - describe "when selectionsMarkerLayer is not passed", -> - it "takes a snapshot of all markerLayers", -> - snapshot = buffer.createMarkerSnapshot() - markerLayerIdsInSnapshot = Object.keys(snapshot) - expect(markerLayerIdsInSnapshot.length).toBe(4) - expect(markerLayers[0].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[1].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[2].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[3].id in markerLayerIdsInSnapshot).toBe(true) - - describe "when selectionsMarkerLayer is passed", -> - it "skips snapshotting of other 'selection' role marker layers", -> - snapshot = buffer.createMarkerSnapshot(markerLayers[0]) - markerLayerIdsInSnapshot = Object.keys(snapshot) - expect(markerLayerIdsInSnapshot.length).toBe(3) - expect(markerLayers[0].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[1].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[2].id in markerLayerIdsInSnapshot).toBe(false) - expect(markerLayers[3].id in markerLayerIdsInSnapshot).toBe(true) - - snapshot = buffer.createMarkerSnapshot(markerLayers[2]) - markerLayerIdsInSnapshot = Object.keys(snapshot) - expect(markerLayerIdsInSnapshot.length).toBe(3) - expect(markerLayers[0].id in markerLayerIdsInSnapshot).toBe(false) - expect(markerLayers[1].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[2].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[3].id in markerLayerIdsInSnapshot).toBe(true) - - describe "selective snapshotting and restoration on transact/undo/redo for selections marker layer", -> - [markerLayers, marker0, marker1, marker2, textUndo, textRedo, rangesBefore, rangesAfter] = [] - ensureMarkerLayer = (markerLayer, range) -> - markers = markerLayer.findMarkers({}) - expect(markers.length).toBe(1) - expect(markers[0].getRange()).toEqual(range) - - getFirstMarker = (markerLayer) -> - markerLayer.findMarkers({})[0] - - beforeEach -> - buffer = new TextBuffer(text: "00000000\n11111111\n22222222\n33333333\n") - - markerLayers = [ - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - ] - - textUndo = "00000000\n11111111\n22222222\n33333333\n" - textRedo = "00000000\n11111111\n22222222\n33333333\n44444444\n" - - rangesBefore = [ - [[0, 1], [0, 1]] - [[0, 2], [0, 2]] - [[0, 3], [0, 3]] - ] - rangesAfter = [ - [[2, 1], [2, 1]] - [[2, 2], [2, 2]] - [[2, 3], [2, 3]] - ] - - marker0 = markerLayers[0].markRange(rangesBefore[0]) - marker1 = markerLayers[1].markRange(rangesBefore[1]) - marker2 = markerLayers[2].markRange(rangesBefore[2]) - - it "restores a snapshot from other selections marker layers on undo/redo", -> - # Snapshot is taken for markerLayers[0] only, markerLayer[1] and markerLayer[2] are skipped - buffer.transact {selectionsMarkerLayer: markerLayers[0]}, -> - buffer.append("44444444\n") - marker0.setRange(rangesAfter[0]) - marker1.setRange(rangesAfter[1]) - marker2.setRange(rangesAfter[2]) - - buffer.undo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesBefore[0]) - ensureMarkerLayer(markerLayers[1], rangesAfter[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - - buffer.redo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textRedo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesAfter[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - - buffer.undo({selectionsMarkerLayer: markerLayers[1]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesBefore[0]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).not.toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - expect(marker1.isDestroyed()).toBe(true) - - buffer.redo({selectionsMarkerLayer: markerLayers[2]}) - expect(buffer.getText()).toBe(textRedo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesBefore[0]) - ensureMarkerLayer(markerLayers[2], rangesAfter[0]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).not.toBe(marker1) - expect(getFirstMarker(markerLayers[2])).not.toBe(marker2) - expect(marker1.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(true) - - buffer.undo({selectionsMarkerLayer: markerLayers[2]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesBefore[0]) - ensureMarkerLayer(markerLayers[2], rangesBefore[0]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).not.toBe(marker1) - expect(getFirstMarker(markerLayers[2])).not.toBe(marker2) - expect(marker1.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(true) - - it "can restore a snapshot taken at a destroyed selections marker layer given selectionsMarkerLayer", -> - buffer.transact {selectionsMarkerLayer: markerLayers[1]}, -> - buffer.append("44444444\n") - marker0.setRange(rangesAfter[0]) - marker1.setRange(rangesAfter[1]) - marker2.setRange(rangesAfter[2]) - - markerLayers[1].destroy() - expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeTruthy() - expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy() - expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeTruthy() - expect(marker0.isDestroyed()).toBe(false) - expect(marker1.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(false) - - buffer.undo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesBefore[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(marker0.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(false) - - buffer.redo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textRedo) - ensureMarkerLayer(markerLayers[0], rangesAfter[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - - markerLayers[3] = markerLayers[2].copy() - ensureMarkerLayer(markerLayers[3], rangesAfter[2]) - markerLayers[0].destroy() - markerLayers[2].destroy() - expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeFalsy() - expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy() - expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeFalsy() - expect(buffer.getMarkerLayer(markerLayers[3].id)).toBeTruthy() - - buffer.undo({selectionsMarkerLayer: markerLayers[3]}) - expect(buffer.getText()).toBe(textUndo) - ensureMarkerLayer(markerLayers[3], rangesBefore[1]) - buffer.redo({selectionsMarkerLayer: markerLayers[3]}) - expect(buffer.getText()).toBe(textRedo) - ensureMarkerLayer(markerLayers[3], rangesAfter[1]) - - it "falls back to normal behavior when the snaphot includes multiple layerSnapshots of selections marker layers", -> - # Transact without selectionsMarkerLayer. - # Taken snapshot includes layerSnapshot of markerLayer[0], markerLayer[1] and markerLayer[2] - buffer.transact -> - buffer.append("44444444\n") - marker0.setRange(rangesAfter[0]) - marker1.setRange(rangesAfter[1]) - marker2.setRange(rangesAfter[2]) - - buffer.undo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesBefore[0]) - ensureMarkerLayer(markerLayers[1], rangesBefore[1]) - ensureMarkerLayer(markerLayers[2], rangesBefore[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - - buffer.redo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textRedo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesAfter[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - - describe "selections marker layer's selective snapshotting on createCheckpoint, groupChangesSinceCheckpoint", -> - it "skips snapshotting of other marker layers with the same role as the selectionsMarkerLayer", -> - eventHandler = jasmine.createSpy('eventHandler') - - args = [] - spyOn(buffer, 'createMarkerSnapshot').and.callFake (arg) -> args.push(arg) - - checkpoint1 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[0]}) - checkpoint2 = buffer.createCheckpoint() - checkpoint3 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[2]}) - checkpoint4 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[1]}) - expect(args).toEqual([ - markerLayers[0], - undefined, - markerLayers[2], - markerLayers[1], - ]) - - buffer.groupChangesSinceCheckpoint(checkpoint4, {selectionsMarkerLayer: markerLayers[0]}) - buffer.groupChangesSinceCheckpoint(checkpoint3, {selectionsMarkerLayer: markerLayers[2]}) - buffer.groupChangesSinceCheckpoint(checkpoint2) - buffer.groupChangesSinceCheckpoint(checkpoint1, {selectionsMarkerLayer: markerLayers[1]}) - expect(args).toEqual([ - markerLayers[0], - undefined, - markerLayers[2], - markerLayers[1], - - markerLayers[0], - markerLayers[2], - undefined, - markerLayers[1], - ]) - - describe "transactions", -> - now = null - - beforeEach -> - now = 0 - spyOn(Date, 'now').and.callFake -> now - - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - buffer.setTextInRange([[1, 3], [1, 5]], 'ms') - - describe "::transact(groupingInterval, fn)", -> - it "groups all operations in the given function in a single transaction", -> - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.transact -> - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you digging?" - buffer.undo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "halts execution of the function if the transaction is aborted", -> - innerContinued = false - outerContinued = false - - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.transact -> - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - buffer.abortTransaction() - innerContinued = true - outerContinued = true - - expect(innerContinued).toBe false - expect(outerContinued).toBe true - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you doing?" - - it "groups all operations performed within the given function into a single undo/redo operation", -> - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you digging?" - - # subsequent changes are not included in the transaction - buffer.setTextInRange([[1, 0], [1, 0]], "little ") - buffer.undo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you digging?" - - # this should undo all changes in the transaction - buffer.undo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - # previous changes are not included in the transaction - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - # this should redo all changes in the transaction - buffer.redo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you digging?" - - # this should redo the change following the transaction - buffer.redo() - expect(buffer.getText()).toBe "hey\nlittle worms\r\nhow are you digging?" - - it "does not push the transaction to the undo stack if it is empty", -> - buffer.transact -> - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - buffer.transact -> buffer.abortTransaction() - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "halts execution undoes all operations since the beginning of the transaction if ::abortTransaction() is called", -> - continuedPastAbort = false - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - buffer.abortTransaction() - continuedPastAbort = true - - expect(continuedPastAbort).toBe false - - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - it "preserves the redo stack until a content change occurs", -> - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - # no changes occur in this transaction before aborting - buffer.transact -> - buffer.markRange([[0, 0], [0, 5]]) - buffer.abortTransaction() - buffer.setTextInRange([[0, 0], [0, 5]], "hey") - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.transact -> - buffer.setTextInRange([[0, 0], [0, 5]], "hey") - buffer.abortTransaction() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "allows nested transactions", -> - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.transact -> - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - buffer.setTextInRange([[2, 18], [2, 19]], "'") - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you diggin'?" - buffer.undo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you doing?" - buffer.redo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you diggin'?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you diggin'?" - - buffer.undo() - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "groups adjacent transactions within each other's grouping intervals", -> - now += 1000 - buffer.transact 101, -> buffer.setTextInRange([[0, 2], [0, 5]], "y") - - now += 100 - buffer.transact 201, -> buffer.setTextInRange([[0, 3], [0, 3]], "yy") - - now += 200 - buffer.transact 201, -> buffer.setTextInRange([[0, 5], [0, 5]], "yy") - - # not grouped because the previous transaction's grouping interval - # is only 200ms and we've advanced 300ms - now += 300 - buffer.transact 301, -> buffer.setTextInRange([[0, 7], [0, 7]], "!!") - - expect(buffer.getText()).toBe "heyyyyy!!\nworms\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "heyyyyy\nworms\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "heyyyyy\nworms\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "heyyyyy!!\nworms\r\nhow are you doing?" - - it "allows undo/redo within transactions, but not beyond the start of the containing transaction", -> - buffer.setText("") - buffer.markPosition([0, 0]) - - buffer.append("a") - - buffer.transact -> - buffer.append("b") - buffer.transact -> buffer.append("c") - buffer.append("d") - - expect(buffer.undo()).toBe true - expect(buffer.getText()).toBe "abc" - - expect(buffer.undo()).toBe true - expect(buffer.getText()).toBe "ab" - - expect(buffer.undo()).toBe true - expect(buffer.getText()).toBe "a" - - expect(buffer.undo()).toBe false - expect(buffer.getText()).toBe "a" - - expect(buffer.redo()).toBe true - expect(buffer.getText()).toBe "ab" - - expect(buffer.redo()).toBe true - expect(buffer.getText()).toBe "abc" - - expect(buffer.redo()).toBe true - expect(buffer.getText()).toBe "abcd" - - expect(buffer.redo()).toBe false - expect(buffer.getText()).toBe "abcd" - - expect(buffer.undo()).toBe true - expect(buffer.getText()).toBe "a" - - it "does not error if the buffer is destroyed in a change callback within the transaction", -> - buffer.onDidChange -> buffer.destroy() - result = buffer.transact -> - buffer.append('!') - 'hi' - expect(result).toBe('hi') - - describe "checkpoints", -> - beforeEach -> - buffer = new TextBuffer - - describe "::getChangesSinceCheckpoint(checkpoint)", -> - it "returns a list of changes that have been made since the checkpoint", -> - buffer.setText('abc\ndef\nghi\njkl\n') - buffer.append("mno\n") - checkpoint = buffer.createCheckpoint() - buffer.transact -> - buffer.append('pqr\n') - buffer.append('stu\n') - buffer.append('vwx\n') - buffer.setTextInRange([[1, 0], [1, 2]], 'yz') - - expect(buffer.getText()).toBe 'abc\nyzf\nghi\njkl\nmno\npqr\nstu\nvwx\n' - assertChangesEqual(buffer.getChangesSinceCheckpoint(checkpoint), [ - { - oldRange: [[1, 0], [1, 2]], - newRange: [[1, 0], [1, 2]], - oldText: "de", - newText: "yz", - }, - { - oldRange: [[5, 0], [5, 0]], - newRange: [[5, 0], [8, 0]], - oldText: "", - newText: "pqr\nstu\nvwx\n", - } - ]) - - it "returns an empty list of changes when no change has been made since the checkpoint", -> - checkpoint = buffer.createCheckpoint() - expect(buffer.getChangesSinceCheckpoint(checkpoint)).toEqual [] - - it "returns an empty list of changes when the checkpoint doesn't exist", -> - buffer.transact -> - buffer.append('abc\n') - buffer.append('def\n') - buffer.append('ghi\n') - expect(buffer.getChangesSinceCheckpoint(-1)).toEqual [] - - describe "::revertToCheckpoint(checkpoint)", -> - it "undoes all changes following the checkpoint", -> - buffer.append("hello") - checkpoint = buffer.createCheckpoint() - - buffer.transact -> - buffer.append("\n") - buffer.append("world") - - buffer.append("\n") - buffer.append("how are you?") - - result = buffer.revertToCheckpoint(checkpoint) - expect(result).toBe(true) - expect(buffer.getText()).toBe("hello") - - buffer.redo() - expect(buffer.getText()).toBe("hello") - - describe "::groupChangesSinceCheckpoint(checkpoint)", -> - it "combines all changes since the checkpoint into a single transaction", -> - historyLayer = buffer.addMarkerLayer(maintainHistory: true) - - buffer.append("one\n") - marker = historyLayer.markRange([[0, 1], [0, 2]]) - marker.setProperties(a: 'b') - - checkpoint = buffer.createCheckpoint() - buffer.append("two\n") - buffer.transact -> - buffer.append("three\n") - buffer.append("four") - - marker.setRange([[0, 1], [2, 3]]) - marker.setProperties(a: 'c') - result = buffer.groupChangesSinceCheckpoint(checkpoint) - - expect(result).toBeTruthy() - expect(buffer.getText()).toBe """ - one - two - three - four - """ - expect(marker.getRange()).toEqual [[0, 1], [2, 3]] - expect(marker.getProperties()).toEqual {a: 'c'} - - buffer.undo() - expect(buffer.getText()).toBe("one\n") - expect(marker.getRange()).toEqual [[0, 1], [0, 2]] - expect(marker.getProperties()).toEqual {a: 'b'} - - buffer.redo() - expect(buffer.getText()).toBe """ - one - two - three - four - """ - expect(marker.getRange()).toEqual [[0, 1], [2, 3]] - expect(marker.getProperties()).toEqual {a: 'c'} - - it "skips any later checkpoints when grouping changes", -> - buffer.append("one\n") - checkpoint = buffer.createCheckpoint() - buffer.append("two\n") - checkpoint2 = buffer.createCheckpoint() - buffer.append("three") - - buffer.groupChangesSinceCheckpoint(checkpoint) - expect(buffer.revertToCheckpoint(checkpoint2)).toBe(false) - - expect(buffer.getText()).toBe """ - one - two - three - """ - - buffer.undo() - expect(buffer.getText()).toBe("one\n") - - buffer.redo() - expect(buffer.getText()).toBe """ - one - two - three - """ - - it "does nothing when no changes have been made since the checkpoint", -> - buffer.append("one\n") - checkpoint = buffer.createCheckpoint() - result = buffer.groupChangesSinceCheckpoint(checkpoint) - expect(result).toBeTruthy() - buffer.undo() - expect(buffer.getText()).toBe "" - - it "returns false and does nothing when the checkpoint is not in the buffer's history", -> - buffer.append("hello\n") - checkpoint = buffer.createCheckpoint() - buffer.undo() - buffer.append("world") - result = buffer.groupChangesSinceCheckpoint(checkpoint) - expect(result).toBeFalsy() - buffer.undo() - expect(buffer.getText()).toBe "" - - it "skips checkpoints when undoing", -> - buffer.append("hello") - buffer.createCheckpoint() - buffer.createCheckpoint() - buffer.createCheckpoint() - buffer.undo() - expect(buffer.getText()).toBe("") - - it "preserves checkpoints across undo and redo", -> - buffer.append("a") - buffer.append("b") - checkpoint1 = buffer.createCheckpoint() - buffer.append("c") - checkpoint2 = buffer.createCheckpoint() - - buffer.undo() - expect(buffer.getText()).toBe("ab") - - buffer.redo() - expect(buffer.getText()).toBe("abc") - - buffer.append("d") - - expect(buffer.revertToCheckpoint(checkpoint2)).toBe true - expect(buffer.getText()).toBe("abc") - expect(buffer.revertToCheckpoint(checkpoint1)).toBe true - expect(buffer.getText()).toBe("ab") - - it "handles checkpoints created when there have been no changes", -> - checkpoint = buffer.createCheckpoint() - buffer.undo() - buffer.append("hello") - buffer.revertToCheckpoint(checkpoint) - expect(buffer.getText()).toBe("") - - it "returns false when the checkpoint is not in the buffer's history", -> - buffer.append("hello\n") - checkpoint = buffer.createCheckpoint() - buffer.undo() - buffer.append("world") - expect(buffer.revertToCheckpoint(checkpoint)).toBe(false) - expect(buffer.getText()).toBe("world") - - it "does not allow changes based on checkpoints outside of the current transaction", -> - checkpoint = buffer.createCheckpoint() - - buffer.append("a") - - buffer.transact -> - expect(buffer.revertToCheckpoint(checkpoint)).toBe false - expect(buffer.getText()).toBe "a" - - buffer.append("b") - - expect(buffer.groupChangesSinceCheckpoint(checkpoint)).toBeFalsy() - - buffer.undo() - expect(buffer.getText()).toBe "a" - - describe "::groupLastChanges()", -> - it "groups the last two changes into a single transaction", -> - buffer = new TextBuffer() - layer = buffer.addMarkerLayer({maintainHistory: true}) - - buffer.append('a') - - # Group two transactions, ensure before/after markers snapshots are preserved - marker = layer.markPosition([0, 0]) - buffer.transact -> - buffer.append('b') - buffer.createCheckpoint() - buffer.transact -> - buffer.append('ccc') - marker.setHeadPosition([0, 2]) - - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(marker.getHeadPosition()).toEqual([0, 0]) - expect(buffer.getText()).toBe('a') - buffer.redo() - expect(marker.getHeadPosition()).toEqual([0, 2]) - buffer.undo() - - # Group two bare changes - buffer.transact -> - buffer.append('b') - buffer.createCheckpoint() - buffer.append('c') - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(buffer.getText()).toBe('a') - - # Group a transaction with a bare change - buffer.transact -> - buffer.transact -> - buffer.append('b') - buffer.append('c') - buffer.append('d') - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(buffer.getText()).toBe('a') - - # Group a bare change with a transaction - buffer.transact -> - buffer.append('b') - buffer.transact -> - buffer.append('c') - buffer.append('d') - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(buffer.getText()).toBe('a') - - # Can't group past the beginning of an open transaction - buffer.transact -> - expect(buffer.groupLastChanges()).toBe(false) - buffer.append('b') - expect(buffer.groupLastChanges()).toBe(false) - buffer.append('c') - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(buffer.getText()).toBe('a') - - describe "::setHistoryProvider(provider)", -> - it "replaces the currently active history provider with the passed one", -> - buffer = new TextBuffer({text: ''}) - buffer.insert([0, 0], 'Lorem ') - buffer.insert([0, 6], 'ipsum ') - expect(buffer.getText()).toBe('Lorem ipsum ') - - buffer.undo() - expect(buffer.getText()).toBe('Lorem ') - - buffer.setHistoryProvider(new DefaultHistoryProvider(buffer)) - buffer.undo() - expect(buffer.getText()).toBe('Lorem ') - - buffer.insert([0, 6], 'dolor ') - expect(buffer.getText()).toBe('Lorem dolor ') - - buffer.undo() - expect(buffer.getText()).toBe('Lorem ') - - describe "::getHistory(maxEntries) and restoreDefaultHistoryProvider(history)", -> - it "returns a base text and the state of the last `maxEntries` entries in the undo and redo stacks", -> - buffer = new TextBuffer({text: ''}) - markerLayer = buffer.addMarkerLayer({maintainHistory: true}) - - buffer.append('Lorem ') - buffer.append('ipsum ') - buffer.append('dolor ') - markerLayer.markPosition([0, 2]) - markersSnapshotAtCheckpoint1 = buffer.createMarkerSnapshot() - checkpoint1 = buffer.createCheckpoint() - buffer.append('sit ') - buffer.append('amet ') - buffer.append('consecteur ') - markerLayer.markPosition([0, 4]) - markersSnapshotAtCheckpoint2 = buffer.createMarkerSnapshot() - checkpoint2 = buffer.createCheckpoint() - buffer.append('adipiscit ') - buffer.append('elit ') - buffer.undo() - buffer.undo() - buffer.undo() - - history = buffer.getHistory(3) - expect(history.baseText).toBe('Lorem ipsum dolor ') - expect(history.nextCheckpointId).toBe(buffer.createCheckpoint()) - expect(history.undoStack).toEqual([ - { - type: 'checkpoint', - id: checkpoint1, - markers: markersSnapshotAtCheckpoint1 - }, - { - type: 'transaction', - changes: [{oldStart: Point(0, 18), oldEnd: Point(0, 18), newStart: Point(0, 18), newEnd: Point(0, 22), oldText: '', newText: 'sit '}], - markersBefore: markersSnapshotAtCheckpoint1, - markersAfter: markersSnapshotAtCheckpoint1 - }, - { - type: 'transaction', - changes: [{oldStart: Point(0, 22), oldEnd: Point(0, 22), newStart: Point(0, 22), newEnd: Point(0, 27), oldText: '', newText: 'amet '}], - markersBefore: markersSnapshotAtCheckpoint1, - markersAfter: markersSnapshotAtCheckpoint1 - } - ]) - expect(history.redoStack).toEqual([ - { - type: 'transaction', - changes: [{oldStart: Point(0, 38), oldEnd: Point(0, 38), newStart: Point(0, 38), newEnd: Point(0, 48), oldText: '', newText: 'adipiscit '}], - markersBefore: markersSnapshotAtCheckpoint2, - markersAfter: markersSnapshotAtCheckpoint2 - }, - { - type: 'checkpoint', - id: checkpoint2, - markers: markersSnapshotAtCheckpoint2 - }, - { - type: 'transaction', - changes: [{oldStart: Point(0, 27), oldEnd: Point(0, 27), newStart: Point(0, 27), newEnd: Point(0, 38), oldText: '', newText: 'consecteur '}], - markersBefore: markersSnapshotAtCheckpoint1, - markersAfter: markersSnapshotAtCheckpoint1 - } - ]) - - buffer.createCheckpoint() - buffer.append('x') - buffer.undo() - buffer.clearUndoStack() - - expect(buffer.getHistory()).not.toEqual(history) - buffer.restoreDefaultHistoryProvider(history) - expect(buffer.getHistory()).toEqual(history) - - it "throws an error when called within a transaction", -> - buffer = new TextBuffer() - expect(-> - buffer.transact(-> buffer.getHistory(3)) - ).toThrowError() - - describe "::getTextInRange(range)", -> - it "returns the text in a given range", -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - expect(buffer.getTextInRange([[1, 1], [1, 4]])).toBe "orl" - expect(buffer.getTextInRange([[0, 3], [2, 3]])).toBe "lo\nworld\r\nhow" - expect(buffer.getTextInRange([[0, 0], [2, 18]])).toBe buffer.getText() - - it "clips the given range", -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - expect(buffer.getTextInRange([[-100, -100], [100, 100]])).toBe buffer.getText() - - describe "::clipPosition(position)", -> - it "returns a valid position closest to the given position", -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - expect(buffer.clipPosition([-1, -1])).toEqual [0, 0] - expect(buffer.clipPosition([-1, 2])).toEqual [0, 0] - expect(buffer.clipPosition([0, -1])).toEqual [0, 0] - expect(buffer.clipPosition([0, 20])).toEqual [0, 5] - expect(buffer.clipPosition([1, -1])).toEqual [1, 0] - expect(buffer.clipPosition([1, 20])).toEqual [1, 5] - expect(buffer.clipPosition([10, 0])).toEqual [2, 18] - expect(buffer.clipPosition([Infinity, 0])).toEqual [2, 18] - - it "throws an error when given an invalid point", -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - expect -> buffer.clipPosition([NaN, 1]) - .toThrowError("Invalid Point: (NaN, 1)") - expect -> buffer.clipPosition([0, NaN]) - .toThrowError("Invalid Point: (0, NaN)") - expect -> buffer.clipPosition([0, {}]) - .toThrowError("Invalid Point: (0, [object Object])") - - describe "::characterIndexForPosition(position)", -> - beforeEach -> - buffer = new TextBuffer(text: "zero\none\r\ntwo\nthree") - - it "returns the absolute character offset for the given position", -> - expect(buffer.characterIndexForPosition([0, 0])).toBe 0 - expect(buffer.characterIndexForPosition([0, 1])).toBe 1 - expect(buffer.characterIndexForPosition([0, 4])).toBe 4 - expect(buffer.characterIndexForPosition([1, 0])).toBe 5 - expect(buffer.characterIndexForPosition([1, 1])).toBe 6 - expect(buffer.characterIndexForPosition([1, 3])).toBe 8 - expect(buffer.characterIndexForPosition([2, 0])).toBe 10 - expect(buffer.characterIndexForPosition([2, 1])).toBe 11 - expect(buffer.characterIndexForPosition([3, 0])).toBe 14 - expect(buffer.characterIndexForPosition([3, 5])).toBe 19 - - it "clips the given position before translating", -> - expect(buffer.characterIndexForPosition([-1, -1])).toBe 0 - expect(buffer.characterIndexForPosition([1, 100])).toBe 8 - expect(buffer.characterIndexForPosition([100, 100])).toBe 19 - - describe "::positionForCharacterIndex(offset)", -> - beforeEach -> - buffer = new TextBuffer(text: "zero\none\r\ntwo\nthree") - - it "returns the position for the given absolute character offset", -> - expect(buffer.positionForCharacterIndex(0)).toEqual [0, 0] - expect(buffer.positionForCharacterIndex(1)).toEqual [0, 1] - expect(buffer.positionForCharacterIndex(4)).toEqual [0, 4] - expect(buffer.positionForCharacterIndex(5)).toEqual [1, 0] - expect(buffer.positionForCharacterIndex(6)).toEqual [1, 1] - expect(buffer.positionForCharacterIndex(8)).toEqual [1, 3] - expect(buffer.positionForCharacterIndex(10)).toEqual [2, 0] - expect(buffer.positionForCharacterIndex(11)).toEqual [2, 1] - expect(buffer.positionForCharacterIndex(14)).toEqual [3, 0] - expect(buffer.positionForCharacterIndex(19)).toEqual [3, 5] - - it "clips the given offset before translating", -> - expect(buffer.positionForCharacterIndex(-1)).toEqual [0, 0] - expect(buffer.positionForCharacterIndex(20)).toEqual [3, 5] - - describe "serialization", -> - expectSameMarkers = (left, right) -> - markers1 = left.getMarkers().sort (a, b) -> a.compare(b) - markers2 = right.getMarkers().sort (a, b) -> a.compare(b) - expect(markers1.length).toBe markers2.length - for marker1, i in markers1 - expect(marker1).toEqual(markers2[i]) - return - - it "can serialize / deserialize the buffer along with its history, marker layers, and display layers", (done) -> - bufferA = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - displayLayer1A = bufferA.addDisplayLayer() - displayLayer2A = bufferA.addDisplayLayer() - displayLayer1A.foldBufferRange([[0, 1], [0, 3]]) - displayLayer2A.foldBufferRange([[0, 0], [0, 2]]) - bufferA.createCheckpoint() - bufferA.setTextInRange([[0, 5], [0, 5]], " there") - bufferA.transact -> bufferA.setTextInRange([[1, 0], [1, 5]], "friend") - layerA = bufferA.addMarkerLayer(maintainHistory: true, persistent: true) - layerA.markRange([[0, 6], [0, 8]], reversed: true, foo: 1) - layerB = bufferA.addMarkerLayer(maintainHistory: true, persistent: true, role: "selections") - marker2A = bufferA.markPosition([2, 2], bar: 2) - bufferA.transact -> - bufferA.setTextInRange([[1, 0], [1, 0]], "good ") - bufferA.append("?") - marker2A.setProperties(bar: 3, baz: 4) - layerA.markRange([[0, 4], [0, 5]], invalidate: 'inside') - bufferA.setTextInRange([[0, 5], [0, 5]], "oo") - bufferA.undo() - - state = JSON.parse(JSON.stringify(bufferA.serialize())) - TextBuffer.deserialize(state).then (bufferB) -> - expect(bufferB.getText()).toBe "hello there\ngood friend\r\nhow are you doing??" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - expect(bufferB.getDisplayLayer(displayLayer1A.id).foldsIntersectingBufferRange([[0, 1], [0, 3]]).length).toBe(1) - expect(bufferB.getDisplayLayer(displayLayer2A.id).foldsIntersectingBufferRange([[0, 0], [0, 2]]).length).toBe(1) - displayLayer3B = bufferB.addDisplayLayer() - expect(displayLayer3B.id).toBeGreaterThan(displayLayer1A.id) - expect(displayLayer3B.id).toBeGreaterThan(displayLayer2A.id) - - expect(bufferB.getMarkerLayer(layerB.id).getRole()).toBe "selections" - expect(bufferB.selectionsMarkerLayerIds.has(layerB.id)).toBe true - expect(bufferB.selectionsMarkerLayerIds.size).toBe 1 - - bufferA.redo() - bufferB.redo() - expect(bufferB.getText()).toBe "hellooo there\ngood friend\r\nhow are you doing??" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - expect(bufferB.getMarkerLayer(layerA.id).maintainHistory).toBe true - expect(bufferB.getMarkerLayer(layerA.id).persistent).toBe true - - bufferA.undo() - bufferB.undo() - expect(bufferB.getText()).toBe "hello there\ngood friend\r\nhow are you doing??" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - - bufferA.undo() - bufferB.undo() - expect(bufferB.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - - bufferA.undo() - bufferB.undo() - expect(bufferB.getText()).toBe "hello there\nworld\r\nhow are you doing?" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - - bufferA.undo() - bufferB.undo() - expect(bufferB.getText()).toBe "hello\nworld\r\nhow are you doing?" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - - # Accounts for deserialized markers when selecting the next marker's id - marker3A = layerA.markRange([[0, 1], [2, 3]]) - marker3B = bufferB.getMarkerLayer(layerA.id).markRange([[0, 1], [2, 3]]) - expect(marker3B.id).toBe marker3A.id - - # Doesn't try to reload the buffer since it has no file. - setTimeout(-> - expect(bufferB.getText()).toBe "hello\nworld\r\nhow are you doing?" - done() - , 50) - - it "serializes / deserializes the buffer's persistent custom marker layers", (done) -> - bufferA = new TextBuffer("abcdefghijklmnopqrstuvwxyz") - - layer1A = bufferA.addMarkerLayer() - layer2A = bufferA.addMarkerLayer(persistent: true) - - layer1A.markRange([[0, 1], [0, 2]]) - layer1A.markRange([[0, 3], [0, 4]]) - - layer2A.markRange([[0, 5], [0, 6]]) - layer2A.markRange([[0, 7], [0, 8]]) - - TextBuffer.deserialize(JSON.parse(JSON.stringify(bufferA.serialize()))).then (bufferB) -> - layer1B = bufferB.getMarkerLayer(layer1A.id) - layer2B = bufferB.getMarkerLayer(layer2A.id) - expect(layer2B.persistent).toBe true - - expect(layer1B).toBe undefined - expectSameMarkers(layer2A, layer2B) - done() - - it "doesn't serialize the default marker layer", (done) -> - bufferA = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - markerLayerA = bufferA.getDefaultMarkerLayer() - marker1A = bufferA.markRange([[0, 1], [1, 2]], foo: 1) - - TextBuffer.deserialize(bufferA.serialize()).then (bufferB) -> - markerLayerB = bufferB.getDefaultMarkerLayer() - expect(bufferB.getMarker(marker1A.id)).toBeUndefined() - done() - - it "doesn't attempt to serialize snapshots for destroyed marker layers", -> - buffer = new TextBuffer(text: "abc") - markerLayer = buffer.addMarkerLayer(maintainHistory: true, persistent: true) - markerLayer.markPosition([0, 3]) - buffer.insert([0, 0], 'x') - markerLayer.destroy() - - expect(-> buffer.serialize()).not.toThrowError() - - it "doesn't remember marker layers when calling serialize with {markerLayers: false}", (done) -> - bufferA = new TextBuffer(text: "world") - layerA = bufferA.addMarkerLayer(maintainHistory: true) - markerA = layerA.markPosition([0, 3]) - markerB = null - bufferA.transact -> - bufferA.insert([0, 0], 'hello ') - markerB = layerA.markPosition([0, 5]) - bufferA.undo() - - TextBuffer.deserialize(bufferA.serialize({markerLayers: false})).then (bufferB) -> - expect(bufferB.getText()).toBe("world") - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined() - - bufferB.redo() - expect(bufferB.getText()).toBe("hello world") - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined() - - bufferB.undo() - expect(bufferB.getText()).toBe("world") - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined() - done() - - it "doesn't remember history when calling serialize with {history: false}", (done) -> - bufferA = new TextBuffer(text: 'abc') - bufferA.append('def') - bufferA.append('ghi') - - TextBuffer.deserialize(bufferA.serialize({history: false})).then (bufferB) -> - expect(bufferB.getText()).toBe("abcdefghi") - expect(bufferB.undo()).toBe(false) - expect(bufferB.getText()).toBe("abcdefghi") - done() - - it "serializes / deserializes the buffer's unique identifier", (done) -> - bufferA = new TextBuffer() - TextBuffer.deserialize(JSON.parse(JSON.stringify(bufferA.serialize()))).then (bufferB) -> - expect(bufferB.getId()).toEqual(bufferA.getId()) - done() - - it "doesn't deserialize a state that was serialized with a different buffer version", (done) -> - bufferA = new TextBuffer() - serializedBuffer = JSON.parse(JSON.stringify(bufferA.serialize())) - serializedBuffer.version = 123456789 - - TextBuffer.deserialize(serializedBuffer).then (bufferB) -> - expect(bufferB).toBeUndefined() - done() - - it "doesn't deserialize a state referencing a file that no longer exists", (done) -> - tempDir = fs.realpathSync(temp.mkdirSync('text-buffer')) - filePath = join(tempDir, 'file.txt') - fs.writeFileSync(filePath, "something\n") - - bufferA = TextBuffer.loadSync(filePath) - state = bufferA.serialize() - - fs.unlinkSync(filePath) - - state.mustExist = true - TextBuffer.deserialize(state).then( - -> expect('serialization succeeded with mustExist: true').toBeUndefined(), - (err) -> expect(err.code).toBe('ENOENT') - ).then(done, done) - - describe "when the serialized buffer was unsaved and had no path", -> - it "restores the previous unsaved state of the buffer", (done) -> - buffer = new TextBuffer() - buffer.setText("abc") - - TextBuffer.deserialize(buffer.serialize()).then (buffer2) -> - expect(buffer2.getPath()).toBeUndefined() - expect(buffer2.getText()).toBe("abc") - done() - - describe "::getRange()", -> - it "returns the range of the entire buffer text", -> - buffer = new TextBuffer("abc\ndef\nghi") - expect(buffer.getRange()).toEqual [[0, 0], [2, 3]] - - describe "::getLength()", -> - it "returns the lenght of the entire buffer text", -> - buffer = new TextBuffer("abc\ndef\nghi") - expect(buffer.getLength()).toBe("abc\ndef\nghi".length) - - describe "::rangeForRow(row, includeNewline)", -> - beforeEach -> - buffer = new TextBuffer("this\nis a test\r\ntesting") - - describe "if includeNewline is false (the default)", -> - it "returns a range from the beginning of the line to the end of the line", -> - expect(buffer.rangeForRow(0)).toEqual([[0, 0], [0, 4]]) - expect(buffer.rangeForRow(1)).toEqual([[1, 0], [1, 9]]) - expect(buffer.rangeForRow(2)).toEqual([[2, 0], [2, 7]]) - - describe "if includeNewline is true", -> - it "returns a range from the beginning of the line to the beginning of the next (if it exists)", -> - expect(buffer.rangeForRow(0, true)).toEqual([[0, 0], [1, 0]]) - expect(buffer.rangeForRow(1, true)).toEqual([[1, 0], [2, 0]]) - expect(buffer.rangeForRow(2, true)).toEqual([[2, 0], [2, 7]]) - - describe "if the given row is out of range", -> - it "returns the range of the nearest valid row", -> - expect(buffer.rangeForRow(-1)).toEqual([[0, 0], [0, 4]]) - expect(buffer.rangeForRow(10)).toEqual([[2, 0], [2, 7]]) - - describe "::onDidChangePath()", -> - [filePath, newPath, bufferToChange, eventHandler] = [] - - beforeEach -> - tempDir = fs.realpathSync(temp.mkdirSync('text-buffer')) - filePath = join(tempDir, "manipulate-me") - newPath = "#{filePath}-i-moved" - fs.writeFileSync(filePath, "") - bufferToChange = TextBuffer.loadSync(filePath) - - afterEach -> - bufferToChange.destroy() - fs.removeSync(filePath) - fs.removeSync(newPath) - - it "notifies observers when the buffer is saved to a new path", (done) -> - bufferToChange.onDidChangePath (p) -> - expect(p).toBe(newPath) - done() - bufferToChange.saveAs(newPath) - - it "notifies observers when the buffer's file is moved", (done) -> - # FIXME: This doesn't pass on Linux - if process.platform in ['linux', 'win32'] - done() - return - - bufferToChange.onDidChangePath (p) -> - expect(p).toBe(newPath) - done() - - fs.removeSync(newPath) - fs.moveSync(filePath, newPath) - - describe "::onWillThrowWatchError", -> - it "notifies observers when the file has a watch error", -> - filePath = temp.openSync('atom').path - fs.writeFileSync(filePath, '') - - buffer = TextBuffer.loadSync(filePath) - - eventHandler = jasmine.createSpy('eventHandler') - buffer.onWillThrowWatchError(eventHandler) - - buffer.file.emitter.emit 'will-throw-watch-error', 'arg' - expect(eventHandler).toHaveBeenCalledWith 'arg' - - describe "::getLines()", -> - it "returns an array of lines in the text contents", -> - filePath = require.resolve('./fixtures/sample.js') - fileContents = fs.readFileSync(filePath, 'utf8') - buffer = TextBuffer.loadSync(filePath) - expect(buffer.getLines().length).toBe fileContents.split("\n").length - expect(buffer.getLines().join('\n')).toBe fileContents - - describe "::setTextInRange(range, string)", -> - changeHandler = null - - beforeEach (done) -> - filePath = require.resolve('./fixtures/sample.js') - fileContents = fs.readFileSync(filePath, 'utf8') - TextBuffer.load(filePath).then (result) -> - buffer = result - changeHandler = jasmine.createSpy('changeHandler') - buffer.onDidChange changeHandler - done() - - 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.setTextInRange 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.calls.allArgs()[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.setTextInRange 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.calls.allArgs()[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.setTextInRange 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.calls.allArgs()[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.setTextInRange 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.calls.allArgs()[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.setTextInRange [[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.setTextInRange 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.calls.allArgs()[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 change to be undone safely from an ::onDidChange callback", -> - buffer.onDidChange -> buffer.undo() - buffer.setTextInRange([[0, 0], [0, 0]], "hello") - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - describe "::setText(text)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - describe "when the buffer contains newlines", -> - 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.onDidChange changeHandler - - newText = "I know you are.\nBut what am I?" - buffer.setText(newText) - - expect(buffer.getText()).toBe newText - expect(changeHandler).toHaveBeenCalled() - - [event] = changeHandler.calls.allArgs()[0] - expect(event.newText).toBe newText - expect(event.oldRange).toEqual expectedPreRange - expect(event.newRange).toEqual [[0, 0], [1, 14]] - - describe "with windows newlines", -> - it "changes the entire contents of the buffer", -> - buffer = new TextBuffer("first\r\nlast") - lastRow = buffer.getLastRow() - expectedPreRange = [[0, 0], [lastRow, buffer.lineForRow(lastRow).length]] - changeHandler = jasmine.createSpy('changeHandler') - buffer.onDidChange changeHandler - - newText = "new first\r\nnew last" - buffer.setText(newText) - - expect(buffer.getText()).toBe newText - expect(changeHandler).toHaveBeenCalled() - - [event] = changeHandler.calls.allArgs()[0] - expect(event.newText).toBe newText - expect(event.oldRange).toEqual expectedPreRange - expect(event.newRange).toEqual [[0, 0], [1, 8]] - - describe "::setTextViaDiff(text)", -> - beforeEach (done) -> - filePath = require.resolve('./fixtures/sample.js') - TextBuffer.load(filePath).then (result) -> - buffer = result - done() - - 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 - - it "can change a buffer that contains lone carriage returns", -> - oldText = 'one\rtwo\nthree\rfour\n' - newText = 'one\rtwo and\nthree\rfour\n' - buffer.setText(oldText) - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - buffer.undo() - expect(buffer.getText()).toBe oldText - - 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 "::getTextInRange(range)", -> - beforeEach (done) -> - filePath = require.resolve('./fixtures/sample.js') - TextBuffer.load(filePath).then (result) -> - buffer = result - done() - - 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 -> - buffer = TextBuffer.loadSync(require.resolve('./fixtures/sample.js')) - - it "calls the given function with the information about each match", -> - matches = [] - buffer.scan /current/g, (match) -> matches.push(match) - expect(matches.length).toBe 5 - - expect(matches[0].matchText).toBe 'current' - expect(matches[0].range).toEqual [[3, 31], [3, 38]] - expect(matches[0].lineText).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(matches[0].lineTextOffset).toBe 0 - expect(matches[0].leadingContextLines.length).toBe 0 - expect(matches[0].trailingContextLines.length).toBe 0 - - expect(matches[1].matchText).toBe 'current' - expect(matches[1].range).toEqual [[5, 6], [5, 13]] - expect(matches[1].lineText).toBe ' current = items.shift();' - expect(matches[1].lineTextOffset).toBe 0 - expect(matches[1].leadingContextLines.length).toBe 0 - expect(matches[1].trailingContextLines.length).toBe 0 - - it "calls the given function with the information about each match including context lines", -> - matches = [] - buffer.scan /current/g, {leadingContextLineCount: 1, trailingContextLineCount: 2}, (match) -> matches.push(match) - expect(matches.length).toBe 5 - - expect(matches[0].matchText).toBe 'current' - expect(matches[0].range).toEqual [[3, 31], [3, 38]] - expect(matches[0].lineText).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(matches[0].lineTextOffset).toBe 0 - expect(matches[0].leadingContextLines.length).toBe 1 - expect(matches[0].leadingContextLines[0]).toBe ' if (items.length <= 1) return items;' - expect(matches[0].trailingContextLines.length).toBe 2 - expect(matches[0].trailingContextLines[0]).toBe ' while(items.length > 0) {' - expect(matches[0].trailingContextLines[1]).toBe ' current = items.shift();' - - expect(matches[1].matchText).toBe 'current' - expect(matches[1].range).toEqual [[5, 6], [5, 13]] - expect(matches[1].lineText).toBe ' current = items.shift();' - expect(matches[1].lineTextOffset).toBe 0 - expect(matches[1].leadingContextLines.length).toBe 1 - expect(matches[1].leadingContextLines[0]).toBe ' while(items.length > 0) {' - expect(matches[1].trailingContextLines.length).toBe 2 - expect(matches[1].trailingContextLines[0]).toBe ' current < pivot ? left.push(current) : right.push(current);' - expect(matches[1].trailingContextLines[1]).toBe ' }' - - describe "::backwardsScan(regex, fn)", -> - beforeEach -> - buffer = TextBuffer.loadSync(require.resolve('./fixtures/sample.js')) - - it "calls the given function with the information about each match in backwards order", -> - matches = [] - buffer.backwardsScan /current/g, (match) -> matches.push(match) - expect(matches.length).toBe 5 - - expect(matches[0].matchText).toBe 'current' - expect(matches[0].range).toEqual [[6, 56], [6, 63]] - expect(matches[0].lineText).toBe ' current < pivot ? left.push(current) : right.push(current);' - expect(matches[0].lineTextOffset).toBe 0 - expect(matches[0].leadingContextLines.length).toBe 0 - expect(matches[0].trailingContextLines.length).toBe 0 - - expect(matches[1].matchText).toBe 'current' - expect(matches[1].range).toEqual [[6, 34], [6, 41]] - expect(matches[1].lineText).toBe ' current < pivot ? left.push(current) : right.push(current);' - expect(matches[1].lineTextOffset).toBe 0 - expect(matches[1].leadingContextLines.length).toBe 0 - expect(matches[1].trailingContextLines.length).toBe 0 - - describe "::scanInRange(range, regex, fn)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - 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 "does not call 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 is 2 - - expect(ranges.length).toBe 2 - - it "returns the same results as a regex match on a regular string", -> - regexps = [ - /\w+/g # 1 word - /\w+\n\s*\w+/g, # 2 words separated by an newline (escape sequence) - RegExp("\\w+\n\\s*\w+", 'g'), # 2 words separated by a newline (literal) - /\w+\s+\w+/g, # 2 words separated by some whitespace - /\w+[^\w]+\w+/g, # 2 words separated by anything - /\w+\n\s*\w+\n\s*\w+/g, # 3 words separated by newlines (escape sequence) - RegExp("\\w+\n\\s*\\w+\n\\s*\\w+", 'g'), # 3 words separated by newlines (literal) - /\w+[^\w]+\w+[^\w]+\w+/g, # 3 words separated by anything - ] - - i = 0 - while i < 20 - seed = Date.now() - random = new Random(seed) - - text = buildRandomLines(random, 40) - buffer = new TextBuffer({text}) - buffer.backwardsScanChunkSize = random.intBetween(100, 1000) - - range = getRandomBufferRange(random, buffer) - .union(getRandomBufferRange(random, buffer)) - .union(getRandomBufferRange(random, buffer)) - regex = regexps[random(regexps.length)] - - expectedMatches = buffer.getTextInRange(range).match(regex) ? [] - continue unless expectedMatches.length > 0 - i++ - - forwardRanges = [] - forwardMatches = [] - buffer.scanInRange regex, range, ({range, matchText}) -> - forwardRanges.push(range) - forwardMatches.push(matchText) - expect(forwardMatches).toEqual(expectedMatches, "Seed: #{seed}") - - backwardRanges = [] - backwardMatches = [] - buffer.backwardsScanInRange regex, range, ({range, matchText}) -> - backwardRanges.push(range) - backwardMatches.push(matchText) - expect(backwardMatches).toEqual(expectedMatches.reverse(), "Seed: #{seed}") - - it "does not return empty matches at the end of the range", -> - ranges = [] - buffer.scanInRange /[ ]*/gm, [[0, 29], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[0, 29], [0, 29]], [[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.scanInRange /[ ]*/gm, [[1, 0], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.scanInRange /\s*/gm, [[0, 29], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[0, 29], [1, 2]]]) - - ranges.length = 0 - buffer.scanInRange /\s*/gm, [[1, 0], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - it "allows empty matches at the end of a range, when the range ends at column 0", -> - ranges = [] - buffer.scanInRange /^[ ]*/gm, [[9, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[9, 0], [9, 2]], [[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.scanInRange /^[ ]*/gm, [[10, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.scanInRange /^\s*/gm, [[9, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[9, 0], [9, 2]], [[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.scanInRange /^\s*/gm, [[10, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.scanInRange /^\s*/gm, [[11, 0], [12, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[11, 0], [11, 2]], [[12, 0], [12, 0]]]) - - it "handles multi-line patterns", -> - matchStrings = [] - - # The '\s' character class - buffer.scan /{\s+var/, ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['{\n var']) - - # A literal newline character - matchStrings.length = 0 - buffer.scan RegExp("{\n var"), ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['{\n var']) - - # A '\n' escape sequence - matchStrings.length = 0 - buffer.scan /{\n var/, ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['{\n var']) - - # A negated character class in the middle of the pattern - matchStrings.length = 0 - buffer.scan /{[^a] var/, ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['{\n var']) - - # A negated character class at the beginning of the pattern - matchStrings.length = 0 - buffer.scan /[^a] var/, ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['\n var']) - - describe "::find(regex)", -> - it "resolves with the first range that matches the given regex", (done) -> - buffer = new TextBuffer('abc\ndefghi') - buffer.find(/\wf\w*/).then (range) -> - expect(range).toEqual(Range(Point(1, 1), Point(1, 6))) - done() - - describe "::findAllSync(regex)", -> - it "returns all the ranges that match the given regex", -> - buffer = new TextBuffer('abc\ndefghi') - expect(buffer.findAllSync(/[bf]\w+/)).toEqual([ - Range(Point(0, 1), Point(0, 3)), - Range(Point(1, 2), Point(1, 6)), - ]) - - describe "::findAndMarkAllInRangeSync(markerLayer, regex, range, options)", -> - it "populates the marker index with the matching ranges", -> - buffer = new TextBuffer('abc def\nghi jkl\n') - layer = buffer.addMarkerLayer() - markers = buffer.findAndMarkAllInRangeSync(layer, /\w+/g, [[0, 1], [1, 6]], {invalidate: 'inside'}) - expect(markers.map((marker) -> marker.getRange())).toEqual([ - [[0, 1], [0, 3]], - [[0, 4], [0, 7]], - [[1, 0], [1, 3]], - [[1, 4], [1, 6]] - ]) - expect(markers[0].getInvalidationStrategy()).toBe('inside') - expect(markers[0].isExclusive()).toBe(true) - - markers = buffer.findAndMarkAllInRangeSync(layer, /abc/g, [[0, 0], [1, 0]], {invalidate: 'touch'}) - expect(markers.map((marker) -> marker.getRange())).toEqual([ - [[0, 0], [0, 3]] - ]) - expect(markers[0].getInvalidationStrategy()).toBe('touch') - expect(markers[0].isExclusive()).toBe(false) - - describe "::findWordsWithSubsequence and ::findWordsWithSubsequenceInRange", -> - it 'resolves with all words matching the given query', (done) -> - buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana') - buffer.findWordsWithSubsequence('bna', '_', 4).then (results) -> - expect(JSON.parse(JSON.stringify(results))).toEqual([ - { - score: 29, - matchIndices: [0, 1, 2], - positions: [{row: 0, column: 36}], - word: "bNa" - }, - { - score: 16, - matchIndices: [0, 2, 4], - positions: [{row: 0, column: 15}], - word: "ban_ana" - }, - { - score: 12, - matchIndices: [0, 2, 3], - positions: [{row: 0, column: 0}, {row: 1, column: 0}], - word: "banana" - }, - { - score: 7, - matchIndices: [0, 5, 6], - positions: [{row: 0, column: 7}], - word: "bandana" - } - ]) - done() - - it 'resolves with all words matching the given query and range', (done) -> - range = {start: {column: 0, row: 0}, end: {column: 22, row: 0}} - buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana') - buffer.findWordsWithSubsequenceInRange('bna', '_', 3, range).then (results) -> - expect(JSON.parse(JSON.stringify(results))).toEqual([ - { - score: 16, - matchIndices: [0, 2, 4], - positions: [{row: 0, column: 15}], - word: "ban_ana" - }, - { - score: 12, - matchIndices: [0, 2, 3], - positions: [{row: 0, column: 0}], - word: "banana" - }, - { - score: 7, - matchIndices: [0, 5, 6], - positions: [{row: 0, column: 7}], - word: "bandana" - } - ]) - done() - - describe "::backwardsScanInRange(range, regex, fn)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - 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 last regex match starts at the beginning of the range", -> - it "calls the iterator with the match", -> - matches = [] - ranges = [] - buffer.scanInRange /quick/g, [[0, 4], [2, 0]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 1 - expect(ranges.length).toBe 1 - - expect(matches[0][0]).toBe 'quick' - expect(ranges[0]).toEqual [[0, 4], [0, 9]] - - matches = [] - ranges = [] - buffer.scanInRange /^/, [[0, 0], [2, 0]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 1 - expect(ranges.length).toBe 1 - - expect(matches[0][0]).toBe "" - expect(ranges[0]).toEqual [[0, 0], [0, 0]] - - describe "when the first 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.backwardsScanInRange /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 'cur' - expect(matches[0][1]).toBe 'r' - expect(ranges[0]).toEqual [[6, 6], [6, 9]] - - expect(matches[1][0]).toBe 'curr' - expect(matches[1][1]).toBe 'rr' - expect(ranges[1]).toEqual [[5, 6], [5, 10]] - - describe "when the portion of the match within the range does not matches the regex", -> - it "does not call the iterator with the truncated match", -> - matches = [] - ranges = [] - buffer.backwardsScanInRange /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.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 is 2 - - expect(ranges.length).toBe 2 - expect(ranges[0]).toEqual [[6, 34], [6, 41]] - expect(ranges[1]).toEqual [[6, 6], [6, 13]] - - describe "when called with a random range", -> - it "returns the same results as ::scanInRange, but in the opposite order", -> - for i in [1...50] - seed = Date.now() - random = new Random(seed) - - buffer.backwardsScanChunkSize = random.intBetween(1, 80) - - [startRow, endRow] = [random(buffer.getLineCount()), random(buffer.getLineCount())].sort() - startColumn = random(buffer.lineForRow(startRow).length) - endColumn = random(buffer.lineForRow(endRow).length) - range = [[startRow, startColumn], [endRow, endColumn]] - - regex = [ - /\w/g - /\w{2}/g - /\w{3}/g - /.{5}/g - ][random(4)] - - if random(2) > 0 - forwardRanges = [] - backwardRanges = [] - forwardMatches = [] - backwardMatches = [] - - buffer.scanInRange regex, range, ({range, matchText}) -> - forwardMatches.push(matchText) - forwardRanges.push(range) - - buffer.backwardsScanInRange regex, range, ({range, matchText}) -> - backwardMatches.unshift(matchText) - backwardRanges.unshift(range) - - expect(backwardRanges).toEqual(forwardRanges, "Seed: #{seed}") - expect(backwardMatches).toEqual(forwardMatches, "Seed: #{seed}") - else - referenceBuffer = new TextBuffer(text: buffer.getText()) - referenceBuffer.scanInRange regex, range, ({matchText, replace}) -> - replace(matchText + '.') - - buffer.backwardsScanInRange regex, range, ({matchText, replace}) -> - replace(matchText + '.') - - expect(buffer.getText()).toBe(referenceBuffer.getText(), "Seed: #{seed}") - - it "does not return empty matches at the end of the range", -> - ranges = [] - - buffer.backwardsScanInRange /[ ]*/gm, [[1, 0], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /[ ]*/m, [[0, 29], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /\s*/gm, [[1, 0], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /\s*/m, [[0, 29], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[0, 29], [1, 2]]]) - - it "allows empty matches at the end of a range, when the range ends at column 0", -> - ranges = [] - buffer.backwardsScanInRange /^[ ]*/gm, [[9, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]], [[9, 0], [9, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /^[ ]*/gm, [[10, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /^\s*/gm, [[9, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]], [[9, 0], [9, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /^\s*/gm, [[10, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]]]) - - describe "::characterIndexForPosition(position)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - 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 = TextBuffer.loadSync(filePath) - - 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 "::isEmpty()", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - 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 "::hasAstral()", -> - it "returns true for buffers containing surrogate pairs", -> - expect(new TextBuffer('hooray 😄').hasAstral()).toBeTruthy() - - it "returns false for buffers that do not contain surrogate pairs", -> - expect(new TextBuffer('nope').hasAstral()).toBeFalsy() - - describe "::onWillChange(callback)", -> - it "notifies observers before a transaction, an undo or a redo", -> - changeCount = 0 - expectedText = '' - - buffer = new TextBuffer() - checkpoint = buffer.createCheckpoint() - - buffer.onWillChange (change) -> - expect(buffer.getText()).toBe expectedText - changeCount++ - - buffer.append('a') - expect(changeCount).toBe(1) - expectedText = 'a' - - buffer.transact -> - buffer.append('b') - buffer.append('c') - expect(changeCount).toBe(2) - expectedText = 'abc' - - # Empty transactions do not cause onWillChange listeners to be called - buffer.transact -> - expect(changeCount).toBe(2) - - buffer.undo() - expect(changeCount).toBe(3) - expectedText = 'a' - - buffer.redo() - expect(changeCount).toBe(4) - expectedText = 'abc' - - buffer.revertToCheckpoint(checkpoint) - expect(changeCount).toBe(5) - - describe "::onDidChange(callback)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - it "notifies observers after a transaction, an undo or a redo", -> - textChanges = [] - buffer.onDidChange ({changes}) -> textChanges.push(changes...) - - buffer.insert([0, 0], "abc") - buffer.delete([[0, 0], [0, 1]]) - - assertChangesEqual(textChanges, [ - { - oldRange: [[0, 0], [0, 0]], - newRange: [[0, 0], [0, 3]] - oldText: "", - newText: "abc" - }, - { - oldRange: [[0, 0], [0, 1]], - newRange: [[0, 0], [0, 0]], - oldText: "a", - newText: "" - } - ]) - - textChanges = [] - buffer.transact -> - buffer.insert([1, 0], "v") - buffer.insert([1, 1], "x") - buffer.insert([1, 2], "y") - buffer.insert([2, 3], "zw") - buffer.delete([[2, 3], [2, 4]]) - - assertChangesEqual(textChanges, [ - { - oldRange: [[1, 0], [1, 0]], - newRange: [[1, 0], [1, 3]], - oldText: "", - newText: "vxy", - }, - { - oldRange: [[2, 3], [2, 3]], - newRange: [[2, 3], [2, 4]], - oldText: "", - newText: "w", - } - ]) - - textChanges = [] - buffer.undo() - assertChangesEqual(textChanges, [ - { - oldRange: [[1, 0], [1, 3]], - newRange: [[1, 0], [1, 0]], - oldText: "vxy", - newText: "", - }, - { - oldRange: [[2, 3], [2, 4]], - newRange: [[2, 3], [2, 3]], - oldText: "w", - newText: "", - } - ]) - - textChanges = [] - buffer.redo() - assertChangesEqual(textChanges, [ - { - oldRange: [[1, 0], [1, 0]], - newRange: [[1, 0], [1, 3]], - oldText: "", - newText: "vxy", - }, - { - oldRange: [[2, 3], [2, 3]], - newRange: [[2, 3], [2, 4]], - oldText: "", - newText: "w", - } - ]) - - textChanges = [] - buffer.transact -> - buffer.transact -> - buffer.insert([0, 0], "j") - - # we emit only one event for nested transactions - assertChangesEqual(textChanges, [ - { - oldRange: [[0, 0], [0, 0]], - newRange: [[0, 0], [0, 1]], - oldText: "", - newText: "j", - } - ]) - - it "doesn't notify observers after an empty transaction", -> - didChangeTextSpy = jasmine.createSpy() - buffer.onDidChange(didChangeTextSpy) - buffer.transact(->) - expect(didChangeTextSpy).not.toHaveBeenCalled() - - it "doesn't throw an error when clearing the undo stack within a transaction", -> - buffer.onDidChange(didChangeTextSpy = jasmine.createSpy()) - expect(-> buffer.transact(-> buffer.clearUndoStack())).not.toThrowError() - expect(didChangeTextSpy).not.toHaveBeenCalled() - - describe "::onDidStopChanging(callback)", -> - [delay, didStopChangingCallback] = [] - - wait = (milliseconds, callback) -> setTimeout(callback, milliseconds) - - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - delay = buffer.stoppedChangingDelay - didStopChangingCallback = jasmine.createSpy("didStopChangingCallback") - buffer.onDidStopChanging didStopChangingCallback - - it "notifies observers after a delay passes following changes", (done) -> - buffer.insert([0, 0], 'a') - expect(didStopChangingCallback).not.toHaveBeenCalled() - - wait delay / 2, -> - buffer.transact -> - buffer.transact -> - buffer.insert([0, 0], 'b') - buffer.insert([1, 0], 'c') - buffer.insert([1, 1], 'd') - expect(didStopChangingCallback).not.toHaveBeenCalled() - - wait delay / 2, -> - expect(didStopChangingCallback).not.toHaveBeenCalled() - - wait delay, -> - expect(didStopChangingCallback).toHaveBeenCalled() - assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ - { - oldRange: [[0, 0], [0, 0]], - newRange: [[0, 0], [0, 2]], - oldText: "", - newText: "ba", - }, - { - oldRange: [[1, 0], [1, 0]], - newRange: [[1, 0], [1, 2]], - oldText: "", - newText: "cd", - } - ]) - - didStopChangingCallback.calls.reset() - buffer.undo() - buffer.undo() - wait delay * 2, -> - expect(didStopChangingCallback).toHaveBeenCalled() - assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ - { - oldRange: [[0, 0], [0, 2]], - newRange: [[0, 0], [0, 0]], - oldText: "ba", - newText: "", - }, - { - oldRange: [[1, 0], [1, 2]], - newRange: [[1, 0], [1, 0]], - oldText: "cd", - newText: "", - }, - ]) - done() - - it "provides the correct changes when the buffer is mutated in the onDidChange callback", (done) -> - buffer.onDidChange ({changes}) -> - switch changes[0].newText - when 'a' - buffer.insert(changes[0].newRange.end, 'b') - when 'b' - buffer.insert(changes[0].newRange.end, 'c') - when 'c' - buffer.insert(changes[0].newRange.end, 'd') - - buffer.insert([0, 0], 'a') - - wait delay * 2, -> - expect(didStopChangingCallback).toHaveBeenCalled() - assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ - { - oldRange: [[0, 0], [0, 0]], - newRange: [[0, 0], [0, 4]], - oldText: "", - newText: "abcd", - } - ]) - done() - - describe "::append(text)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - 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 "::setLanguageMode", -> - it "destroys the previous language mode", -> - buffer = new TextBuffer() - - languageMode1 = { - alive: true, - destroy: -> @alive = false - onDidChangeHighlighting: -> {dispose: ->} - } - - languageMode2 = { - alive: true, - destroy: -> @alive = false - onDidChangeHighlighting: -> {dispose: ->} - } - - buffer.setLanguageMode(languageMode1) - expect(languageMode1.alive).toBe(true) - expect(languageMode2.alive).toBe(true) - - buffer.setLanguageMode(languageMode2) - expect(languageMode1.alive).toBe(false) - expect(languageMode2.alive).toBe(true) - - buffer.destroy() - expect(languageMode1.alive).toBe(false) - expect(languageMode2.alive).toBe(false) - - it "notifies ::onDidChangeLanguageMode observers when the language mode changes", -> - buffer = new TextBuffer() - expect(buffer.getLanguageMode() instanceof NullLanguageMode).toBe(true) - - events = [] - buffer.onDidChangeLanguageMode (newMode, oldMode) -> events.push({newMode: newMode, oldMode: oldMode}) - - languageMode = { - onDidChangeHighlighting: -> {dispose: ->} - } - - buffer.setLanguageMode(languageMode) - expect(buffer.getLanguageMode()).toBe(languageMode) - expect(events.length).toBe(1) - expect(events[0].newMode).toBe(languageMode) - expect(events[0].oldMode instanceof NullLanguageMode).toBe(true) - - buffer.setLanguageMode(null) - expect(buffer.getLanguageMode() instanceof NullLanguageMode).toBe(true) - expect(events.length).toBe(2) - expect(events[1].newMode).toBe(buffer.getLanguageMode()) - expect(events[1].oldMode).toBe(languageMode) - - describe "line ending support", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - 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" - - describe "::setPreferredLineEnding(lineEnding)", -> - it "uses the given line ending when normalizing, rather than inferring one from the surrounding text", -> - buffer = new TextBuffer(text: "a \r\n") - - expect(buffer.getPreferredLineEnding()).toBe null - buffer.append(" b \n") - expect(buffer.getText()).toBe "a \r\n b \r\n" - - buffer.setPreferredLineEnding("\n") - expect(buffer.getPreferredLineEnding()).toBe "\n" - buffer.append(" c \n") - expect(buffer.getText()).toBe "a \r\n b \r\n c \n" - - buffer.setPreferredLineEnding(null) - buffer.append(" d \r\n") - expect(buffer.getText()).toBe "a \r\n b \r\n c \n d \n" - - it "persists across serialization and deserialization", (done) -> - bufferA = new TextBuffer - bufferA.setPreferredLineEnding("\r\n") - - TextBuffer.deserialize(bufferA.serialize()).then (bufferB) -> - expect(bufferB.getPreferredLineEnding()).toBe "\r\n" - done() - -assertChangesEqual = (actualChanges, expectedChanges) -> - expect(actualChanges.length).toBe(expectedChanges.length) - for actualChange, i in actualChanges - expectedChange = expectedChanges[i] - expect(actualChange.oldRange).toEqual(expectedChange.oldRange) - expect(actualChange.newRange).toEqual(expectedChange.newRange) - expect(actualChange.oldText).toEqual(expectedChange.oldText) - expect(actualChange.newText).toEqual(expectedChange.newText) diff --git a/spec/text-buffer-spec.js b/spec/text-buffer-spec.js index 1e44cc05ac..9cea1764ea 100644 --- a/spec/text-buffer-spec.js +++ b/spec/text-buffer-spec.js @@ -1,32 +1,3259 @@ -const path = require('path') -const TextBuffer = require('../src/text-buffer') - -describe('when a buffer is already open', () => { - const filePath = path.join(__dirname, 'fixtures', 'sample.js') - const buffer = new TextBuffer() - - it('replaces foo( with bar( using /\bfoo\\(\b/gim', () => { - buffer.setPath(filePath) - buffer.setText('foo(x)') - buffer.replace(/\bfoo\(\b/gim, 'bar(') - - expect(buffer.getText()).toBe('bar(x)') - }) - - describe('Texts should be replaced properly with strings containing literals when using the regex option', () => { - it('replaces tstat_fvars()->curr_setpoint[HEAT_EN] with tstat_set_curr_setpoint($1, $2);', () => { - buffer.setPath(filePath) - buffer.setText('tstat_fvars()->curr_setpoint[HEAT_EN] = new_tptr->heat_limit;') - buffer.replace(/tstat_fvars\(\)->curr_setpoint\[(.+?)\] = (.+?);/, 'tstat_set_curr_setpoint($1, $2);') - - expect(buffer.getText()).toBe('tstat_set_curr_setpoint(HEAT_EN, new_tptr->heat_limit);') - }) - - it('replaces atom/flight-manualatomio with $1', () => { - buffer.setText('atom/flight-manualatomio') - buffer.replace(/\.(atom)\./, '$1') - - expect(buffer.getText()).toBe('atom/flight-manualatomio') - }) - }) -}) +const fs = require('fs-plus'); +const {join} = require('path'); +const temp = require('temp'); +const {File} = require('pathwatcher'); +const Random = require('random-seed'); +const Point = require('../src/point'); +const Range = require('../src/range'); +const DisplayLayer = require('../src/display-layer'); +const DefaultHistoryProvider = require('../src/default-history-provider'); +const TextBuffer = require('../src/text-buffer'); +const SampleText = fs.readFileSync(join(__dirname, 'fixtures', 'sample.js'), 'utf8'); +const {buildRandomLines, getRandomBufferRange} = require('./helpers/random'); +const NullLanguageMode = require('../src/null-language-mode'); + +describe("TextBuffer", function() { + let buffer = null; + + beforeEach(function() { + temp.track(); + jasmine.addCustomEqualityTester(require("underscore-plus").isEqual); + // When running specs in Atom, setTimeout is spied on by default. + return (typeof jasmine.useRealClock === 'function' ? jasmine.useRealClock() : undefined); + }); + + afterEach(function() { + if (buffer != null) { + buffer.destroy(); + } + return buffer = null; + }); + + describe("construction", function() { + it("can be constructed empty", function() { + buffer = new TextBuffer; + expect(buffer.getLineCount()).toBe(1); + expect(buffer.getText()).toBe(''); + expect(buffer.lineForRow(0)).toBe(''); + return expect(buffer.lineEndingForRow(0)).toBe(''); + }); + + it("can be constructed with initial text containing no trailing newline", function() { + const text = "hello\nworld\r\nhow are you doing?\r\nlast"; + buffer = new TextBuffer(text); + expect(buffer.getLineCount()).toBe(4); + expect(buffer.getText()).toBe(text); + expect(buffer.lineForRow(0)).toBe('hello'); + expect(buffer.lineEndingForRow(0)).toBe('\n'); + expect(buffer.lineForRow(1)).toBe('world'); + expect(buffer.lineEndingForRow(1)).toBe('\r\n'); + expect(buffer.lineForRow(2)).toBe('how are you doing?'); + expect(buffer.lineEndingForRow(2)).toBe('\r\n'); + expect(buffer.lineForRow(3)).toBe('last'); + return expect(buffer.lineEndingForRow(3)).toBe(''); + }); + + it("can be constructed with initial text containing a trailing newline", function() { + const text = "first\n"; + buffer = new TextBuffer(text); + expect(buffer.getLineCount()).toBe(2); + expect(buffer.getText()).toBe(text); + expect(buffer.lineForRow(0)).toBe('first'); + expect(buffer.lineEndingForRow(0)).toBe('\n'); + expect(buffer.lineForRow(1)).toBe(''); + return expect(buffer.lineEndingForRow(1)).toBe(''); + }); + + return it("automatically assigns a unique identifier to new buffers", function() { + const bufferIds = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(() => new TextBuffer().getId()); + const uniqueBufferIds = new Set(bufferIds); + + return expect(uniqueBufferIds.size).toBe(bufferIds.length); + }); + }); + + describe("::destroy()", () => it("clears the buffer's state", function(done) { + const filePath = temp.openSync('atom').path; + buffer = new TextBuffer(); + buffer.setPath(filePath); + buffer.append("a"); + buffer.append("b"); + buffer.destroy(); + + expect(buffer.getText()).toBe(''); + buffer.undo(); + expect(buffer.getText()).toBe(''); + return buffer.save().catch(function(error) { + expect(error.message).toMatch(/Can't save destroyed buffer/); + return done(); + }); + })); + + describe("::setTextInRange(range, text)", function() { + beforeEach(() => buffer = new TextBuffer("hello\nworld\r\nhow are you doing?")); + + it("can replace text on a single line with a standard newline", function() { + buffer.setTextInRange([[0, 2], [0, 4]], "y y"); + return expect(buffer.getText()).toEqual("hey yo\nworld\r\nhow are you doing?"); + }); + + it("can replace text on a single line with a carriage-return/newline", function() { + buffer.setTextInRange([[1, 3], [1, 5]], "ms"); + return expect(buffer.getText()).toEqual("hello\nworms\r\nhow are you doing?"); + }); + + it("can replace text in a region spanning multiple lines, ending on the last line", function() { + buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", {normalizeLineEndings: false}); + return expect(buffer.getText()).toEqual("hey there\r\ncat\nwhat are you doing?"); + }); + + it("can replace text in a region spanning multiple lines, ending with a carriage-return/newline", function() { + buffer.setTextInRange([[0, 2], [1, 3]], "y\nyou're o", {normalizeLineEndings: false}); + return expect(buffer.getText()).toEqual("hey\nyou're old\r\nhow are you doing?"); + }); + + describe("after a change", () => it("notifies, in order: the language mode, display layers, and display layer ::onDidChange observers with the relevant details", function() { + buffer = new TextBuffer("hello\nworld\r\nhow are you doing?"); + + const events = []; + const languageMode = { + bufferDidChange(e) { return events.push({source: 'language-mode', event: e}); }, + bufferDidFinishTransaction() {}, + onDidChangeHighlighting() { return {dispose() {}}; } + }; + const displayLayer1 = buffer.addDisplayLayer(); + const displayLayer2 = buffer.addDisplayLayer(); + spyOn(displayLayer1, 'bufferDidChange').and.callFake(function(e) { + events.push({source: 'display-layer-1', event: e}); + return DisplayLayer.prototype.bufferDidChange.call(displayLayer1, e); + }); + spyOn(displayLayer2, 'bufferDidChange').and.callFake(function(e) { + events.push({source: 'display-layer-2', event: e}); + return DisplayLayer.prototype.bufferDidChange.call(displayLayer2, e); + }); + buffer.setLanguageMode(languageMode); + buffer.onDidChange(e => events.push({source: 'buffer', event: JSON.parse(JSON.stringify(e))})); + displayLayer1.onDidChange(e => events.push({source: 'display-layer-event', event: e})); + + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", {normalizeLineEndings: false}); + return buffer.setTextInRange([[1, 1], [1, 2]], "abc", {normalizeLineEndings: false}); + }); + + const changeEvent1 = { + oldRange: [[0, 2], [2, 3]], newRange: [[0, 2], [2, 4]], + oldText: "llo\nworld\r\nhow", newText: "y there\r\ncat\nwhat", + }; + const changeEvent2 = { + oldRange: [[1, 1], [1, 2]], newRange: [[1, 1], [1, 4]], + oldText: "a", newText: "abc", + }; + return expect(events).toEqual([ + {source: 'language-mode', event: changeEvent1}, + {source: 'display-layer-1', event: changeEvent1}, + {source: 'display-layer-2', event: changeEvent1}, + + {source: 'language-mode', event: changeEvent2}, + {source: 'display-layer-1', event: changeEvent2}, + {source: 'display-layer-2', event: changeEvent2}, + + { + source: 'buffer', + event: { + oldRange: Range(Point(0, 2), Point(2, 3)), + newRange: Range(Point(0, 2), Point(2, 4)), + changes: [ + { + oldRange: Range(Point(0, 2), Point(2, 3)), + newRange: Range(Point(0, 2), Point(2, 4)), + oldText: "llo\nworld\r\nhow", + newText: "y there\r\ncabct\nwhat" + } + ] + } + }, + { + source: 'display-layer-event', + event: [{ + oldRange: Range(Point(0, 0), Point(3, 0)), + newRange: Range(Point(0, 0), Point(3, 0)) + }] + } + ]); + })); + + it("returns the newRange of the change", () => expect(buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat"), {normalizeLineEndings: false}).toEqual([[0, 2], [2, 4]])); + + it("clips the given range", function() { + buffer.setTextInRange([[-1, -1], [0, 1]], "y"); + buffer.setTextInRange([[0, 10], [0, 100]], "w"); + return expect(buffer.lineForRow(0)).toBe("yellow"); + }); + + it("preserves the line endings of existing lines", function() { + buffer.setTextInRange([[0, 1], [0, 2]], 'o'); + expect(buffer.lineEndingForRow(0)).toBe('\n'); + buffer.setTextInRange([[1, 1], [1, 3]], 'i'); + return expect(buffer.lineEndingForRow(1)).toBe('\r\n'); + }); + + it("freezes change event ranges", function() { + let changedOldRange = null; + let changedNewRange = null; + buffer.onDidChange(function({oldRange, newRange}) { + oldRange.start = Point(0, 3); + oldRange.start.row = 1; + newRange.start = Point(4, 4); + newRange.end.row = 2; + changedOldRange = oldRange; + return changedNewRange = newRange; + }); + + buffer.setTextInRange(Range(Point(0, 2), Point(0, 4)), "y y"); + + expect(changedOldRange).toEqual([[0, 2], [0, 4]]); + return expect(changedNewRange).toEqual([[0, 2], [0, 5]]); + }); + + describe("when the undo option is 'skip'", function() { + it("replaces the contents of the buffer with the given text", function() { + buffer.setTextInRange([[0, 0], [0, 1]], "y"); + buffer.setTextInRange([[0, 10], [0, 100]], "w", {undo: 'skip'}); + expect(buffer.lineForRow(0)).toBe("yellow"); + + expect(buffer.undo()).toBe(true); + return expect(buffer.lineForRow(0)).toBe("hello"); + }); + + it("still emits marker change events (regression)", function() { + const markerLayer = buffer.addMarkerLayer(); + const marker = markerLayer.markRange([[0, 0], [0, 3]]); + + let markerLayerUpdateEventsCount = 0; + const markerChangeEvents = []; + markerLayer.onDidUpdate(() => markerLayerUpdateEventsCount++); + marker.onDidChange(event => markerChangeEvents.push(event)); + + buffer.setTextInRange([[0, 0], [0, 1]], '', {undo: 'skip'}); + expect(markerLayerUpdateEventsCount).toBe(1); + expect(markerChangeEvents).toEqual([{ + wasValid: true, isValid: true, + hadTail: true, hasTail: true, + oldProperties: {}, newProperties: {}, + oldHeadPosition: Point(0, 3), newHeadPosition: Point(0, 2), + oldTailPosition: Point(0, 0), newTailPosition: Point(0, 0), + textChanged: true + }]); + markerChangeEvents.length = 0; + + buffer.transact(() => buffer.setTextInRange([[0, 0], [0, 1]], '', {undo: 'skip'})); + expect(markerLayerUpdateEventsCount).toBe(2); + return expect(markerChangeEvents).toEqual([{ + wasValid: true, isValid: true, + hadTail: true, hasTail: true, + oldProperties: {}, newProperties: {}, + oldHeadPosition: Point(0, 2), newHeadPosition: Point(0, 1), + oldTailPosition: Point(0, 0), newTailPosition: Point(0, 0), + textChanged: true + }]); + }); + + return it("still emits text change events (regression)", function(done) { + const didChangeEvents = []; + buffer.onDidChange(event => didChangeEvents.push(event)); + + buffer.onDidStopChanging(function({changes}) { + assertChangesEqual(changes, [{ + oldRange: [[0, 0], [0, 1]], + newRange: [[0, 0], [0, 1]], + oldText: 'h', + newText: 'z' + }]); + return done(); + }); + + buffer.setTextInRange([[0, 0], [0, 1]], 'y', {undo: 'skip'}); + expect(didChangeEvents.length).toBe(1); + assertChangesEqual(didChangeEvents[0].changes, [{ + oldRange: [[0, 0], [0, 1]], + newRange: [[0, 0], [0, 1]], + oldText: 'h', + newText: 'y' + }]); + + buffer.transact(() => buffer.setTextInRange([[0, 0], [0, 1]], 'z', {undo: 'skip'})); + expect(didChangeEvents.length).toBe(2); + return assertChangesEqual(didChangeEvents[1].changes, [{ + oldRange: [[0, 0], [0, 1]], + newRange: [[0, 0], [0, 1]], + oldText: 'y', + newText: 'z' + }]); + }); + }); + + describe("when the normalizeLineEndings argument is true (the default)", function() { + describe("when the range's start row has a line ending", () => it("normalizes inserted line endings to match the line ending of the range's start row", function() { + const changeEvents = []; + buffer.onDidChange(e => changeEvents.push(e)); + + expect(buffer.lineEndingForRow(0)).toBe('\n'); + buffer.setTextInRange([[0, 2], [0, 5]], "y\r\nthere\r\ncrazy"); + expect(buffer.lineEndingForRow(0)).toBe('\n'); + expect(buffer.lineEndingForRow(1)).toBe('\n'); + expect(buffer.lineEndingForRow(2)).toBe('\n'); + expect(changeEvents[0].newText).toBe("y\nthere\ncrazy"); + + expect(buffer.lineEndingForRow(3)).toBe('\r\n'); + buffer.setTextInRange([[3, 3], [4, Infinity]], "ms\ndo you\r\nlike\ndirt"); + expect(buffer.lineEndingForRow(3)).toBe('\r\n'); + expect(buffer.lineEndingForRow(4)).toBe('\r\n'); + expect(buffer.lineEndingForRow(5)).toBe('\r\n'); + expect(buffer.lineEndingForRow(6)).toBe(''); + expect(changeEvents[1].newText).toBe("ms\r\ndo you\r\nlike\r\ndirt"); + + buffer.setTextInRange([[5, 1], [5, 3]], '\r'); + expect(changeEvents[2].changes).toEqual([{ + oldRange: [[5, 1], [5, 3]], + newRange: [[5, 1], [6, 0]], + oldText: 'ik', + newText: '\r\n' + }]); + + buffer.undo(); + expect(changeEvents[3].changes).toEqual([{ + oldRange: [[5, 1], [6, 0]], + newRange: [[5, 1], [5, 3]], + oldText: '\r\n', + newText: 'ik' + }]); + + buffer.redo(); + return expect(changeEvents[4].changes).toEqual([{ + oldRange: [[5, 1], [5, 3]], + newRange: [[5, 1], [6, 0]], + oldText: 'ik', + newText: '\r\n' + }]); + })); + + return describe("when the range's start row has no line ending (because it's the last line of the buffer)", function() { + describe("when the buffer contains no newlines", () => it("honors the newlines in the inserted text", function() { + buffer = new TextBuffer("hello"); + buffer.setTextInRange([[0, 2], [0, Infinity]], "hey\r\nthere\nworld"); + expect(buffer.lineEndingForRow(0)).toBe('\r\n'); + expect(buffer.lineEndingForRow(1)).toBe('\n'); + return expect(buffer.lineEndingForRow(2)).toBe(''); + })); + + return describe("when the buffer contains newlines", () => it("normalizes inserted line endings to match the line ending of the penultimate row", function() { + expect(buffer.lineEndingForRow(1)).toBe('\r\n'); + buffer.setTextInRange([[2, 0], [2, Infinity]], "what\ndo\r\nyou\nwant?"); + expect(buffer.lineEndingForRow(2)).toBe('\r\n'); + expect(buffer.lineEndingForRow(3)).toBe('\r\n'); + expect(buffer.lineEndingForRow(4)).toBe('\r\n'); + return expect(buffer.lineEndingForRow(5)).toBe(''); + })); + }); + }); + + return describe("when the normalizeLineEndings argument is false", () => it("honors the newlines in the inserted text", function() { + buffer.setTextInRange([[1, 0], [1, 5]], "moon\norbiting\r\nhappily\nthere", {normalizeLineEndings: false}); + expect(buffer.lineEndingForRow(1)).toBe('\n'); + expect(buffer.lineEndingForRow(2)).toBe('\r\n'); + expect(buffer.lineEndingForRow(3)).toBe('\n'); + expect(buffer.lineEndingForRow(4)).toBe('\r\n'); + return expect(buffer.lineEndingForRow(5)).toBe(''); + })); + }); + + describe("::setText(text)", () => it("replaces the contents of the buffer with the given text", function() { + buffer = new TextBuffer("hello\nworld\r\nyou are cool"); + buffer.setText("goodnight\r\nmoon\nit's been good"); + expect(buffer.getText()).toBe("goodnight\r\nmoon\nit's been good"); + buffer.undo(); + return expect(buffer.getText()).toBe("hello\nworld\r\nyou are cool"); + })); + + describe("::insert(position, text, normalizeNewlinesn)", function() { + it("inserts text at the given position", function() { + buffer = new TextBuffer("hello world"); + buffer.insert([0, 5], " there"); + return expect(buffer.getText()).toBe("hello there world"); + }); + + return it("honors the normalizeNewlines option", function() { + buffer = new TextBuffer("hello\nworld"); + buffer.insert([0, 5], "\r\nthere\r\nlittle", {normalizeLineEndings: false}); + return expect(buffer.getText()).toBe("hello\r\nthere\r\nlittle\nworld"); + }); + }); + + describe("::append(text, normalizeNewlines)", function() { + it("appends text to the end of the buffer", function() { + buffer = new TextBuffer("hello world"); + buffer.append(", how are you?"); + return expect(buffer.getText()).toBe("hello world, how are you?"); + }); + + return it("honors the normalizeNewlines option", function() { + buffer = new TextBuffer("hello\nworld"); + buffer.append("\r\nhow\r\nare\nyou?", {normalizeLineEndings: false}); + return expect(buffer.getText()).toBe("hello\nworld\r\nhow\r\nare\nyou?"); + }); + }); + + describe("::delete(range)", () => it("deletes text in the given range", function() { + buffer = new TextBuffer("hello world"); + buffer.delete([[0, 5], [0, 11]]); + return expect(buffer.getText()).toBe("hello"); + })); + + describe("::deleteRows(startRow, endRow)", function() { + beforeEach(() => buffer = new TextBuffer("first\nsecond\nthird\nlast")); + + describe("when the endRow is less than the last row of the buffer", () => it("deletes the specified rows", function() { + buffer.deleteRows(1, 2); + expect(buffer.getText()).toBe("first\nlast"); + buffer.deleteRows(0, 0); + return expect(buffer.getText()).toBe("last"); + })); + + describe("when the endRow is the last row of the buffer", () => it("deletes the specified rows", function() { + buffer.deleteRows(2, 3); + expect(buffer.getText()).toBe("first\nsecond"); + buffer.deleteRows(0, 1); + return expect(buffer.getText()).toBe(""); + })); + + it("clips the given row range", function() { + buffer.deleteRows(-1, 0); + expect(buffer.getText()).toBe("second\nthird\nlast"); + buffer.deleteRows(1, 5); + expect(buffer.getText()).toBe("second"); + + buffer.deleteRows(-2, -1); + expect(buffer.getText()).toBe("second"); + buffer.deleteRows(1, 2); + return expect(buffer.getText()).toBe("second"); + }); + + return it("handles out of order row ranges", function() { + buffer.deleteRows(2, 1); + return expect(buffer.getText()).toBe("first\nlast"); + }); + }); + + describe("::getText()", () => it("returns the contents of the buffer as a single string", function() { + buffer = new TextBuffer("hello\nworld\r\nhow are you?"); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you?"); + buffer.setTextInRange([[1, 0], [1, 5]], "mom"); + return expect(buffer.getText()).toBe("hello\nmom\r\nhow are you?"); + })); + + describe("::undo() and ::redo()", function() { + beforeEach(() => buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"})); + + it("undoes and redoes multiple changes", function() { + buffer.setTextInRange([[0, 5], [0, 5]], " there"); + buffer.setTextInRange([[1, 0], [1, 5]], "friend"); + expect(buffer.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + buffer.redo(); + expect(buffer.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + + buffer.redo(); + return expect(buffer.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + }); + + it("clears the redo stack upon a fresh change", function() { + buffer.setTextInRange([[0, 5], [0, 5]], " there"); + buffer.setTextInRange([[1, 0], [1, 5]], "friend"); + expect(buffer.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + + buffer.setTextInRange([[1, 3], [1, 5]], "m"); + expect(buffer.getText()).toBe("hello there\nworm\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello there\nworm\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + + buffer.undo(); + return expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + return it("does not allow the undo stack to grow without bound", function() { + buffer = new TextBuffer({maxUndoEntries: 12}); + + // Each transaction is treated as a single undo entry. We can undo up + // to 12 of them. + buffer.setText(""); + buffer.clearUndoStack(); + for (var i = 0; i < 13; i++) { + buffer.transact(function() { + buffer.append(String(i)); + return buffer.append("\n"); + }); + } + expect(buffer.getLineCount()).toBe(14); + + let undoCount = 0; + while (buffer.undo()) { undoCount++; } + expect(undoCount).toBe(12); + return expect(buffer.getText()).toBe('0\n'); + }); + }); + + describe("::createMarkerSnapshot", function() { + let markerLayers = null; + + beforeEach(function() { + buffer = new TextBuffer; + + return markerLayers = [ + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}), + buffer.addMarkerLayer({maintainHistory: true}), + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}), + buffer.addMarkerLayer({maintainHistory: true}) + ];}); + + describe("when selectionsMarkerLayer is not passed", () => it("takes a snapshot of all markerLayers", function() { + const snapshot = buffer.createMarkerSnapshot(); + const markerLayerIdsInSnapshot = Object.keys(snapshot); + expect(markerLayerIdsInSnapshot.length).toBe(4); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[0].id)).toBe(true); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[1].id)).toBe(true); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[2].id)).toBe(true); + return expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[3].id)).toBe(true); + })); + + return describe("when selectionsMarkerLayer is passed", () => it("skips snapshotting of other 'selection' role marker layers", function() { + let snapshot = buffer.createMarkerSnapshot(markerLayers[0]); + let markerLayerIdsInSnapshot = Object.keys(snapshot); + expect(markerLayerIdsInSnapshot.length).toBe(3); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[0].id)).toBe(true); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[1].id)).toBe(true); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[2].id)).toBe(false); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[3].id)).toBe(true); + + snapshot = buffer.createMarkerSnapshot(markerLayers[2]); + markerLayerIdsInSnapshot = Object.keys(snapshot); + expect(markerLayerIdsInSnapshot.length).toBe(3); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[0].id)).toBe(false); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[1].id)).toBe(true); + expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[2].id)).toBe(true); + return expect(Array.from(markerLayerIdsInSnapshot).includes(markerLayers[3].id)).toBe(true); + })); + }); + + describe("selective snapshotting and restoration on transact/undo/redo for selections marker layer", function() { + let [markerLayers, marker0, marker1, marker2, textUndo, textRedo, rangesBefore, rangesAfter] = Array.from([]); + const ensureMarkerLayer = function(markerLayer, range) { + const markers = markerLayer.findMarkers({}); + expect(markers.length).toBe(1); + return expect(markers[0].getRange()).toEqual(range); + }; + + const getFirstMarker = markerLayer => markerLayer.findMarkers({})[0]; + + beforeEach(function() { + buffer = new TextBuffer({text: "00000000\n11111111\n22222222\n33333333\n"}); + + markerLayers = [ + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}), + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}), + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}) + ]; + + textUndo = "00000000\n11111111\n22222222\n33333333\n"; + textRedo = "00000000\n11111111\n22222222\n33333333\n44444444\n"; + + rangesBefore = [ + [[0, 1], [0, 1]], + [[0, 2], [0, 2]], + [[0, 3], [0, 3]] + ]; + rangesAfter = [ + [[2, 1], [2, 1]], + [[2, 2], [2, 2]], + [[2, 3], [2, 3]] + ]; + + marker0 = markerLayers[0].markRange(rangesBefore[0]); + marker1 = markerLayers[1].markRange(rangesBefore[1]); + return marker2 = markerLayers[2].markRange(rangesBefore[2]); + }); + + it("restores a snapshot from other selections marker layers on undo/redo", function() { + // Snapshot is taken for markerLayers[0] only, markerLayer[1] and markerLayer[2] are skipped + buffer.transact({selectionsMarkerLayer: markerLayers[0]}, function() { + buffer.append("44444444\n"); + marker0.setRange(rangesAfter[0]); + marker1.setRange(rangesAfter[1]); + return marker2.setRange(rangesAfter[2]); + }); + + buffer.undo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesBefore[0]); + ensureMarkerLayer(markerLayers[1], rangesAfter[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + + buffer.redo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textRedo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesAfter[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + + buffer.undo({selectionsMarkerLayer: markerLayers[1]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesBefore[0]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).not.toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + expect(marker1.isDestroyed()).toBe(true); + + buffer.redo({selectionsMarkerLayer: markerLayers[2]}); + expect(buffer.getText()).toBe(textRedo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesBefore[0]); + ensureMarkerLayer(markerLayers[2], rangesAfter[0]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).not.toBe(marker1); + expect(getFirstMarker(markerLayers[2])).not.toBe(marker2); + expect(marker1.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(true); + + buffer.undo({selectionsMarkerLayer: markerLayers[2]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesBefore[0]); + ensureMarkerLayer(markerLayers[2], rangesBefore[0]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).not.toBe(marker1); + expect(getFirstMarker(markerLayers[2])).not.toBe(marker2); + expect(marker1.isDestroyed()).toBe(true); + return expect(marker2.isDestroyed()).toBe(true); + }); + + it("can restore a snapshot taken at a destroyed selections marker layer given selectionsMarkerLayer", function() { + buffer.transact({selectionsMarkerLayer: markerLayers[1]}, function() { + buffer.append("44444444\n"); + marker0.setRange(rangesAfter[0]); + marker1.setRange(rangesAfter[1]); + return marker2.setRange(rangesAfter[2]); + }); + + markerLayers[1].destroy(); + expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeTruthy(); + expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy(); + expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeTruthy(); + expect(marker0.isDestroyed()).toBe(false); + expect(marker1.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(false); + + buffer.undo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesBefore[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(marker0.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(false); + + buffer.redo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textRedo); + ensureMarkerLayer(markerLayers[0], rangesAfter[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + + markerLayers[3] = markerLayers[2].copy(); + ensureMarkerLayer(markerLayers[3], rangesAfter[2]); + markerLayers[0].destroy(); + markerLayers[2].destroy(); + expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeFalsy(); + expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy(); + expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeFalsy(); + expect(buffer.getMarkerLayer(markerLayers[3].id)).toBeTruthy(); + + buffer.undo({selectionsMarkerLayer: markerLayers[3]}); + expect(buffer.getText()).toBe(textUndo); + ensureMarkerLayer(markerLayers[3], rangesBefore[1]); + buffer.redo({selectionsMarkerLayer: markerLayers[3]}); + expect(buffer.getText()).toBe(textRedo); + return ensureMarkerLayer(markerLayers[3], rangesAfter[1]); + }); + + it("falls back to normal behavior when the snaphot includes multiple layerSnapshots of selections marker layers", function() { + // Transact without selectionsMarkerLayer. + // Taken snapshot includes layerSnapshot of markerLayer[0], markerLayer[1] and markerLayer[2] + buffer.transact(function() { + buffer.append("44444444\n"); + marker0.setRange(rangesAfter[0]); + marker1.setRange(rangesAfter[1]); + return marker2.setRange(rangesAfter[2]); + }); + + buffer.undo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesBefore[0]); + ensureMarkerLayer(markerLayers[1], rangesBefore[1]); + ensureMarkerLayer(markerLayers[2], rangesBefore[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + + buffer.redo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textRedo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesAfter[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).toBe(marker1); + return expect(getFirstMarker(markerLayers[2])).toBe(marker2); + }); + + return describe("selections marker layer's selective snapshotting on createCheckpoint, groupChangesSinceCheckpoint", () => it("skips snapshotting of other marker layers with the same role as the selectionsMarkerLayer", function() { + const eventHandler = jasmine.createSpy('eventHandler'); + + const args = []; + spyOn(buffer, 'createMarkerSnapshot').and.callFake(arg => args.push(arg)); + + const checkpoint1 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[0]}); + const checkpoint2 = buffer.createCheckpoint(); + const checkpoint3 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[2]}); + const checkpoint4 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[1]}); + expect(args).toEqual([ + markerLayers[0], + undefined, + markerLayers[2], + markerLayers[1], + ]); + + buffer.groupChangesSinceCheckpoint(checkpoint4, {selectionsMarkerLayer: markerLayers[0]}); + buffer.groupChangesSinceCheckpoint(checkpoint3, {selectionsMarkerLayer: markerLayers[2]}); + buffer.groupChangesSinceCheckpoint(checkpoint2); + buffer.groupChangesSinceCheckpoint(checkpoint1, {selectionsMarkerLayer: markerLayers[1]}); + return expect(args).toEqual([ + markerLayers[0], + undefined, + markerLayers[2], + markerLayers[1], + + markerLayers[0], + markerLayers[2], + undefined, + markerLayers[1], + ]); + })); + }); + + describe("transactions", function() { + let now = null; + + beforeEach(function() { + now = 0; + spyOn(Date, 'now').and.callFake(() => now); + + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + return buffer.setTextInRange([[1, 3], [1, 5]], 'ms'); + }); + + return describe("::transact(groupingInterval, fn)", function() { + it("groups all operations in the given function in a single transaction", function() { + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + return buffer.transact(() => buffer.setTextInRange([[2, 13], [2, 14]], "igg")); + }); + + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?"); + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + buffer.undo(); + return expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("halts execution of the function if the transaction is aborted", function() { + let innerContinued = false; + let outerContinued = false; + + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + buffer.transact(function() { + buffer.setTextInRange([[2, 13], [2, 14]], "igg"); + buffer.abortTransaction(); + return innerContinued = true; + }); + return outerContinued = true; + }); + + expect(innerContinued).toBe(false); + expect(outerContinued).toBe(true); + return expect(buffer.getText()).toBe("hey\nworms\r\nhow are you doing?"); + }); + + it("groups all operations performed within the given function into a single undo/redo operation", function() { + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + return buffer.setTextInRange([[2, 13], [2, 14]], "igg"); + }); + + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?"); + + // subsequent changes are not included in the transaction + buffer.setTextInRange([[1, 0], [1, 0]], "little "); + buffer.undo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?"); + + // this should undo all changes in the transaction + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + // previous changes are not included in the transaction + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + // this should redo all changes in the transaction + buffer.redo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?"); + + // this should redo the change following the transaction + buffer.redo(); + return expect(buffer.getText()).toBe("hey\nlittle worms\r\nhow are you digging?"); + }); + + it("does not push the transaction to the undo stack if it is empty", function() { + buffer.transact(function() {}); + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + buffer.transact(() => buffer.abortTransaction()); + buffer.undo(); + return expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("halts execution undoes all operations since the beginning of the transaction if ::abortTransaction() is called", function() { + let continuedPastAbort = false; + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + buffer.setTextInRange([[2, 13], [2, 14]], "igg"); + buffer.abortTransaction(); + return continuedPastAbort = true; + }); + + expect(continuedPastAbort).toBe(false); + + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.redo(); + return expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + }); + + it("preserves the redo stack until a content change occurs", function() { + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + // no changes occur in this transaction before aborting + buffer.transact(function() { + buffer.markRange([[0, 0], [0, 5]]); + buffer.abortTransaction(); + return buffer.setTextInRange([[0, 0], [0, 5]], "hey"); + }); + + buffer.redo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.transact(function() { + buffer.setTextInRange([[0, 0], [0, 5]], "hey"); + return buffer.abortTransaction(); + }); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + return expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("allows nested transactions", function() { + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + buffer.transact(function() { + buffer.setTextInRange([[2, 13], [2, 14]], "igg"); + return buffer.setTextInRange([[2, 18], [2, 19]], "'"); + }); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you diggin'?"); + buffer.undo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you doing?"); + buffer.redo(); + return expect(buffer.getText()).toBe("hey\nworms\r\nhow are you diggin'?"); + }); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you diggin'?"); + + buffer.undo(); + buffer.undo(); + return expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("groups adjacent transactions within each other's grouping intervals", function() { + now += 1000; + buffer.transact(101, () => buffer.setTextInRange([[0, 2], [0, 5]], "y")); + + now += 100; + buffer.transact(201, () => buffer.setTextInRange([[0, 3], [0, 3]], "yy")); + + now += 200; + buffer.transact(201, () => buffer.setTextInRange([[0, 5], [0, 5]], "yy")); + + // not grouped because the previous transaction's grouping interval + // is only 200ms and we've advanced 300ms + now += 300; + buffer.transact(301, () => buffer.setTextInRange([[0, 7], [0, 7]], "!!")); + + expect(buffer.getText()).toBe("heyyyyy!!\nworms\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("heyyyyy\nworms\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("heyyyyy\nworms\r\nhow are you doing?"); + + buffer.redo(); + return expect(buffer.getText()).toBe("heyyyyy!!\nworms\r\nhow are you doing?"); + }); + + it("allows undo/redo within transactions, but not beyond the start of the containing transaction", function() { + buffer.setText(""); + buffer.markPosition([0, 0]); + + buffer.append("a"); + + buffer.transact(function() { + buffer.append("b"); + buffer.transact(() => buffer.append("c")); + buffer.append("d"); + + expect(buffer.undo()).toBe(true); + expect(buffer.getText()).toBe("abc"); + + expect(buffer.undo()).toBe(true); + expect(buffer.getText()).toBe("ab"); + + expect(buffer.undo()).toBe(true); + expect(buffer.getText()).toBe("a"); + + expect(buffer.undo()).toBe(false); + expect(buffer.getText()).toBe("a"); + + expect(buffer.redo()).toBe(true); + expect(buffer.getText()).toBe("ab"); + + expect(buffer.redo()).toBe(true); + expect(buffer.getText()).toBe("abc"); + + expect(buffer.redo()).toBe(true); + expect(buffer.getText()).toBe("abcd"); + + expect(buffer.redo()).toBe(false); + return expect(buffer.getText()).toBe("abcd"); + }); + + expect(buffer.undo()).toBe(true); + return expect(buffer.getText()).toBe("a"); + }); + + return it("does not error if the buffer is destroyed in a change callback within the transaction", function() { + buffer.onDidChange(() => buffer.destroy()); + const result = buffer.transact(function() { + buffer.append('!'); + return 'hi'; + }); + return expect(result).toBe('hi'); + }); + }); + }); + + describe("checkpoints", function() { + beforeEach(() => buffer = new TextBuffer); + + describe("::getChangesSinceCheckpoint(checkpoint)", function() { + it("returns a list of changes that have been made since the checkpoint", function() { + buffer.setText('abc\ndef\nghi\njkl\n'); + buffer.append("mno\n"); + const checkpoint = buffer.createCheckpoint(); + buffer.transact(function() { + buffer.append('pqr\n'); + return buffer.append('stu\n'); + }); + buffer.append('vwx\n'); + buffer.setTextInRange([[1, 0], [1, 2]], 'yz'); + + expect(buffer.getText()).toBe('abc\nyzf\nghi\njkl\nmno\npqr\nstu\nvwx\n'); + return assertChangesEqual(buffer.getChangesSinceCheckpoint(checkpoint), [ + { + oldRange: [[1, 0], [1, 2]], + newRange: [[1, 0], [1, 2]], + oldText: "de", + newText: "yz", + }, + { + oldRange: [[5, 0], [5, 0]], + newRange: [[5, 0], [8, 0]], + oldText: "", + newText: "pqr\nstu\nvwx\n", + } + ]); + }); + + it("returns an empty list of changes when no change has been made since the checkpoint", function() { + const checkpoint = buffer.createCheckpoint(); + return expect(buffer.getChangesSinceCheckpoint(checkpoint)).toEqual([]); + }); + + return it("returns an empty list of changes when the checkpoint doesn't exist", function() { + buffer.transact(function() { + buffer.append('abc\n'); + return buffer.append('def\n'); + }); + buffer.append('ghi\n'); + return expect(buffer.getChangesSinceCheckpoint(-1)).toEqual([]); + }); + }); + + describe("::revertToCheckpoint(checkpoint)", () => it("undoes all changes following the checkpoint", function() { + buffer.append("hello"); + const checkpoint = buffer.createCheckpoint(); + + buffer.transact(function() { + buffer.append("\n"); + return buffer.append("world"); + }); + + buffer.append("\n"); + buffer.append("how are you?"); + + const result = buffer.revertToCheckpoint(checkpoint); + expect(result).toBe(true); + expect(buffer.getText()).toBe("hello"); + + buffer.redo(); + return expect(buffer.getText()).toBe("hello"); + })); + + describe("::groupChangesSinceCheckpoint(checkpoint)", function() { + it("combines all changes since the checkpoint into a single transaction", function() { + const historyLayer = buffer.addMarkerLayer({maintainHistory: true}); + + buffer.append("one\n"); + const marker = historyLayer.markRange([[0, 1], [0, 2]]); + marker.setProperties({a: 'b'}); + + const checkpoint = buffer.createCheckpoint(); + buffer.append("two\n"); + buffer.transact(function() { + buffer.append("three\n"); + return buffer.append("four"); + }); + + marker.setRange([[0, 1], [2, 3]]); + marker.setProperties({a: 'c'}); + const result = buffer.groupChangesSinceCheckpoint(checkpoint); + + expect(result).toBeTruthy(); + expect(buffer.getText()).toBe(`\ +one +two +three +four\ +` + ); + expect(marker.getRange()).toEqual([[0, 1], [2, 3]]); + expect(marker.getProperties()).toEqual({a: 'c'}); + + buffer.undo(); + expect(buffer.getText()).toBe("one\n"); + expect(marker.getRange()).toEqual([[0, 1], [0, 2]]); + expect(marker.getProperties()).toEqual({a: 'b'}); + + buffer.redo(); + expect(buffer.getText()).toBe(`\ +one +two +three +four\ +` + ); + expect(marker.getRange()).toEqual([[0, 1], [2, 3]]); + return expect(marker.getProperties()).toEqual({a: 'c'}); + }); + + it("skips any later checkpoints when grouping changes", function() { + buffer.append("one\n"); + const checkpoint = buffer.createCheckpoint(); + buffer.append("two\n"); + const checkpoint2 = buffer.createCheckpoint(); + buffer.append("three"); + + buffer.groupChangesSinceCheckpoint(checkpoint); + expect(buffer.revertToCheckpoint(checkpoint2)).toBe(false); + + expect(buffer.getText()).toBe(`\ +one +two +three\ +` + ); + + buffer.undo(); + expect(buffer.getText()).toBe("one\n"); + + buffer.redo(); + return expect(buffer.getText()).toBe(`\ +one +two +three\ +` + ); + }); + + it("does nothing when no changes have been made since the checkpoint", function() { + buffer.append("one\n"); + const checkpoint = buffer.createCheckpoint(); + const result = buffer.groupChangesSinceCheckpoint(checkpoint); + expect(result).toBeTruthy(); + buffer.undo(); + return expect(buffer.getText()).toBe(""); + }); + + return it("returns false and does nothing when the checkpoint is not in the buffer's history", function() { + buffer.append("hello\n"); + const checkpoint = buffer.createCheckpoint(); + buffer.undo(); + buffer.append("world"); + const result = buffer.groupChangesSinceCheckpoint(checkpoint); + expect(result).toBeFalsy(); + buffer.undo(); + return expect(buffer.getText()).toBe(""); + }); + }); + + it("skips checkpoints when undoing", function() { + buffer.append("hello"); + buffer.createCheckpoint(); + buffer.createCheckpoint(); + buffer.createCheckpoint(); + buffer.undo(); + return expect(buffer.getText()).toBe(""); + }); + + it("preserves checkpoints across undo and redo", function() { + buffer.append("a"); + buffer.append("b"); + const checkpoint1 = buffer.createCheckpoint(); + buffer.append("c"); + const checkpoint2 = buffer.createCheckpoint(); + + buffer.undo(); + expect(buffer.getText()).toBe("ab"); + + buffer.redo(); + expect(buffer.getText()).toBe("abc"); + + buffer.append("d"); + + expect(buffer.revertToCheckpoint(checkpoint2)).toBe(true); + expect(buffer.getText()).toBe("abc"); + expect(buffer.revertToCheckpoint(checkpoint1)).toBe(true); + return expect(buffer.getText()).toBe("ab"); + }); + + it("handles checkpoints created when there have been no changes", function() { + const checkpoint = buffer.createCheckpoint(); + buffer.undo(); + buffer.append("hello"); + buffer.revertToCheckpoint(checkpoint); + return expect(buffer.getText()).toBe(""); + }); + + it("returns false when the checkpoint is not in the buffer's history", function() { + buffer.append("hello\n"); + const checkpoint = buffer.createCheckpoint(); + buffer.undo(); + buffer.append("world"); + expect(buffer.revertToCheckpoint(checkpoint)).toBe(false); + return expect(buffer.getText()).toBe("world"); + }); + + return it("does not allow changes based on checkpoints outside of the current transaction", function() { + const checkpoint = buffer.createCheckpoint(); + + buffer.append("a"); + + buffer.transact(function() { + expect(buffer.revertToCheckpoint(checkpoint)).toBe(false); + expect(buffer.getText()).toBe("a"); + + buffer.append("b"); + + return expect(buffer.groupChangesSinceCheckpoint(checkpoint)).toBeFalsy(); + }); + + buffer.undo(); + return expect(buffer.getText()).toBe("a"); + }); + }); + + describe("::groupLastChanges()", () => it("groups the last two changes into a single transaction", function() { + buffer = new TextBuffer(); + const layer = buffer.addMarkerLayer({maintainHistory: true}); + + buffer.append('a'); + + // Group two transactions, ensure before/after markers snapshots are preserved + const marker = layer.markPosition([0, 0]); + buffer.transact(() => buffer.append('b')); + buffer.createCheckpoint(); + buffer.transact(function() { + buffer.append('ccc'); + return marker.setHeadPosition([0, 2]); + }); + + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + expect(marker.getHeadPosition()).toEqual([0, 0]); + expect(buffer.getText()).toBe('a'); + buffer.redo(); + expect(marker.getHeadPosition()).toEqual([0, 2]); + buffer.undo(); + + // Group two bare changes + buffer.transact(function() { + buffer.append('b'); + buffer.createCheckpoint(); + buffer.append('c'); + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + return expect(buffer.getText()).toBe('a'); + }); + + // Group a transaction with a bare change + buffer.transact(function() { + buffer.transact(function() { + buffer.append('b'); + return buffer.append('c'); + }); + buffer.append('d'); + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + return expect(buffer.getText()).toBe('a'); + }); + + // Group a bare change with a transaction + buffer.transact(function() { + buffer.append('b'); + buffer.transact(function() { + buffer.append('c'); + return buffer.append('d'); + }); + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + return expect(buffer.getText()).toBe('a'); + }); + + // Can't group past the beginning of an open transaction + return buffer.transact(function() { + expect(buffer.groupLastChanges()).toBe(false); + buffer.append('b'); + expect(buffer.groupLastChanges()).toBe(false); + buffer.append('c'); + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + return expect(buffer.getText()).toBe('a'); + }); + })); + + describe("::setHistoryProvider(provider)", () => it("replaces the currently active history provider with the passed one", function() { + buffer = new TextBuffer({text: ''}); + buffer.insert([0, 0], 'Lorem '); + buffer.insert([0, 6], 'ipsum '); + expect(buffer.getText()).toBe('Lorem ipsum '); + + buffer.undo(); + expect(buffer.getText()).toBe('Lorem '); + + buffer.setHistoryProvider(new DefaultHistoryProvider(buffer)); + buffer.undo(); + expect(buffer.getText()).toBe('Lorem '); + + buffer.insert([0, 6], 'dolor '); + expect(buffer.getText()).toBe('Lorem dolor '); + + buffer.undo(); + return expect(buffer.getText()).toBe('Lorem '); + })); + + describe("::getHistory(maxEntries) and restoreDefaultHistoryProvider(history)", function() { + it("returns a base text and the state of the last `maxEntries` entries in the undo and redo stacks", function() { + buffer = new TextBuffer({text: ''}); + const markerLayer = buffer.addMarkerLayer({maintainHistory: true}); + + buffer.append('Lorem '); + buffer.append('ipsum '); + buffer.append('dolor '); + markerLayer.markPosition([0, 2]); + const markersSnapshotAtCheckpoint1 = buffer.createMarkerSnapshot(); + const checkpoint1 = buffer.createCheckpoint(); + buffer.append('sit '); + buffer.append('amet '); + buffer.append('consecteur '); + markerLayer.markPosition([0, 4]); + const markersSnapshotAtCheckpoint2 = buffer.createMarkerSnapshot(); + const checkpoint2 = buffer.createCheckpoint(); + buffer.append('adipiscit '); + buffer.append('elit '); + buffer.undo(); + buffer.undo(); + buffer.undo(); + + const history = buffer.getHistory(3); + expect(history.baseText).toBe('Lorem ipsum dolor '); + expect(history.nextCheckpointId).toBe(buffer.createCheckpoint()); + expect(history.undoStack).toEqual([ + { + type: 'checkpoint', + id: checkpoint1, + markers: markersSnapshotAtCheckpoint1 + }, + { + type: 'transaction', + changes: [{oldStart: Point(0, 18), oldEnd: Point(0, 18), newStart: Point(0, 18), newEnd: Point(0, 22), oldText: '', newText: 'sit '}], + markersBefore: markersSnapshotAtCheckpoint1, + markersAfter: markersSnapshotAtCheckpoint1 + }, + { + type: 'transaction', + changes: [{oldStart: Point(0, 22), oldEnd: Point(0, 22), newStart: Point(0, 22), newEnd: Point(0, 27), oldText: '', newText: 'amet '}], + markersBefore: markersSnapshotAtCheckpoint1, + markersAfter: markersSnapshotAtCheckpoint1 + } + ]); + expect(history.redoStack).toEqual([ + { + type: 'transaction', + changes: [{oldStart: Point(0, 38), oldEnd: Point(0, 38), newStart: Point(0, 38), newEnd: Point(0, 48), oldText: '', newText: 'adipiscit '}], + markersBefore: markersSnapshotAtCheckpoint2, + markersAfter: markersSnapshotAtCheckpoint2 + }, + { + type: 'checkpoint', + id: checkpoint2, + markers: markersSnapshotAtCheckpoint2 + }, + { + type: 'transaction', + changes: [{oldStart: Point(0, 27), oldEnd: Point(0, 27), newStart: Point(0, 27), newEnd: Point(0, 38), oldText: '', newText: 'consecteur '}], + markersBefore: markersSnapshotAtCheckpoint1, + markersAfter: markersSnapshotAtCheckpoint1 + } + ]); + + buffer.createCheckpoint(); + buffer.append('x'); + buffer.undo(); + buffer.clearUndoStack(); + + expect(buffer.getHistory()).not.toEqual(history); + buffer.restoreDefaultHistoryProvider(history); + return expect(buffer.getHistory()).toEqual(history); + }); + + return it("throws an error when called within a transaction", function() { + buffer = new TextBuffer(); + return expect(() => buffer.transact(() => buffer.getHistory(3))).toThrowError(); + }); + }); + + describe("::getTextInRange(range)", function() { + it("returns the text in a given range", function() { + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + expect(buffer.getTextInRange([[1, 1], [1, 4]])).toBe("orl"); + expect(buffer.getTextInRange([[0, 3], [2, 3]])).toBe("lo\nworld\r\nhow"); + return expect(buffer.getTextInRange([[0, 0], [2, 18]])).toBe(buffer.getText()); + }); + + return it("clips the given range", function() { + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + return expect(buffer.getTextInRange([[-100, -100], [100, 100]])).toBe(buffer.getText()); + }); + }); + + describe("::clipPosition(position)", function() { + it("returns a valid position closest to the given position", function() { + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + expect(buffer.clipPosition([-1, -1])).toEqual([0, 0]); + expect(buffer.clipPosition([-1, 2])).toEqual([0, 0]); + expect(buffer.clipPosition([0, -1])).toEqual([0, 0]); + expect(buffer.clipPosition([0, 20])).toEqual([0, 5]); + expect(buffer.clipPosition([1, -1])).toEqual([1, 0]); + expect(buffer.clipPosition([1, 20])).toEqual([1, 5]); + expect(buffer.clipPosition([10, 0])).toEqual([2, 18]); + return expect(buffer.clipPosition([Infinity, 0])).toEqual([2, 18]); + }); + + return it("throws an error when given an invalid point", function() { + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + expect(() => buffer.clipPosition([NaN, 1])) + .toThrowError("Invalid Point: (NaN, 1)"); + expect(() => buffer.clipPosition([0, NaN])) + .toThrowError("Invalid Point: (0, NaN)"); + return expect(() => buffer.clipPosition([0, {}])) + .toThrowError("Invalid Point: (0, [object Object])"); + }); + }); + + describe("::characterIndexForPosition(position)", function() { + beforeEach(() => buffer = new TextBuffer({text: "zero\none\r\ntwo\nthree"})); + + it("returns the absolute character offset for the given position", function() { + expect(buffer.characterIndexForPosition([0, 0])).toBe(0); + expect(buffer.characterIndexForPosition([0, 1])).toBe(1); + expect(buffer.characterIndexForPosition([0, 4])).toBe(4); + expect(buffer.characterIndexForPosition([1, 0])).toBe(5); + expect(buffer.characterIndexForPosition([1, 1])).toBe(6); + expect(buffer.characterIndexForPosition([1, 3])).toBe(8); + expect(buffer.characterIndexForPosition([2, 0])).toBe(10); + expect(buffer.characterIndexForPosition([2, 1])).toBe(11); + expect(buffer.characterIndexForPosition([3, 0])).toBe(14); + return expect(buffer.characterIndexForPosition([3, 5])).toBe(19); + }); + + return it("clips the given position before translating", function() { + expect(buffer.characterIndexForPosition([-1, -1])).toBe(0); + expect(buffer.characterIndexForPosition([1, 100])).toBe(8); + return expect(buffer.characterIndexForPosition([100, 100])).toBe(19); + }); + }); + + describe("::positionForCharacterIndex(offset)", function() { + beforeEach(() => buffer = new TextBuffer({text: "zero\none\r\ntwo\nthree"})); + + it("returns the position for the given absolute character offset", function() { + expect(buffer.positionForCharacterIndex(0)).toEqual([0, 0]); + expect(buffer.positionForCharacterIndex(1)).toEqual([0, 1]); + expect(buffer.positionForCharacterIndex(4)).toEqual([0, 4]); + expect(buffer.positionForCharacterIndex(5)).toEqual([1, 0]); + expect(buffer.positionForCharacterIndex(6)).toEqual([1, 1]); + expect(buffer.positionForCharacterIndex(8)).toEqual([1, 3]); + expect(buffer.positionForCharacterIndex(10)).toEqual([2, 0]); + expect(buffer.positionForCharacterIndex(11)).toEqual([2, 1]); + expect(buffer.positionForCharacterIndex(14)).toEqual([3, 0]); + return expect(buffer.positionForCharacterIndex(19)).toEqual([3, 5]); + }); + + return it("clips the given offset before translating", function() { + expect(buffer.positionForCharacterIndex(-1)).toEqual([0, 0]); + return expect(buffer.positionForCharacterIndex(20)).toEqual([3, 5]); + }); +}); + + describe("serialization", function() { + const expectSameMarkers = function(left, right) { + const markers1 = left.getMarkers().sort((a, b) => a.compare(b)); + const markers2 = right.getMarkers().sort((a, b) => a.compare(b)); + expect(markers1.length).toBe(markers2.length); + for (let i = 0; i < markers1.length; i++) { + const marker1 = markers1[i]; + expect(marker1).toEqual(markers2[i]); + } + }; + + it("can serialize / deserialize the buffer along with its history, marker layers, and display layers", function(done) { + const bufferA = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + const displayLayer1A = bufferA.addDisplayLayer(); + const displayLayer2A = bufferA.addDisplayLayer(); + displayLayer1A.foldBufferRange([[0, 1], [0, 3]]); + displayLayer2A.foldBufferRange([[0, 0], [0, 2]]); + bufferA.createCheckpoint(); + bufferA.setTextInRange([[0, 5], [0, 5]], " there"); + bufferA.transact(() => bufferA.setTextInRange([[1, 0], [1, 5]], "friend")); + const layerA = bufferA.addMarkerLayer({maintainHistory: true, persistent: true}); + layerA.markRange([[0, 6], [0, 8]], {reversed: true, foo: 1}); + const layerB = bufferA.addMarkerLayer({maintainHistory: true, persistent: true, role: "selections"}); + const marker2A = bufferA.markPosition([2, 2], {bar: 2}); + bufferA.transact(function() { + bufferA.setTextInRange([[1, 0], [1, 0]], "good "); + bufferA.append("?"); + return marker2A.setProperties({bar: 3, baz: 4}); + }); + layerA.markRange([[0, 4], [0, 5]], {invalidate: 'inside'}); + bufferA.setTextInRange([[0, 5], [0, 5]], "oo"); + bufferA.undo(); + + const state = JSON.parse(JSON.stringify(bufferA.serialize())); + return TextBuffer.deserialize(state).then(function(bufferB) { + expect(bufferB.getText()).toBe("hello there\ngood friend\r\nhow are you doing??"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + expect(bufferB.getDisplayLayer(displayLayer1A.id).foldsIntersectingBufferRange([[0, 1], [0, 3]]).length).toBe(1); + expect(bufferB.getDisplayLayer(displayLayer2A.id).foldsIntersectingBufferRange([[0, 0], [0, 2]]).length).toBe(1); + const displayLayer3B = bufferB.addDisplayLayer(); + expect(displayLayer3B.id).toBeGreaterThan(displayLayer1A.id); + expect(displayLayer3B.id).toBeGreaterThan(displayLayer2A.id); + + expect(bufferB.getMarkerLayer(layerB.id).getRole()).toBe("selections"); + expect(bufferB.selectionsMarkerLayerIds.has(layerB.id)).toBe(true); + expect(bufferB.selectionsMarkerLayerIds.size).toBe(1); + + bufferA.redo(); + bufferB.redo(); + expect(bufferB.getText()).toBe("hellooo there\ngood friend\r\nhow are you doing??"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + expect(bufferB.getMarkerLayer(layerA.id).maintainHistory).toBe(true); + expect(bufferB.getMarkerLayer(layerA.id).persistent).toBe(true); + + bufferA.undo(); + bufferB.undo(); + expect(bufferB.getText()).toBe("hello there\ngood friend\r\nhow are you doing??"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + + bufferA.undo(); + bufferB.undo(); + expect(bufferB.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + + bufferA.undo(); + bufferB.undo(); + expect(bufferB.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + + bufferA.undo(); + bufferB.undo(); + expect(bufferB.getText()).toBe("hello\nworld\r\nhow are you doing?"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + + // Accounts for deserialized markers when selecting the next marker's id + const marker3A = layerA.markRange([[0, 1], [2, 3]]); + const marker3B = bufferB.getMarkerLayer(layerA.id).markRange([[0, 1], [2, 3]]); + expect(marker3B.id).toBe(marker3A.id); + + // Doesn't try to reload the buffer since it has no file. + return setTimeout(function() { + expect(bufferB.getText()).toBe("hello\nworld\r\nhow are you doing?"); + return done(); + } + , 50); + }); + }); + + it("serializes / deserializes the buffer's persistent custom marker layers", function(done) { + const bufferA = new TextBuffer("abcdefghijklmnopqrstuvwxyz"); + + const layer1A = bufferA.addMarkerLayer(); + const layer2A = bufferA.addMarkerLayer({persistent: true}); + + layer1A.markRange([[0, 1], [0, 2]]); + layer1A.markRange([[0, 3], [0, 4]]); + + layer2A.markRange([[0, 5], [0, 6]]); + layer2A.markRange([[0, 7], [0, 8]]); + + return TextBuffer.deserialize(JSON.parse(JSON.stringify(bufferA.serialize()))).then(function(bufferB) { + const layer1B = bufferB.getMarkerLayer(layer1A.id); + const layer2B = bufferB.getMarkerLayer(layer2A.id); + expect(layer2B.persistent).toBe(true); + + expect(layer1B).toBe(undefined); + expectSameMarkers(layer2A, layer2B); + return done(); + }); + }); + + it("doesn't serialize the default marker layer", function(done) { + const bufferA = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + const markerLayerA = bufferA.getDefaultMarkerLayer(); + const marker1A = bufferA.markRange([[0, 1], [1, 2]], {foo: 1}); + + return TextBuffer.deserialize(bufferA.serialize()).then(function(bufferB) { + const markerLayerB = bufferB.getDefaultMarkerLayer(); + expect(bufferB.getMarker(marker1A.id)).toBeUndefined(); + return done(); + }); + }); + + it("doesn't attempt to serialize snapshots for destroyed marker layers", function() { + buffer = new TextBuffer({text: "abc"}); + const markerLayer = buffer.addMarkerLayer({maintainHistory: true, persistent: true}); + markerLayer.markPosition([0, 3]); + buffer.insert([0, 0], 'x'); + markerLayer.destroy(); + + return expect(() => buffer.serialize()).not.toThrowError(); + }); + + it("doesn't remember marker layers when calling serialize with {markerLayers: false}", function(done) { + const bufferA = new TextBuffer({text: "world"}); + const layerA = bufferA.addMarkerLayer({maintainHistory: true}); + const markerA = layerA.markPosition([0, 3]); + let markerB = null; + bufferA.transact(function() { + bufferA.insert([0, 0], 'hello '); + return markerB = layerA.markPosition([0, 5]); + }); + bufferA.undo(); + + return TextBuffer.deserialize(bufferA.serialize({markerLayers: false})).then(function(bufferB) { + expect(bufferB.getText()).toBe("world"); + expect(__guard__(bufferB.getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).toBeUndefined(); + expect(__guard__(bufferB.getMarkerLayer(layerA.id), x1 => x1.getMarker(markerB.id))).toBeUndefined(); + + bufferB.redo(); + expect(bufferB.getText()).toBe("hello world"); + expect(__guard__(bufferB.getMarkerLayer(layerA.id), x2 => x2.getMarker(markerA.id))).toBeUndefined(); + expect(__guard__(bufferB.getMarkerLayer(layerA.id), x3 => x3.getMarker(markerB.id))).toBeUndefined(); + + bufferB.undo(); + expect(bufferB.getText()).toBe("world"); + expect(__guard__(bufferB.getMarkerLayer(layerA.id), x4 => x4.getMarker(markerA.id))).toBeUndefined(); + expect(__guard__(bufferB.getMarkerLayer(layerA.id), x5 => x5.getMarker(markerB.id))).toBeUndefined(); + return done(); + }); + }); + + it("doesn't remember history when calling serialize with {history: false}", function(done) { + const bufferA = new TextBuffer({text: 'abc'}); + bufferA.append('def'); + bufferA.append('ghi'); + + return TextBuffer.deserialize(bufferA.serialize({history: false})).then(function(bufferB) { + expect(bufferB.getText()).toBe("abcdefghi"); + expect(bufferB.undo()).toBe(false); + expect(bufferB.getText()).toBe("abcdefghi"); + return done(); + }); + }); + + it("serializes / deserializes the buffer's unique identifier", function(done) { + const bufferA = new TextBuffer(); + return TextBuffer.deserialize(JSON.parse(JSON.stringify(bufferA.serialize()))).then(function(bufferB) { + expect(bufferB.getId()).toEqual(bufferA.getId()); + return done(); + }); + }); + + it("doesn't deserialize a state that was serialized with a different buffer version", function(done) { + const bufferA = new TextBuffer(); + const serializedBuffer = JSON.parse(JSON.stringify(bufferA.serialize())); + serializedBuffer.version = 123456789; + + return TextBuffer.deserialize(serializedBuffer).then(function(bufferB) { + expect(bufferB).toBeUndefined(); + return done(); + }); + }); + + it("doesn't deserialize a state referencing a file that no longer exists", function(done) { + const tempDir = fs.realpathSync(temp.mkdirSync('text-buffer')); + const filePath = join(tempDir, 'file.txt'); + fs.writeFileSync(filePath, "something\n"); + + const bufferA = TextBuffer.loadSync(filePath); + const state = bufferA.serialize(); + + fs.unlinkSync(filePath); + + state.mustExist = true; + return TextBuffer.deserialize(state).then( + () => expect('serialization succeeded with mustExist: true').toBeUndefined(), + err => expect(err.code).toBe('ENOENT')).then(done, done); + }); + + return describe("when the serialized buffer was unsaved and had no path", () => it("restores the previous unsaved state of the buffer", function(done) { + buffer = new TextBuffer(); + buffer.setText("abc"); + + return TextBuffer.deserialize(buffer.serialize()).then(function(buffer2) { + expect(buffer2.getPath()).toBeUndefined(); + expect(buffer2.getText()).toBe("abc"); + return done(); + }); + })); + }); + + describe("::getRange()", () => it("returns the range of the entire buffer text", function() { + buffer = new TextBuffer("abc\ndef\nghi"); + return expect(buffer.getRange()).toEqual([[0, 0], [2, 3]]); +})); + + describe("::getLength()", () => it("returns the lenght of the entire buffer text", function() { + buffer = new TextBuffer("abc\ndef\nghi"); + return expect(buffer.getLength()).toBe("abc\ndef\nghi".length); + })); + + describe("::rangeForRow(row, includeNewline)", function() { + beforeEach(() => buffer = new TextBuffer("this\nis a test\r\ntesting")); + + describe("if includeNewline is false (the default)", () => it("returns a range from the beginning of the line to the end of the line", function() { + expect(buffer.rangeForRow(0)).toEqual([[0, 0], [0, 4]]); + expect(buffer.rangeForRow(1)).toEqual([[1, 0], [1, 9]]); + return expect(buffer.rangeForRow(2)).toEqual([[2, 0], [2, 7]]); + })); + + describe("if includeNewline is true", () => it("returns a range from the beginning of the line to the beginning of the next (if it exists)", function() { + expect(buffer.rangeForRow(0, true)).toEqual([[0, 0], [1, 0]]); + expect(buffer.rangeForRow(1, true)).toEqual([[1, 0], [2, 0]]); + return expect(buffer.rangeForRow(2, true)).toEqual([[2, 0], [2, 7]]); + })); + + return describe("if the given row is out of range", () => it("returns the range of the nearest valid row", function() { + expect(buffer.rangeForRow(-1)).toEqual([[0, 0], [0, 4]]); + return expect(buffer.rangeForRow(10)).toEqual([[2, 0], [2, 7]]); + })); + }); + + describe("::onDidChangePath()", function() { + let [filePath, newPath, bufferToChange, eventHandler] = Array.from([]); + + beforeEach(function() { + const tempDir = fs.realpathSync(temp.mkdirSync('text-buffer')); + filePath = join(tempDir, "manipulate-me"); + newPath = `${filePath}-i-moved`; + fs.writeFileSync(filePath, ""); + return bufferToChange = TextBuffer.loadSync(filePath); + }); + + afterEach(function() { + bufferToChange.destroy(); + fs.removeSync(filePath); + return fs.removeSync(newPath); + }); + + it("notifies observers when the buffer is saved to a new path", function(done) { + bufferToChange.onDidChangePath(function(p) { + expect(p).toBe(newPath); + return done(); + }); + return bufferToChange.saveAs(newPath); + }); + + return it("notifies observers when the buffer's file is moved", function(done) { + // FIXME: This doesn't pass on Linux + if (['linux', 'win32'].includes(process.platform)) { + done(); + return; + } + + bufferToChange.onDidChangePath(function(p) { + expect(p).toBe(newPath); + return done(); + }); + + fs.removeSync(newPath); + return fs.moveSync(filePath, newPath); + }); + }); + + describe("::onWillThrowWatchError", () => it("notifies observers when the file has a watch error", function() { + const filePath = temp.openSync('atom').path; + fs.writeFileSync(filePath, ''); + + buffer = TextBuffer.loadSync(filePath); + + const eventHandler = jasmine.createSpy('eventHandler'); + buffer.onWillThrowWatchError(eventHandler); + + buffer.file.emitter.emit('will-throw-watch-error', 'arg'); + return expect(eventHandler).toHaveBeenCalledWith('arg'); + })); + + describe("::getLines()", () => it("returns an array of lines in the text contents", function() { + const filePath = require.resolve('./fixtures/sample.js'); + const fileContents = fs.readFileSync(filePath, 'utf8'); + buffer = TextBuffer.loadSync(filePath); + expect(buffer.getLines().length).toBe(fileContents.split("\n").length); + return expect(buffer.getLines().join('\n')).toBe(fileContents); + })); + + describe("::setTextInRange(range, string)", function() { + let changeHandler = null; + + beforeEach(function(done) { + const filePath = require.resolve('./fixtures/sample.js'); + const fileContents = fs.readFileSync(filePath, 'utf8'); + return TextBuffer.load(filePath).then(function(result) { + buffer = result; + changeHandler = jasmine.createSpy('changeHandler'); + buffer.onDidChange(changeHandler); + return done(); + }); + }); + + describe("when used to insert (called with an empty range and a non-empty string)", function() { + describe("when the given string has no newlines", () => it("inserts the string at the location of the given range", function() { + const range = [[3, 4], [3, 4]]; + buffer.setTextInRange(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(); + const [event] = Array.from(changeHandler.calls.allArgs()[0]); + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 4], [3, 7]]); + expect(event.oldText).toBe(""); + return expect(event.newText).toBe("foo"); + })); + + return describe("when the given string has newlines", () => it("inserts the lines at the location of the given range", function() { + const range = [[3, 4], [3, 4]]; + + buffer.setTextInRange(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(); + const [event] = Array.from(changeHandler.calls.allArgs()[0]); + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 4], [6, 3]]); + expect(event.oldText).toBe(""); + return expect(event.newText).toBe("foo\n\nbar\nbaz"); + })); + }); + + describe("when used to remove (called with a non-empty range and an empty string)", function() { + describe("when the range is contained within a single line", () => it("removes the characters within the range", function() { + const range = [[3, 4], [3, 7]]; + buffer.setTextInRange(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(); + const [event] = Array.from(changeHandler.calls.allArgs()[0]); + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 4], [3, 4]]); + expect(event.oldText).toBe("var"); + return expect(event.newText).toBe(""); + })); + + describe("when the range spans 2 lines", () => it("removes the characters within the range and joins the lines", function() { + const range = [[3, 16], [4, 4]]; + buffer.setTextInRange(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(); + const [event] = Array.from(changeHandler.calls.allArgs()[0]); + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 16], [3, 16]]); + expect(event.oldText).toBe("items.shift(), current, left = [], right = [];\n "); + return expect(event.newText).toBe(""); + })); + + return 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", function() { + buffer.setTextInRange([[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));"); + return 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", function() { + const range = [[3, 16], [11, 9]]; + const oldText = buffer.getTextInRange(range); + + buffer.setTextInRange(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(); + const [event] = Array.from(changeHandler.calls.allArgs()[0]); + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 16], [4, 3]]); + expect(event.oldText).toBe(oldText); + return expect(event.newText).toBe("foo\nbar"); + })); + + return it("allows a change to be undone safely from an ::onDidChange callback", function() { + buffer.onDidChange(() => buffer.undo()); + buffer.setTextInRange([[0, 0], [0, 0]], "hello"); + return expect(buffer.lineForRow(0)).toBe("var quicksort = function () {"); + }); + }); + + describe("::setText(text)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + describe("when the buffer contains newlines", () => it("changes the entire contents of the buffer and emits a change event", function() { + const lastRow = buffer.getLastRow(); + const expectedPreRange = [[0, 0], [lastRow, buffer.lineForRow(lastRow).length]]; + const changeHandler = jasmine.createSpy('changeHandler'); + buffer.onDidChange(changeHandler); + + const newText = "I know you are.\nBut what am I?"; + buffer.setText(newText); + + expect(buffer.getText()).toBe(newText); + expect(changeHandler).toHaveBeenCalled(); + + const [event] = Array.from(changeHandler.calls.allArgs()[0]); + expect(event.newText).toBe(newText); + expect(event.oldRange).toEqual(expectedPreRange); + return expect(event.newRange).toEqual([[0, 0], [1, 14]]); + })); + + return describe("with windows newlines", () => it("changes the entire contents of the buffer", function() { + buffer = new TextBuffer("first\r\nlast"); + const lastRow = buffer.getLastRow(); + const expectedPreRange = [[0, 0], [lastRow, buffer.lineForRow(lastRow).length]]; + const changeHandler = jasmine.createSpy('changeHandler'); + buffer.onDidChange(changeHandler); + + const newText = "new first\r\nnew last"; + buffer.setText(newText); + + expect(buffer.getText()).toBe(newText); + expect(changeHandler).toHaveBeenCalled(); + + const [event] = Array.from(changeHandler.calls.allArgs()[0]); + expect(event.newText).toBe(newText); + expect(event.oldRange).toEqual(expectedPreRange); + return expect(event.newRange).toEqual([[0, 0], [1, 8]]); + })); +}); + + describe("::setTextViaDiff(text)", function() { + beforeEach(function(done) { + const filePath = require.resolve('./fixtures/sample.js'); + return TextBuffer.load(filePath).then(function(result) { + buffer = result; + return done(); + }); + }); + + it("can change the entire contents of the buffer when there are no newlines", function() { + buffer.setText('BUFFER CHANGE'); + const newText = 'DISK CHANGE'; + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + it("can change a buffer that contains lone carriage returns", function() { + const oldText = 'one\rtwo\nthree\rfour\n'; + const newText = 'one\rtwo and\nthree\rfour\n'; + buffer.setText(oldText); + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + buffer.undo(); + return expect(buffer.getText()).toBe(oldText); + }); + + describe("with standard newlines", function() { + it("can change the entire contents of the buffer with no newline at the end", function() { + const newText = "I know you are.\nBut what am I?"; + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + it("can change the entire contents of the buffer with a newline at the end", function() { + const newText = "I know you are.\nBut what am I?\n"; + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + it("can change a few lines at the beginning in the buffer", function() { + const newText = buffer.getText().replace(/function/g, 'omgwow'); + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + it("can change a few lines in the middle of the buffer", function() { + const newText = buffer.getText().replace(/shift/g, 'omgwow'); + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + return it("can adds a newline at the end", function() { + const newText = buffer.getText() + '\n'; + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + }); + + return describe("with windows newlines", function() { + beforeEach(() => buffer.setText(buffer.getText().replace(/\n/g, '\r\n'))); + + it("adds a newline at the end", function() { + const newText = buffer.getText() + '\r\n'; + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + it("changes the entire contents of the buffer with smaller content with no newline at the end", function() { + const newText = "I know you are.\r\nBut what am I?"; + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + it("changes the entire contents of the buffer with smaller content with newline at the end", function() { + const newText = "I know you are.\r\nBut what am I?\r\n"; + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + it("changes a few lines at the beginning in the buffer", function() { + const newText = buffer.getText().replace(/function/g, 'omgwow'); + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + + return it("changes a few lines in the middle of the buffer", function() { + const newText = buffer.getText().replace(/shift/g, 'omgwow'); + buffer.setTextViaDiff(newText); + return expect(buffer.getText()).toBe(newText); + }); + }); + }); + + describe("::getTextInRange(range)", function() { + beforeEach(function(done) { + const filePath = require.resolve('./fixtures/sample.js'); + return TextBuffer.load(filePath).then(function(result) { + buffer = result; + return done(); + }); + }); + + describe("when range is empty", () => it("returns an empty string", function() { + const range = [[1, 1], [1, 1]]; + return expect(buffer.getTextInRange(range)).toBe(""); + })); + + describe("when range spans one line", () => it("returns characters in range", function() { + let range = [[2, 8], [2, 13]]; + expect(buffer.getTextInRange(range)).toBe("items"); + + const lineLength = buffer.lineForRow(2).length; + range = [[2, 0], [2, lineLength]]; + return expect(buffer.getTextInRange(range)).toBe(" if (items.length <= 1) return items;"); + })); + + describe("when range spans multiple lines", () => it("returns characters in range (including newlines)", function() { + let lineLength = buffer.lineForRow(2).length; + let 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]]; + return 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)))); + + return 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)", function() { + beforeEach(() => buffer = TextBuffer.loadSync(require.resolve('./fixtures/sample.js'))); + + it("calls the given function with the information about each match", function() { + const matches = []; + buffer.scan(/current/g, match => matches.push(match)); + expect(matches.length).toBe(5); + + expect(matches[0].matchText).toBe('current'); + expect(matches[0].range).toEqual([[3, 31], [3, 38]]); + expect(matches[0].lineText).toBe(' var pivot = items.shift(), current, left = [], right = [];'); + expect(matches[0].lineTextOffset).toBe(0); + expect(matches[0].leadingContextLines.length).toBe(0); + expect(matches[0].trailingContextLines.length).toBe(0); + + expect(matches[1].matchText).toBe('current'); + expect(matches[1].range).toEqual([[5, 6], [5, 13]]); + expect(matches[1].lineText).toBe(' current = items.shift();'); + expect(matches[1].lineTextOffset).toBe(0); + expect(matches[1].leadingContextLines.length).toBe(0); + return expect(matches[1].trailingContextLines.length).toBe(0); + }); + + return it("calls the given function with the information about each match including context lines", function() { + const matches = []; + buffer.scan(/current/g, {leadingContextLineCount: 1, trailingContextLineCount: 2}, match => matches.push(match)); + expect(matches.length).toBe(5); + + expect(matches[0].matchText).toBe('current'); + expect(matches[0].range).toEqual([[3, 31], [3, 38]]); + expect(matches[0].lineText).toBe(' var pivot = items.shift(), current, left = [], right = [];'); + expect(matches[0].lineTextOffset).toBe(0); + expect(matches[0].leadingContextLines.length).toBe(1); + expect(matches[0].leadingContextLines[0]).toBe(' if (items.length <= 1) return items;'); + expect(matches[0].trailingContextLines.length).toBe(2); + expect(matches[0].trailingContextLines[0]).toBe(' while(items.length > 0) {'); + expect(matches[0].trailingContextLines[1]).toBe(' current = items.shift();'); + + expect(matches[1].matchText).toBe('current'); + expect(matches[1].range).toEqual([[5, 6], [5, 13]]); + expect(matches[1].lineText).toBe(' current = items.shift();'); + expect(matches[1].lineTextOffset).toBe(0); + expect(matches[1].leadingContextLines.length).toBe(1); + expect(matches[1].leadingContextLines[0]).toBe(' while(items.length > 0) {'); + expect(matches[1].trailingContextLines.length).toBe(2); + expect(matches[1].trailingContextLines[0]).toBe(' current < pivot ? left.push(current) : right.push(current);'); + return expect(matches[1].trailingContextLines[1]).toBe(' }'); + }); + }); + + describe("::backwardsScan(regex, fn)", function() { + beforeEach(() => buffer = TextBuffer.loadSync(require.resolve('./fixtures/sample.js'))); + + return it("calls the given function with the information about each match in backwards order", function() { + const matches = []; + buffer.backwardsScan(/current/g, match => matches.push(match)); + expect(matches.length).toBe(5); + + expect(matches[0].matchText).toBe('current'); + expect(matches[0].range).toEqual([[6, 56], [6, 63]]); + expect(matches[0].lineText).toBe(' current < pivot ? left.push(current) : right.push(current);'); + expect(matches[0].lineTextOffset).toBe(0); + expect(matches[0].leadingContextLines.length).toBe(0); + expect(matches[0].trailingContextLines.length).toBe(0); + + expect(matches[1].matchText).toBe('current'); + expect(matches[1].range).toEqual([[6, 34], [6, 41]]); + expect(matches[1].lineText).toBe(' current < pivot ? left.push(current) : right.push(current);'); + expect(matches[1].lineTextOffset).toBe(0); + expect(matches[1].leadingContextLines.length).toBe(0); + return expect(matches[1].trailingContextLines.length).toBe(0); + }); + }); + + describe("::scanInRange(range, regex, fn)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + describe("when given a regex with a ignore case flag", () => it("does a case-insensitive search", function() { + const matches = []; + buffer.scanInRange(/cuRRent/i, [[0, 0], [12, 0]], ({match, range}) => matches.push(match)); + return 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", function() { + const matches = []; + const ranges = []; + buffer.scanInRange(/cu(rr)ent/, [[4, 0], [6, 44]], function({match, range}) { + matches.push(match); + return 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'); + return 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", function() { + const matches = []; + const ranges = []; + buffer.scanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({match, range}) { + matches.push(match); + return 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'); + return expect(ranges[2]).toEqual([[6, 34], [6, 41]]); + })); + + describe("when the last regex match exceeds the end of the range", function() { + describe("when the portion of the match within the range also matches the regex", () => it("calls the iterator with the truncated match", function() { + const matches = []; + const ranges = []; + buffer.scanInRange(/cu(r*)/g, [[4, 0], [6, 9]], function({match, range}) { + matches.push(match); + return 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'); + return expect(ranges[1]).toEqual([[6, 6], [6, 9]]); + })); + + return describe("when the portion of the match within the range does not matches the regex", () => it("does not call the iterator with the truncated match", function() { + const matches = []; + const ranges = []; + buffer.scanInRange(/cu(r*)e/g, [[4, 0], [6, 9]], function({match, range}) { + matches.push(match); + return 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'); + return expect(ranges[0]).toEqual([[5, 6], [5, 11]]); + })); + }); + + describe("when the iterator calls the 'replace' control function with a replacement string", function() { + it("replaces each occurrence of the regex match with the string", function() { + const ranges = []; + buffer.scanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({range, replace}) { + ranges.push(range); + return 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();'); + return expect(buffer.lineForRow(6)).toBe(' foo < pivot ? left.push(foo) : right.push(current);'); + }); + + return it("allows the match to be replaced with the empty string", function() { + buffer.scanInRange(/current/g, [[4, 0], [6, 59]], ({replace}) => replace("")); + + expect(buffer.lineForRow(5)).toBe(' = items.shift();'); + return expect(buffer.lineForRow(6)).toBe(' < pivot ? left.push() : right.push(current);'); + }); + }); + + describe("when the iterator calls the 'stop' control function", () => it("stops the traversal", function() { + const ranges = []; + buffer.scanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({range, stop}) { + ranges.push(range); + if (ranges.length === 2) { return stop(); } + }); + + return expect(ranges.length).toBe(2); + })); + + it("returns the same results as a regex match on a regular string", function() { + const regexps = [ + /\w+/g, // 1 word + /\w+\n\s*\w+/g, // 2 words separated by an newline (escape sequence) + RegExp("\\w+\n\\s*\w+", 'g'), // 2 words separated by a newline (literal) + /\w+\s+\w+/g, // 2 words separated by some whitespace + /\w+[^\w]+\w+/g, // 2 words separated by anything + /\w+\n\s*\w+\n\s*\w+/g, // 3 words separated by newlines (escape sequence) + RegExp("\\w+\n\\s*\\w+\n\\s*\\w+", 'g'), // 3 words separated by newlines (literal) + /\w+[^\w]+\w+[^\w]+\w+/g, // 3 words separated by anything + ]; + + let i = 0; + return (() => { + const result = []; + while (i < 20) { + var left; + const seed = Date.now(); + const random = new Random(seed); + + const text = buildRandomLines(random, 40); + buffer = new TextBuffer({text}); + buffer.backwardsScanChunkSize = random.intBetween(100, 1000); + + const range = getRandomBufferRange(random, buffer) + .union(getRandomBufferRange(random, buffer)) + .union(getRandomBufferRange(random, buffer)); + const regex = regexps[random(regexps.length)]; + + const expectedMatches = (left = buffer.getTextInRange(range).match(regex)) != null ? left : []; + if (!(expectedMatches.length > 0)) { continue; } + i++; + + var forwardRanges = []; + var forwardMatches = []; + buffer.scanInRange(regex, range, function({range, matchText}) { + forwardRanges.push(range); + return forwardMatches.push(matchText); + }); + expect(forwardMatches).toEqual(expectedMatches, `Seed: ${seed}`); + + var backwardRanges = []; + var backwardMatches = []; + buffer.backwardsScanInRange(regex, range, function({range, matchText}) { + backwardRanges.push(range); + return backwardMatches.push(matchText); + }); + result.push(expect(backwardMatches).toEqual(expectedMatches.reverse(), `Seed: ${seed}`)); + } + return result; + })(); + }); + + it("does not return empty matches at the end of the range", function() { + const ranges = []; + buffer.scanInRange(/[ ]*/gm, [[0, 29], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[0, 29], [0, 29]], [[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.scanInRange(/[ ]*/gm, [[1, 0], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.scanInRange(/\s*/gm, [[0, 29], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[0, 29], [1, 2]]]); + + ranges.length = 0; + buffer.scanInRange(/\s*/gm, [[1, 0], [1, 2]], ({range}) => ranges.push(range)); + return expect(ranges).toEqual([[[1, 0], [1, 2]]]); + }); + + it("allows empty matches at the end of a range, when the range ends at column 0", function() { + const ranges = []; + buffer.scanInRange(/^[ ]*/gm, [[9, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[9, 0], [9, 2]], [[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.scanInRange(/^[ ]*/gm, [[10, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.scanInRange(/^\s*/gm, [[9, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[9, 0], [9, 2]], [[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.scanInRange(/^\s*/gm, [[10, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.scanInRange(/^\s*/gm, [[11, 0], [12, 0]], ({range}) => ranges.push(range)); + return expect(ranges).toEqual([[[11, 0], [11, 2]], [[12, 0], [12, 0]]]); + }); + + return it("handles multi-line patterns", function() { + const matchStrings = []; + + // The '\s' character class + buffer.scan(/{\s+var/, ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['{\n var']); + + // A literal newline character + matchStrings.length = 0; + buffer.scan(RegExp("{\n var"), ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['{\n var']); + + // A '\n' escape sequence + matchStrings.length = 0; + buffer.scan(/{\n var/, ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['{\n var']); + + // A negated character class in the middle of the pattern + matchStrings.length = 0; + buffer.scan(/{[^a] var/, ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['{\n var']); + + // A negated character class at the beginning of the pattern + matchStrings.length = 0; + buffer.scan(/[^a] var/, ({matchText}) => matchStrings.push(matchText)); + return expect(matchStrings).toEqual(['\n var']); + }); + }); + + describe("::find(regex)", () => it("resolves with the first range that matches the given regex", function(done) { + buffer = new TextBuffer('abc\ndefghi'); + return buffer.find(/\wf\w*/).then(function(range) { + expect(range).toEqual(Range(Point(1, 1), Point(1, 6))); + return done(); + }); + })); + + describe("::findAllSync(regex)", () => it("returns all the ranges that match the given regex", function() { + buffer = new TextBuffer('abc\ndefghi'); + return expect(buffer.findAllSync(/[bf]\w+/)).toEqual([ + Range(Point(0, 1), Point(0, 3)), + Range(Point(1, 2), Point(1, 6)), + ]); + })); + + describe("::findAndMarkAllInRangeSync(markerLayer, regex, range, options)", () => it("populates the marker index with the matching ranges", function() { + buffer = new TextBuffer('abc def\nghi jkl\n'); + const layer = buffer.addMarkerLayer(); + let markers = buffer.findAndMarkAllInRangeSync(layer, /\w+/g, [[0, 1], [1, 6]], {invalidate: 'inside'}); + expect(markers.map(marker => marker.getRange())).toEqual([ + [[0, 1], [0, 3]], + [[0, 4], [0, 7]], + [[1, 0], [1, 3]], + [[1, 4], [1, 6]] + ]); + expect(markers[0].getInvalidationStrategy()).toBe('inside'); + expect(markers[0].isExclusive()).toBe(true); + + markers = buffer.findAndMarkAllInRangeSync(layer, /abc/g, [[0, 0], [1, 0]], {invalidate: 'touch'}); + expect(markers.map(marker => marker.getRange())).toEqual([ + [[0, 0], [0, 3]] + ]); + expect(markers[0].getInvalidationStrategy()).toBe('touch'); + return expect(markers[0].isExclusive()).toBe(false); + })); + + describe("::findWordsWithSubsequence and ::findWordsWithSubsequenceInRange", function() { + it('resolves with all words matching the given query', function(done) { + buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana'); + return buffer.findWordsWithSubsequence('bna', '_', 4).then(function(results) { + expect(JSON.parse(JSON.stringify(results))).toEqual([ + { + score: 29, + matchIndices: [0, 1, 2], + positions: [{row: 0, column: 36}], + word: "bNa" + }, + { + score: 16, + matchIndices: [0, 2, 4], + positions: [{row: 0, column: 15}], + word: "ban_ana" + }, + { + score: 12, + matchIndices: [0, 2, 3], + positions: [{row: 0, column: 0}, {row: 1, column: 0}], + word: "banana" + }, + { + score: 7, + matchIndices: [0, 5, 6], + positions: [{row: 0, column: 7}], + word: "bandana" + } + ]); + return done(); + }); + }); + + return it('resolves with all words matching the given query and range', function(done) { + const range = {start: {column: 0, row: 0}, end: {column: 22, row: 0}}; + buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana'); + return buffer.findWordsWithSubsequenceInRange('bna', '_', 3, range).then(function(results) { + expect(JSON.parse(JSON.stringify(results))).toEqual([ + { + score: 16, + matchIndices: [0, 2, 4], + positions: [{row: 0, column: 15}], + word: "ban_ana" + }, + { + score: 12, + matchIndices: [0, 2, 3], + positions: [{row: 0, column: 0}], + word: "banana" + }, + { + score: 7, + matchIndices: [0, 5, 6], + positions: [{row: 0, column: 7}], + word: "bandana" + } + ]); + return done(); + }); + }); + }); + + describe("::backwardsScanInRange(range, regex, fn)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + 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", function() { + const matches = []; + const ranges = []; + buffer.backwardsScanInRange(/cu(rr)ent/, [[4, 0], [6, 44]], function({match, range}) { + matches.push(match); + return 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'); + return 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", function() { + const matches = []; + const ranges = []; + buffer.backwardsScanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({match, range}) { + matches.push(match); + return 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'); + return expect(ranges[2]).toEqual([[5, 6], [5, 13]]); + })); + + describe("when the last regex match starts at the beginning of the range", () => it("calls the iterator with the match", function() { + let matches = []; + let ranges = []; + buffer.scanInRange(/quick/g, [[0, 4], [2, 0]], function({match, range}) { + matches.push(match); + return ranges.push(range); + }); + + expect(matches.length).toBe(1); + expect(ranges.length).toBe(1); + + expect(matches[0][0]).toBe('quick'); + expect(ranges[0]).toEqual([[0, 4], [0, 9]]); + + matches = []; + ranges = []; + buffer.scanInRange(/^/, [[0, 0], [2, 0]], function({match, range}) { + matches.push(match); + return ranges.push(range); + }); + + expect(matches.length).toBe(1); + expect(ranges.length).toBe(1); + + expect(matches[0][0]).toBe(""); + return expect(ranges[0]).toEqual([[0, 0], [0, 0]]); + })); + + describe("when the first regex match exceeds the end of the range", function() { + describe("when the portion of the match within the range also matches the regex", () => it("calls the iterator with the truncated match", function() { + const matches = []; + const ranges = []; + buffer.backwardsScanInRange(/cu(r*)/g, [[4, 0], [6, 9]], function({match, range}) { + matches.push(match); + return ranges.push(range); + }); + + expect(matches.length).toBe(2); + expect(ranges.length).toBe(2); + + expect(matches[0][0]).toBe('cur'); + expect(matches[0][1]).toBe('r'); + expect(ranges[0]).toEqual([[6, 6], [6, 9]]); + + expect(matches[1][0]).toBe('curr'); + expect(matches[1][1]).toBe('rr'); + return expect(ranges[1]).toEqual([[5, 6], [5, 10]]); + })); + + return describe("when the portion of the match within the range does not matches the regex", () => it("does not call the iterator with the truncated match", function() { + const matches = []; + const ranges = []; + buffer.backwardsScanInRange(/cu(r*)e/g, [[4, 0], [6, 9]], function({match, range}) { + matches.push(match); + return 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'); + return 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", function() { + const ranges = []; + buffer.backwardsScanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({range, replace}) { + ranges.push(range); + if (!range.start.isEqual([6, 6])) { return replace("foo"); } + }); + + 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();'); + return 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", function() { + const ranges = []; + buffer.backwardsScanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({range, stop}) { + ranges.push(range); + if (ranges.length === 2) { return stop(); } + }); + + expect(ranges.length).toBe(2); + expect(ranges[0]).toEqual([[6, 34], [6, 41]]); + return expect(ranges[1]).toEqual([[6, 6], [6, 13]]); + })); + + describe("when called with a random range", () => it("returns the same results as ::scanInRange, but in the opposite order", () => (() => { + const result = []; + for (let i = 1; i < 50; i++) { + const seed = Date.now(); + const random = new Random(seed); + + buffer.backwardsScanChunkSize = random.intBetween(1, 80); + + const [startRow, endRow] = Array.from([random(buffer.getLineCount()), random(buffer.getLineCount())].sort()); + const startColumn = random(buffer.lineForRow(startRow).length); + const endColumn = random(buffer.lineForRow(endRow).length); + const range = [[startRow, startColumn], [endRow, endColumn]]; + + const regex = [ + /\w/g, + /\w{2}/g, + /\w{3}/g, + /.{5}/g + ][random(4)]; + + if (random(2) > 0) { + var forwardRanges = []; + var backwardRanges = []; + var forwardMatches = []; + var backwardMatches = []; + + buffer.scanInRange(regex, range, function({range, matchText}) { + forwardMatches.push(matchText); + return forwardRanges.push(range); + }); + + buffer.backwardsScanInRange(regex, range, function({range, matchText}) { + backwardMatches.unshift(matchText); + return backwardRanges.unshift(range); + }); + + expect(backwardRanges).toEqual(forwardRanges, `Seed: ${seed}`); + result.push(expect(backwardMatches).toEqual(forwardMatches, `Seed: ${seed}`)); + } else { + const referenceBuffer = new TextBuffer({text: buffer.getText()}); + referenceBuffer.scanInRange(regex, range, ({matchText, replace}) => replace(matchText + '.')); + + buffer.backwardsScanInRange(regex, range, ({matchText, replace}) => replace(matchText + '.')); + + result.push(expect(buffer.getText()).toBe(referenceBuffer.getText(), `Seed: ${seed}`)); + } + } + return result; + })())); + + it("does not return empty matches at the end of the range", function() { + const ranges = []; + + buffer.backwardsScanInRange(/[ ]*/gm, [[1, 0], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/[ ]*/m, [[0, 29], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/\s*/gm, [[1, 0], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/\s*/m, [[0, 29], [1, 2]], ({range}) => ranges.push(range)); + return expect(ranges).toEqual([[[0, 29], [1, 2]]]); + }); + + return it("allows empty matches at the end of a range, when the range ends at column 0", function() { + const ranges = []; + buffer.backwardsScanInRange(/^[ ]*/gm, [[9, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]], [[9, 0], [9, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/^[ ]*/gm, [[10, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/^\s*/gm, [[9, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]], [[9, 0], [9, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/^\s*/gm, [[10, 0], [10, 0]], ({range}) => ranges.push(range)); + return expect(ranges).toEqual([[[10, 0], [10, 0]]]); + }); + }); + + describe("::characterIndexForPosition(position)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + it("returns the total number of characters that precede the given position", function() { + 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); + return expect(buffer.characterIndexForPosition([Infinity])).toBe(408); + }); + + return describe("when the buffer contains crlf line endings", () => it("returns the total number of characters that precede the given position", function() { + buffer.setText("line1\r\nline2\nline3\r\nline4"); + expect(buffer.characterIndexForPosition([1])).toBe(7); + expect(buffer.characterIndexForPosition([2])).toBe(13); + return expect(buffer.characterIndexForPosition([3])).toBe(20); + })); + }); + + describe("::positionForCharacterIndex(position)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + it("returns the position based on character index", function() { + 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]); + return expect(buffer.positionForCharacterIndex(408)).toEqual([12, 2]); + }); + + return describe("when the buffer contains crlf line endings", () => it("returns the position based on character index", function() { + buffer.setText("line1\r\nline2\nline3\r\nline4"); + expect(buffer.positionForCharacterIndex(7)).toEqual([1, 0]); + expect(buffer.positionForCharacterIndex(13)).toEqual([2, 0]); + return expect(buffer.positionForCharacterIndex(20)).toEqual([3, 0]); + })); +}); + + describe("::isEmpty()", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + it("returns true for an empty buffer", function() { + buffer.setText(''); + return expect(buffer.isEmpty()).toBeTruthy(); + }); + + return it("returns false for a non-empty buffer", function() { + buffer.setText('a'); + expect(buffer.isEmpty()).toBeFalsy(); + buffer.setText('a\nb\nc'); + expect(buffer.isEmpty()).toBeFalsy(); + buffer.setText('\n'); + return expect(buffer.isEmpty()).toBeFalsy(); + }); + }); + + describe("::hasAstral()", function() { + it("returns true for buffers containing surrogate pairs", () => expect(new TextBuffer('hooray 😄').hasAstral()).toBeTruthy()); + + return it("returns false for buffers that do not contain surrogate pairs", () => expect(new TextBuffer('nope').hasAstral()).toBeFalsy()); + }); + + describe("::onWillChange(callback)", () => it("notifies observers before a transaction, an undo or a redo", function() { + let changeCount = 0; + let expectedText = ''; + + buffer = new TextBuffer(); + const checkpoint = buffer.createCheckpoint(); + + buffer.onWillChange(function(change) { + expect(buffer.getText()).toBe(expectedText); + return changeCount++; + }); + + buffer.append('a'); + expect(changeCount).toBe(1); + expectedText = 'a'; + + buffer.transact(function() { + buffer.append('b'); + return buffer.append('c'); + }); + expect(changeCount).toBe(2); + expectedText = 'abc'; + + // Empty transactions do not cause onWillChange listeners to be called + buffer.transact(function() {}); + expect(changeCount).toBe(2); + + buffer.undo(); + expect(changeCount).toBe(3); + expectedText = 'a'; + + buffer.redo(); + expect(changeCount).toBe(4); + expectedText = 'abc'; + + buffer.revertToCheckpoint(checkpoint); + return expect(changeCount).toBe(5); + })); + + describe("::onDidChange(callback)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + it("notifies observers after a transaction, an undo or a redo", function() { + let textChanges = []; + buffer.onDidChange(({changes}) => textChanges.push(...Array.from(changes || []))); + + buffer.insert([0, 0], "abc"); + buffer.delete([[0, 0], [0, 1]]); + + assertChangesEqual(textChanges, [ + { + oldRange: [[0, 0], [0, 0]], + newRange: [[0, 0], [0, 3]], + oldText: "", + newText: "abc" + }, + { + oldRange: [[0, 0], [0, 1]], + newRange: [[0, 0], [0, 0]], + oldText: "a", + newText: "" + } + ]); + + textChanges = []; + buffer.transact(function() { + buffer.insert([1, 0], "v"); + buffer.insert([1, 1], "x"); + buffer.insert([1, 2], "y"); + buffer.insert([2, 3], "zw"); + return buffer.delete([[2, 3], [2, 4]]); + }); + + assertChangesEqual(textChanges, [ + { + oldRange: [[1, 0], [1, 0]], + newRange: [[1, 0], [1, 3]], + oldText: "", + newText: "vxy", + }, + { + oldRange: [[2, 3], [2, 3]], + newRange: [[2, 3], [2, 4]], + oldText: "", + newText: "w", + } + ]); + + textChanges = []; + buffer.undo(); + assertChangesEqual(textChanges, [ + { + oldRange: [[1, 0], [1, 3]], + newRange: [[1, 0], [1, 0]], + oldText: "vxy", + newText: "", + }, + { + oldRange: [[2, 3], [2, 4]], + newRange: [[2, 3], [2, 3]], + oldText: "w", + newText: "", + } + ]); + + textChanges = []; + buffer.redo(); + assertChangesEqual(textChanges, [ + { + oldRange: [[1, 0], [1, 0]], + newRange: [[1, 0], [1, 3]], + oldText: "", + newText: "vxy", + }, + { + oldRange: [[2, 3], [2, 3]], + newRange: [[2, 3], [2, 4]], + oldText: "", + newText: "w", + } + ]); + + textChanges = []; + buffer.transact(() => buffer.transact(() => buffer.insert([0, 0], "j"))); + + // we emit only one event for nested transactions + return assertChangesEqual(textChanges, [ + { + oldRange: [[0, 0], [0, 0]], + newRange: [[0, 0], [0, 1]], + oldText: "", + newText: "j", + } + ]); + }); + + it("doesn't notify observers after an empty transaction", function() { + const didChangeTextSpy = jasmine.createSpy(); + buffer.onDidChange(didChangeTextSpy); + buffer.transact(function() {}); + return expect(didChangeTextSpy).not.toHaveBeenCalled(); + }); + + return it("doesn't throw an error when clearing the undo stack within a transaction", function() { + let didChangeTextSpy; + buffer.onDidChange(didChangeTextSpy = jasmine.createSpy()); + expect(() => buffer.transact(() => buffer.clearUndoStack())).not.toThrowError(); + return expect(didChangeTextSpy).not.toHaveBeenCalled(); + }); + }); + + describe("::onDidStopChanging(callback)", function() { + let [delay, didStopChangingCallback] = Array.from([]); + + const wait = (milliseconds, callback) => setTimeout(callback, milliseconds); + + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + delay = buffer.stoppedChangingDelay; + didStopChangingCallback = jasmine.createSpy("didStopChangingCallback"); + return buffer.onDidStopChanging(didStopChangingCallback); + }); + + it("notifies observers after a delay passes following changes", function(done) { + buffer.insert([0, 0], 'a'); + expect(didStopChangingCallback).not.toHaveBeenCalled(); + + return wait(delay / 2, function() { + buffer.transact(() => buffer.transact(function() { + buffer.insert([0, 0], 'b'); + buffer.insert([1, 0], 'c'); + return buffer.insert([1, 1], 'd'); + })); + expect(didStopChangingCallback).not.toHaveBeenCalled(); + + return wait(delay / 2, function() { + expect(didStopChangingCallback).not.toHaveBeenCalled(); + + return wait(delay, function() { + expect(didStopChangingCallback).toHaveBeenCalled(); + assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ + { + oldRange: [[0, 0], [0, 0]], + newRange: [[0, 0], [0, 2]], + oldText: "", + newText: "ba", + }, + { + oldRange: [[1, 0], [1, 0]], + newRange: [[1, 0], [1, 2]], + oldText: "", + newText: "cd", + } + ]); + + didStopChangingCallback.calls.reset(); + buffer.undo(); + buffer.undo(); + return wait(delay * 2, function() { + expect(didStopChangingCallback).toHaveBeenCalled(); + assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ + { + oldRange: [[0, 0], [0, 2]], + newRange: [[0, 0], [0, 0]], + oldText: "ba", + newText: "", + }, + { + oldRange: [[1, 0], [1, 2]], + newRange: [[1, 0], [1, 0]], + oldText: "cd", + newText: "", + }, + ]); + return done(); + }); + }); + }); + }); + }); + + return it("provides the correct changes when the buffer is mutated in the onDidChange callback", function(done) { + buffer.onDidChange(function({changes}) { + switch (changes[0].newText) { + case 'a': + return buffer.insert(changes[0].newRange.end, 'b'); + case 'b': + return buffer.insert(changes[0].newRange.end, 'c'); + case 'c': + return buffer.insert(changes[0].newRange.end, 'd'); + } + }); + + buffer.insert([0, 0], 'a'); + + return wait(delay * 2, function() { + expect(didStopChangingCallback).toHaveBeenCalled(); + assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ + { + oldRange: [[0, 0], [0, 0]], + newRange: [[0, 0], [0, 4]], + oldText: "", + newText: "abcd", + } + ]); + return done(); + }); + }); + }); + + describe("::append(text)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + return it("adds text to the end of the buffer", function() { + buffer.setText(""); + buffer.append("a"); + expect(buffer.getText()).toBe("a"); + buffer.append("b\nc"); + return expect(buffer.getText()).toBe("ab\nc"); + }); + }); + + describe("::setLanguageMode", function() { + it("destroys the previous language mode", function() { + buffer = new TextBuffer(); + + const languageMode1 = { + alive: true, + destroy() { return this.alive = false; }, + onDidChangeHighlighting() { return {dispose() {}}; } + }; + + const languageMode2 = { + alive: true, + destroy() { return this.alive = false; }, + onDidChangeHighlighting() { return {dispose() {}}; } + }; + + buffer.setLanguageMode(languageMode1); + expect(languageMode1.alive).toBe(true); + expect(languageMode2.alive).toBe(true); + + buffer.setLanguageMode(languageMode2); + expect(languageMode1.alive).toBe(false); + expect(languageMode2.alive).toBe(true); + + buffer.destroy(); + expect(languageMode1.alive).toBe(false); + return expect(languageMode2.alive).toBe(false); + }); + + return it("notifies ::onDidChangeLanguageMode observers when the language mode changes", function() { + buffer = new TextBuffer(); + expect(buffer.getLanguageMode() instanceof NullLanguageMode).toBe(true); + + const events = []; + buffer.onDidChangeLanguageMode((newMode, oldMode) => events.push({newMode, oldMode})); + + const languageMode = { + onDidChangeHighlighting() { return {dispose() {}}; } + }; + + buffer.setLanguageMode(languageMode); + expect(buffer.getLanguageMode()).toBe(languageMode); + expect(events.length).toBe(1); + expect(events[0].newMode).toBe(languageMode); + expect(events[0].oldMode instanceof NullLanguageMode).toBe(true); + + buffer.setLanguageMode(null); + expect(buffer.getLanguageMode() instanceof NullLanguageMode).toBe(true); + expect(events.length).toBe(2); + expect(events[1].newMode).toBe(buffer.getLanguageMode()); + return expect(events[1].oldMode).toBe(languageMode); + }); + }); + + return describe("line ending support", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + return buffer = TextBuffer.loadSync(filePath); + }); + + describe(".getText()", () => it("returns the text with the corrent line endings for each row", function() { + buffer.setText("a\r\nb\nc"); + expect(buffer.getText()).toBe("a\r\nb\nc"); + buffer.setText("a\r\nb\nc\n"); + return expect(buffer.getText()).toBe("a\r\nb\nc\n"); + })); + + describe("when editing a line", () => it("preserves the existing line ending", function() { + buffer.setText("a\r\nb\nc"); + buffer.insert([0, 1], "1"); + return expect(buffer.getText()).toBe("a1\r\nb\nc"); + })); + + describe("when inserting text with multiple lines", function() { + describe("when the current line has a line ending", () => it("uses the same line ending as the line where the text is inserted", function() { + buffer.setText("a\r\n"); + buffer.insert([0, 1], "hello\n1\n\n2"); + return expect(buffer.getText()).toBe("ahello\r\n1\r\n\r\n2\r\n"); + })); + + return describe("when the current line has no line ending (because it's the last line of the buffer)", function() { + describe("when the buffer contains only a single line", () => it("honors the line endings in the inserted text", function() { + buffer.setText("initialtext"); + buffer.append("hello\n1\r\n2\n"); + return expect(buffer.getText()).toBe("initialtexthello\n1\r\n2\n"); + })); + + return describe("when the buffer contains a preceding line", () => it("uses the line ending of the preceding line", function() { + buffer.setText("\ninitialtext"); + buffer.append("hello\n1\r\n2\n"); + return expect(buffer.getText()).toBe("\ninitialtexthello\n1\n2\n"); + })); + }); + }); + + return describe("::setPreferredLineEnding(lineEnding)", function() { + it("uses the given line ending when normalizing, rather than inferring one from the surrounding text", function() { + buffer = new TextBuffer({text: "a \r\n"}); + + expect(buffer.getPreferredLineEnding()).toBe(null); + buffer.append(" b \n"); + expect(buffer.getText()).toBe("a \r\n b \r\n"); + + buffer.setPreferredLineEnding("\n"); + expect(buffer.getPreferredLineEnding()).toBe("\n"); + buffer.append(" c \n"); + expect(buffer.getText()).toBe("a \r\n b \r\n c \n"); + + buffer.setPreferredLineEnding(null); + buffer.append(" d \r\n"); + return expect(buffer.getText()).toBe("a \r\n b \r\n c \n d \n"); + }); + + return it("persists across serialization and deserialization", function(done) { + const bufferA = new TextBuffer; + bufferA.setPreferredLineEnding("\r\n"); + + return TextBuffer.deserialize(bufferA.serialize()).then(function(bufferB) { + expect(bufferB.getPreferredLineEnding()).toBe("\r\n"); + return done(); + }); + }); + }); + }); +}); + +var assertChangesEqual = function(actualChanges, expectedChanges) { + expect(actualChanges.length).toBe(expectedChanges.length); + return (() => { + const result = []; + for (let i = 0; i < actualChanges.length; i++) { + const actualChange = actualChanges[i]; + const expectedChange = expectedChanges[i]; + expect(actualChange.oldRange).toEqual(expectedChange.oldRange); + expect(actualChange.newRange).toEqual(expectedChange.newRange); + expect(actualChange.oldText).toEqual(expectedChange.oldText); + result.push(expect(actualChange.newText).toEqual(expectedChange.newText)); + } + return result; + })(); +}; + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +}