diff --git a/package-lock.json b/package-lock.json index b11c1629f1..8867982ecc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "text-buffer", - "version": "13.7.1", + "version": "13.8.0-display-layer-change-event-2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f48b32e71a..34dc635409 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "text-buffer", - "version": "13.7.2", + "version": "13.8.0-display-layer-change-event-2", "description": "A container for large mutable strings with annotated regions", "main": "./lib/text-buffer", "scripts": { diff --git a/spec/display-layer-spec.js b/spec/display-layer-spec.js index fa6c23fb13..3df091a400 100644 --- a/spec/display-layer-spec.js +++ b/spec/display-layer-spec.js @@ -1989,14 +1989,13 @@ describe('DisplayLayer', () => { displayLayer.setTextDecorationLayer(decorationLayer) const allChanges = [] - displayLayer.onDidChangeSync((changes) => allChanges.push(...changes)) + displayLayer.onDidChange((changes) => allChanges.push(...changes)) decorationLayer.emitInvalidateRangeEvent([[2, 1], [3, 2]]) expect(allChanges).toEqual([{ - start: Point(1, 0), - oldExtent: Point(2, 0), - newExtent: Point(2, 0) + oldRange: Range(Point(1, 0), Point(3, 0)), + newRange: Range(Point(1, 0), Point(3, 0)) }]) }) @@ -2167,6 +2166,128 @@ describe('DisplayLayer', () => { }) }) + describe('.onDidChange', () => { + it('calls the given callback when the display layer\'s content changes', () => { + const buffer = new TextBuffer({ + text: 'abc\ndef\nghi\njkl\nmno' + }) + + const displayLayer = buffer.addDisplayLayer({tabLength: 4}) + + const events = [] + displayLayer.onDidChange((changes) => events.push(...changes)) + + displayLayer.foldBufferRange(Range(Point(1, 1), Point(2, 2))) + expect(events).toEqual([ + { + oldRange: Range(Point(1, 0), Point(3, 0)), + newRange: Range(Point(1, 0), Point(2, 0)) + } + ]) + + events.length = 0 + displayLayer.foldBufferRange(Range(Point(3, 1), Point(4, 2))) + expect(events).toEqual([ + { + oldRange: Range(Point(2, 0), Point(4, 0)), + newRange: Range(Point(2, 0), Point(3, 0)) + } + ]) + + events.length = 0 + displayLayer.destroyAllFolds() + expect(events).toEqual([ + { + oldRange: Range(Point(1, 0), Point(3, 0)), + newRange: Range(Point(1, 0), Point(5, 0)) + } + ]) + + // When multiple changes occur in a transaction, the changes are combined. + events.length = 0 + buffer.transact(() => { + displayLayer.foldBufferRange(Range(Point(1, 1), Point(2, 2))) + displayLayer.foldBufferRange(Range(Point(3, 1), Point(4, 2))) + }) + expect(events).toEqual([ + { + oldRange: Range(Point(1, 0), Point(5, 0)), + newRange: Range(Point(1, 0), Point(3, 0)) + } + ]) + }) + + it('calls the callback only one time per text buffer transaction', () => { + const buffer = new TextBuffer({ + text: 'abc\ndef\nghi\njkl\nmno' + }) + + const displayLayer = buffer.addDisplayLayer({tabLength: 4}) + + const events = [] + displayLayer.onDidChange((changes) => events.push(changes)) + + const checkpoint = buffer.createCheckpoint() + + buffer.transact(() => { + buffer.setTextInRange([[0, 1], [0, 1]], '\n') + buffer.setTextInRange([[4, 2], [4, 2]], '\n') + buffer.setTextInRange([[4, 2], [4, 2]], '.') + buffer.setTextInRange([[4, 3], [4, 3]], '.') + buffer.setTextInRange([[4, 4], [4, 4]], '.') + }) + expect(events).toEqual([[ + { + oldRange: Range(Point(0, 0), Point(1, 0)), + newRange: Range(Point(0, 0), Point(2, 0)) + }, + { + oldRange: Range(Point(3, 0), Point(4, 0)), + newRange: Range(Point(4, 0), Point(6, 0)) + } + ]]) + + events.length = 0 + buffer.undo() + expect(events).toEqual([[ + { + oldRange: Range(Point(0, 0), Point(2, 0)), + newRange: Range(Point(0, 0), Point(1, 0)) + }, + { + oldRange: Range(Point(4, 0), Point(6, 0)), + newRange: Range(Point(3, 0), Point(4, 0)) + } + ]]) + + events.length = 0 + buffer.redo() + expect(events).toEqual([[ + { + oldRange: Range(Point(0, 0), Point(1, 0)), + newRange: Range(Point(0, 0), Point(2, 0)) + }, + { + oldRange: Range(Point(3, 0), Point(4, 0)), + newRange: Range(Point(4, 0), Point(6, 0)) + } + ]]) + + events.length = 0 + buffer.revertToCheckpoint(checkpoint) + expect(events).toEqual([[ + { + oldRange: Range(Point(0, 0), Point(2, 0)), + newRange: Range(Point(0, 0), Point(1, 0)) + }, + { + oldRange: Range(Point(4, 0), Point(6, 0)), + newRange: Range(Point(3, 0), Point(4, 0)) + } + ]]) + }) + }) + describe('.getApproximateScreenLineCount()', () => { it('estimates the screen line count based on the currently-indexed portion of the buffer', () => { const buffer = new TextBuffer({ @@ -2458,10 +2579,10 @@ function verifyChangeEvent (displayLayer, fn) { displayLayerCopy.setTextDecorationLayer(displayLayer.getTextDecorationLayer()) const previousTokenLines = getTokens(displayLayerCopy) displayLayerCopy.destroy() - let lastChanges = null + let changes = [] - const disposable = displayLayer.onDidChangeSync(function (changes) { - lastChanges = changes + const disposable = displayLayer.onDidChange((event) => { + changes.push(...event) }) fn() @@ -2469,7 +2590,7 @@ function verifyChangeEvent (displayLayer, fn) { displayLayerCopy = displayLayer.copy() displayLayerCopy.setTextDecorationLayer(displayLayer.getTextDecorationLayer()) const expectedTokenLines = getTokens(displayLayerCopy) - updateTokenLines(previousTokenLines, displayLayerCopy, lastChanges) + updateTokenLines(previousTokenLines, displayLayerCopy, changes) displayLayerCopy.destroy() expect(previousTokenLines).toEqual(expectedTokenLines) } @@ -2700,9 +2821,9 @@ function getTokenBoundaries (displayLayer, startRow = 0, endRow = displayLayer.g } function updateTokenLines (tokenLines, displayLayer, changes) { - for (const {start, oldExtent, newExtent} of changes || []) { - const newTokenLines = getTokens(displayLayer, start.row, start.row + newExtent.row) - tokenLines.splice(start.row, oldExtent.row, ...newTokenLines) + for (const {oldRange, newRange} of changes || []) { + const newTokenLines = getTokens(displayLayer, newRange.start.row, newRange.end.row) + tokenLines.splice(newRange.start.row, oldRange.end.row - oldRange.start.row, ...newTokenLines) } } diff --git a/spec/marker-layer-spec.coffee b/spec/marker-layer-spec.coffee index 06659400cd..f8efa226af 100644 --- a/spec/marker-layer-spec.coffee +++ b/spec/marker-layer-spec.coffee @@ -219,7 +219,7 @@ describe "MarkerLayer", -> expect(updateCount).toBe(6) # update events happen after updating display layers when there is no parent transaction. - displayLayer.onDidChangeSync -> + displayLayer.onDidChange -> displayLayerDidChange = true buffer.undo() expect(updateCount).toBe(7) diff --git a/spec/text-buffer-io-spec.js b/spec/text-buffer-io-spec.js index c691e8fa84..a41a7f4381 100644 --- a/spec/text-buffer-io-spec.js +++ b/spec/text-buffer-io-spec.js @@ -165,7 +165,7 @@ describe('TextBuffer IO', () => { const events = [] const displayLayer = buffer.addDisplayLayer() - displayLayer.onDidChangeSync((event) => events.push(['display-layer', event])) + displayLayer.onDidChange((event) => events.push(['display-layer', event])) buffer.registerTextDecorationLayer({ bufferDidChange ({oldRange, newRange}) { events.push(['decoration-layer', {oldRange, newRange}]) } @@ -174,7 +174,7 @@ describe('TextBuffer IO', () => { buffer.reload().then(() => { expect(events).toEqual([ ['decoration-layer', {oldRange: Range(Point(0, 7), Point(0, 7)), newRange: Range(Point(0, 7), Point(0, 11))}], - ['display-layer', [{start: Point(0, 0), oldExtent: Point(1, 0), newExtent: Point(1, 0)}]] + ['display-layer', [{oldRange: Range(Point(0, 0), Point(1, 0)), newRange: Range(Point(0, 0), Point(1, 0))}]] ]) done() }) diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee index 1eff8c121d..7e3888517d 100644 --- a/spec/text-buffer-spec.coffee +++ b/spec/text-buffer-spec.coffee @@ -97,7 +97,9 @@ describe "TextBuffer", -> expect(buffer.getText()).toEqual "hey\nyou're old\r\nhow are you doing?" describe "after a change", -> - it "notifies, in order, decoration layers, display layers, and display layer ::onDidChangeSync observers with the relevant details", -> + it "notifies, in order, decoration layers, display layers, and display layer ::onDidChange observers with the relevant details", -> + buffer = new TextBuffer("hello\nworld\r\nhow are you doing?") + events = [] textDecorationLayer1 = { bufferDidChange: (e) -> events.push({source: 'decoration-layer-1', event: e}) @@ -118,11 +120,11 @@ describe "TextBuffer", -> buffer.registerTextDecorationLayer(textDecorationLayer1) # insert a duplicate decoration layer buffer.registerTextDecorationLayer(textDecorationLayer2) buffer.onDidChange (e) -> events.push({source: 'buffer', event: JSON.parse(JSON.stringify(e))}) + displayLayer1.onDidChange (e) -> events.push({source: 'display-layer-event', event: e}) - disposable = displayLayer1.onDidChangeSync -> - disposable.dispose() + buffer.transact -> + buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", normalizeLineEndings: false) buffer.setTextInRange([[1, 1], [1, 2]], "abc", normalizeLineEndings: false) - buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", normalizeLineEndings: false) changeEvent1 = { oldRange: [[0, 2], [2, 3]], newRange: [[0, 2], [2, 4]] @@ -157,6 +159,13 @@ describe "TextBuffer", -> } ] } + }, + { + source: 'display-layer-event', + event: [{ + oldRange: Range(Point(0, 0), Point(3, 0)), + newRange: Range(Point(0, 0), Point(3, 0)) + }] } ] diff --git a/src/display-layer.js b/src/display-layer.js index a4a82eedaa..cadc063783 100644 --- a/src/display-layer.js +++ b/src/display-layer.js @@ -24,6 +24,7 @@ class DisplayLayer { this.textDecorationLayer = new EmptyDecorationLayer() this.displayMarkerLayersById = new Map() this.destroyed = false + this.changesSinceLastEvent = new Patch() this.invisibles = params.invisibles != null ? params.invisibles : {} this.tabLength = params.tabLength != null ? params.tabLength : 4 @@ -153,11 +154,7 @@ class DisplayLayer { const endRow = this.translateBufferPositionWithSpatialIndex(Point(endBufferRow, 0), 'backward').row const extent = Point(endRow - startRow, 0) spliceArray(this.cachedScreenLines, startRow, extent.row, new Array(extent.row)) - this.emitDidChangeSyncEvent([{ - start: Point(startRow, 0), - oldExtent: extent, - newExtent: extent - }]) + this.didChange({start: Point(startRow, 0), oldExtent: extent, newExtent: extent}) }) } } @@ -185,8 +182,8 @@ class DisplayLayer { this.displayMarkerLayersById.delete(id) } - onDidChangeSync (callback) { - return this.emitter.on('did-change-sync', callback) + onDidChange (callback) { + return this.emitter.on('did-change', callback) } onDidReset (callback) { @@ -207,9 +204,7 @@ class DisplayLayer { if (containingFoldMarkers.length === 0) { const foldStartRow = bufferRange.start.row const foldEndRow = bufferRange.end.row + 1 - this.emitDidChangeSyncEvent([ - this.updateSpatialIndex(foldStartRow, foldEndRow, foldEndRow, Infinity) - ]) + this.didChange(this.updateSpatialIndex(foldStartRow, foldEndRow, foldEndRow, Infinity)) this.notifyObserversIfMarkerScreenPositionsChanged() } return foldId @@ -269,12 +264,12 @@ class DisplayLayer { foldMarker.destroy() } - this.emitDidChangeSyncEvent([this.updateSpatialIndex( + this.didChange(this.updateSpatialIndex( combinedRangeStart.row, combinedRangeEnd.row + 1, combinedRangeEnd.row + 1, Infinity - )]) + )) this.notifyObserversIfMarkerScreenPositionsChanged() return foldedRanges @@ -807,10 +802,8 @@ class DisplayLayer { } } - const combinedChanges = new Patch() this.indexedBufferRowCount += newEndRow - oldEndRow - const {start, oldExtent, newExtent} = this.updateSpatialIndex(startRow, oldEndRow + 1, newEndRow + 1, Infinity) - combinedChanges.splice(start, oldExtent, newExtent) + this.didChange(this.updateSpatialIndex(startRow, oldEndRow + 1, newEndRow + 1, Infinity)) for (let bufferRange of this.textDecorationLayer.getInvalidatedRanges()) { bufferRange = Range.fromObject(bufferRange) @@ -821,20 +814,25 @@ class DisplayLayer { const endRow = this.translateBufferPositionWithSpatialIndex(Point(endBufferRow, 0), 'backward').row const extent = Point(endRow - startRow, 0) spliceArray(this.cachedScreenLines, startRow, extent.row, new Array(extent.row)) - combinedChanges.splice(Point(startRow, 0), extent, extent) + this.didChange({start: Point(startRow, 0), oldExtent: extent, newExtent: extent}) } + } - return Object.freeze(combinedChanges.getChanges().map((hunk) => { - return { - start: Point.fromObject(hunk.newStart), - oldExtent: traversal(hunk.oldEnd, hunk.oldStart), - newExtent: traversal(hunk.newEnd, hunk.newStart) - } - })) + didChange ({start, oldExtent, newExtent}) { + this.changesSinceLastEvent.splice(start, oldExtent, newExtent) + if (this.buffer.transactCallDepth === 0) this.emitDeferredChangeEvents() } - emitDidChangeSyncEvent (event) { - this.emitter.emit('did-change-sync', event) + emitDeferredChangeEvents () { + if (this.changesSinceLastEvent.getChangeCount() > 0) { + this.emitter.emit('did-change', this.changesSinceLastEvent.getChanges().map((change) => { + return { + oldRange: new Range(change.oldStart, change.oldEnd), + newRange: new Range(change.newStart, change.newEnd) + } + })) + this.changesSinceLastEvent = new Patch() + } } notifyObserversIfMarkerScreenPositionsChanged () { diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 4bd511c9ed..3e65ecf000 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -369,9 +369,6 @@ class TextBuffer # Public: This is now identical to {onDidChange}. onDidChangeText: (callback) -> @onDidChange(callback) - preemptDidChange: (callback) -> - @emitter.preempt 'did-change', callback - # Public: Invoke the given callback asynchronously following one or more # changes after {::getStoppedChangingDelay} milliseconds elapse without an # additional change. @@ -884,16 +881,9 @@ class TextBuffer # Emit the change event on all the registered text decoration layers. @textDecorationLayers.forEach (textDecorationLayer) -> textDecorationLayer.bufferDidChange(changeEvent) - # Emit the change event on all the registered display layers. - changeEventsByDisplayLayer = new Map() for id, displayLayer of @displayLayers event = displayLayer.bufferDidChange(changeEvent) - changeEventsByDisplayLayer.set(displayLayer, event) - # Emit a normal `did-change` event for other subscribers too. - @emitter.emit 'did-change', changeEvent - # Emit a `did-change-sync` event from all the registered display layers. - changeEventsByDisplayLayer.forEach (event, displayLayer) -> - displayLayer.emitDidChangeSyncEvent(event) + return # Public: Delete the text in the given range. # @@ -1126,7 +1116,11 @@ class TextBuffer undo: -> if pop = @historyProvider.undo() @emitWillChangeEvent() - @applyChange(change) for change in pop.textUpdates + @transactCallDepth++ + try + @applyChange(change) for change in pop.textUpdates + finally + @transactCallDepth-- @restoreFromMarkerSnapshot(pop.markers) @emitDidChangeTextEvent() @emitMarkerChangeEvents(pop.markers) @@ -1138,7 +1132,11 @@ class TextBuffer redo: -> if pop = @historyProvider.redo() @emitWillChangeEvent() - @applyChange(change) for change in pop.textUpdates + @transactCallDepth++ + try + @applyChange(change) for change in pop.textUpdates + finally + @transactCallDepth-- @restoreFromMarkerSnapshot(pop.markers) @emitDidChangeTextEvent() @emitMarkerChangeEvents(pop.markers) @@ -1210,7 +1208,11 @@ class TextBuffer revertToCheckpoint: (checkpoint, options) -> if truncated = @historyProvider.revertToCheckpoint(checkpoint, options) @emitWillChangeEvent() - @applyChange(change) for change in truncated.textUpdates + @transactCallDepth++ + try + @applyChange(change) for change in truncated.textUpdates + finally + @transactCallDepth-- @restoreFromMarkerSnapshot(truncated.markers) @emitDidChangeTextEvent() @emitter.emit 'did-update-markers' @@ -1906,15 +1908,19 @@ class TextBuffer @_emittedWillChangeEvent = true emitDidChangeTextEvent: -> - if @transactCallDepth is 0 and @changesSinceLastDidChangeTextEvent.length > 0 - compactedChanges = Object.freeze(normalizePatchChanges( - patchFromChanges(@changesSinceLastDidChangeTextEvent).getChanges() - )) - @changesSinceLastDidChangeTextEvent.length = 0 - if compactedChanges.length > 0 - @emitter.emit 'did-change-text', new ChangeEvent(this, compactedChanges) - @debouncedEmitDidStopChangingEvent() - @_emittedWillChangeEvent = false + if @transactCallDepth is 0 + if @changesSinceLastDidChangeTextEvent.length > 0 + compactedChanges = Object.freeze(normalizePatchChanges( + patchFromChanges(@changesSinceLastDidChangeTextEvent).getChanges() + )) + @changesSinceLastDidChangeTextEvent.length = 0 + if compactedChanges.length > 0 + @emitter.emit 'did-change-text', new ChangeEvent(this, compactedChanges) + @debouncedEmitDidStopChangingEvent() + @_emittedWillChangeEvent = false + for id, displayLayer of @displayLayers + displayLayer.emitDeferredChangeEvents() + return # Identifies if the buffer belongs to multiple editors. #