diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 140ca2f9e..4c48607a5 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -1207,6 +1207,50 @@ describe "DisplayBuffer", -> expect(markerCreated1).toHaveBeenCalled() expect(markerCreated2).not.toHaveBeenCalled() + describe "::observeMarkers(callback)", -> + [observationWindow, events] = [] + + beforeEach -> + events = [] + observationWindow = displayBuffer.observeMarkers (event) -> events.push(event) + displayBuffer.unfoldBufferRow(4, 7) + + it "calls the callback when markers enter, leave, or move within the screen range", -> + expect(events).toHaveLength 0 + + observationWindow.setScreenRange([[0, 0], [4, 0]]) + expect(events).toHaveLength 0 + + marker1 = displayBuffer.markScreenPosition([4, 2]) + expect(events).toHaveLength 0 + + observationWindow.setScreenRange([[0, 0], [5, 0]]) + expect(events).toHaveLength 1 + expect(events[0]).toEqual { + insert: new Set([marker1.id]) + update: new Set + remove: new Set + } + + marker2 = displayBuffer.markScreenPosition([5, 2]) + expect(events).toHaveLength 1 + + observationWindow.setBufferRange([[1, 0], [6, 0]]) + expect(events).toHaveLength 2 + expect(events[1]).toEqual { + insert: new Set([marker2.id]) + update: new Set([marker1.id]) + remove: new Set + } + + marker1.destroy() + expect(events).toHaveLength 3 + expect(events[2]).toEqual { + insert: new Set + update: new Set + remove: new Set([marker1.id]) + } + describe "decorations", -> [marker, decoration, decorationProperties] = [] beforeEach -> diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index b2460addc..b361e416b 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -10,6 +10,7 @@ Model = require './model' Token = require './token' Decoration = require './decoration' Marker = require './marker' +MarkerObservationWindow = require './marker-observation-window' class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> @@ -950,6 +951,9 @@ class DisplayBuffer extends Model @emitter.emit 'did-remove-decoration', decoration delete @decorationsByMarkerId[marker.id] if decorations.length is 0 + decorationsForMarkerId: (markerId) -> + @decorationsByMarkerId[markerId] + # Retrieves a {Marker} based on its id. # # id - A {Number} representing a marker id @@ -1045,6 +1049,9 @@ class DisplayBuffer extends Model params = @translateToBufferMarkerParams(params) @buffer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) + observeMarkers: (callback) -> + new MarkerObservationWindow(this, @buffer.observeMarkers(callback)) + translateToBufferMarkerParams: (params) -> bufferMarkerParams = {} for key, value of params diff --git a/src/marker-observation-window.coffee b/src/marker-observation-window.coffee new file mode 100644 index 000000000..aa7b71f69 --- /dev/null +++ b/src/marker-observation-window.coffee @@ -0,0 +1,12 @@ +module.exports = +class MarkerObservationWindow + constructor: (@displayBuffer, @bufferWindow) -> + + setScreenRange: (range) -> + @bufferWindow.setRange(@displayBuffer.bufferRangeForScreenRange(range)) + + setBufferRange: (range) -> + @bufferWindow.setRange(range) + + destroy: -> + @bufferWindow.destroy() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 3aea57f29..e3e3ad5a7 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1,4 +1,4 @@ -{CompositeDisposable, Emitter} = require 'event-kit' +{CompositeDisposable, Disposable, Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' _ = require 'underscore-plus' Decoration = require './decoration' @@ -24,6 +24,11 @@ class TextEditorPresenter @disposables = new CompositeDisposable @emitter = new Emitter @characterWidthsByScope = {} + @rangesByDecorationId = {} + @lineDecorationsByScreenRow = {} + @lineNumberDecorationsByScreenRow = {} + @customGutterDecorationsByGutterNameAndScreenRow = {} + @highlightDecorationsById = {} @transferMeasurementsToModel() @observeModel() @observeConfig() @@ -121,6 +126,10 @@ class TextEditorPresenter @shouldUpdateCustomGutterDecorationState = true @emitDidUpdateState() + + @markerObservationWindow = @model.observeMarkers(@markersInRangeDidChange.bind(this)) + @disposables.add new Disposable => @markerObservationWindow.destroy() + @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) @disposables.add @model.onDidChangePlaceholderText => @shouldUpdateContentState = true @@ -579,6 +588,7 @@ class TextEditorPresenter visibleLinesCount = Math.ceil(@height / @lineHeight) + 1 endRow = startRow + visibleLinesCount + @lineOverdrawMargin @endRow = Math.min(@model.getScreenLineCount(), endRow) + @markerObservationWindow.setScreenRange(Range(Point(@startRow, 0), Point(@endRow, 0))) updateScrollWidth: -> return unless @contentWidth? and @clientWidth? @@ -1055,7 +1065,6 @@ class TextEditorPresenter observeDecoration: (decoration) -> decorationDisposables = new CompositeDisposable - decorationDisposables.add decoration.getMarker().onDidChange(@decorationMarkerDidChange.bind(this, decoration)) if decoration.isType('highlight') decorationDisposables.add decoration.onDidFlash(@highlightDidFlash.bind(this, decoration)) decorationDisposables.add decoration.onDidChangeProperties(@decorationPropertiesDidChange.bind(this, decoration)) @@ -1065,38 +1074,41 @@ class TextEditorPresenter @didDestroyDecoration(decoration) @disposables.add(decorationDisposables) - decorationMarkerDidChange: (decoration, change) -> - if decoration.isType('line') or decoration.isType('gutter') - return if change.textChanged - - intersectsVisibleRowRange = false - oldRange = new Range(change.oldTailScreenPosition, change.oldHeadScreenPosition) - newRange = new Range(change.newTailScreenPosition, change.newHeadScreenPosition) - - if oldRange.intersectsRowRange(@startRow, @endRow - 1) - @removeFromLineDecorationCaches(decoration, oldRange) - intersectsVisibleRowRange = true - - if newRange.intersectsRowRange(@startRow, @endRow - 1) - @addToLineDecorationCaches(decoration, newRange) - intersectsVisibleRowRange = true - - if intersectsVisibleRowRange - @shouldUpdateLinesState = true if decoration.isType('line') - if decoration.isType('line-number') - @shouldUpdateLineNumbersState = true - else if decoration.isType('gutter') - @shouldUpdateCustomGutterDecorationState = true + markersInRangeDidChange: (event) -> + event.insert.forEach (markerId) => + range = @model.getMarker(markerId).getScreenRange() + if decorations = @model.decorationsForMarkerId(markerId) + for decoration in decorations + @decorationMarkerDidChange(decoration) + if decoration.isType('line') or decoration.isType('gutter') + @addToLineDecorationCaches(decoration, range) + event.update.forEach (markerId) => + range = @model.getMarker(markerId).getScreenRange() + if decorations = @model.decorationsForMarkerId(markerId) + for decoration in decorations + @decorationMarkerDidChange(decoration) + if decoration.isType('line') or decoration.isType('gutter') + @removeFromLineDecorationCaches(decoration) + @addToLineDecorationCaches(decoration, range) + event.remove.forEach (markerId) => + if decorations = @model.decorationsForMarkerId(markerId) + for decoration in decorations + @decorationMarkerDidChange(decoration) + if decoration.isType('line') or decoration.isType('gutter') + @removeFromLineDecorationCaches(decoration) + @emitDidUpdateState() + decorationMarkerDidChange: (decoration) -> if decoration.isType('highlight') - return if change.textChanged - @updateHighlightState(decoration) - if decoration.isType('overlay') @shouldUpdateOverlaysState = true - - @emitDidUpdateState() + if decoration.isType('line') + @shouldUpdateLinesState = true + if decoration.isType('line-number') + @shouldUpdateLineNumbersState = true + else if decoration.isType('gutter') + @shouldUpdateCustomGutterDecorationState = true decorationPropertiesDidChange: (decoration, event) -> {oldProperties} = event @@ -1161,6 +1173,7 @@ class TextEditorPresenter @emitDidUpdateState() updateDecorations: -> + @rangesByDecorationId = {} @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} @@ -1183,16 +1196,19 @@ class TextEditorPresenter return - removeFromLineDecorationCaches: (decoration, range) -> - @removePropertiesFromLineDecorationCaches(decoration.id, decoration.getProperties(), range) + removeFromLineDecorationCaches: (decoration) -> + @removePropertiesFromLineDecorationCaches(decoration.id, decoration.getProperties()) - removePropertiesFromLineDecorationCaches: (decorationId, decorationProperties, range) -> - gutterName = decorationProperties.gutterName - for row in [range.start.row..range.end.row] by 1 - delete @lineDecorationsByScreenRow[row]?[decorationId] - delete @lineNumberDecorationsByScreenRow[row]?[decorationId] - delete @customGutterDecorationsByGutterNameAndScreenRow[gutterName]?[row]?[decorationId] if gutterName - return + removePropertiesFromLineDecorationCaches: (decorationId, decorationProperties) -> + if range = @rangesByDecorationId[decorationId] + delete @rangesByDecorationId[decorationId] + + gutterName = decorationProperties.gutterName + for row in [range.start.row..range.end.row] by 1 + delete @lineDecorationsByScreenRow[row]?[decorationId] + delete @lineNumberDecorationsByScreenRow[row]?[decorationId] + delete @customGutterDecorationsByGutterNameAndScreenRow[gutterName]?[row]?[decorationId] if gutterName + return addToLineDecorationCaches: (decoration, range) -> marker = decoration.getMarker() @@ -1206,6 +1222,8 @@ class TextEditorPresenter return if properties.onlyEmpty omitLastRow = range.end.column is 0 + @rangesByDecorationId[decoration.id] = range + for row in [range.start.row..range.end.row] by 1 continue if properties.onlyHead and row isnt marker.getHeadScreenPosition().row continue if omitLastRow and row is range.end.row diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6d979fdf7..edfc17e0d 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1378,6 +1378,9 @@ class TextEditor extends Model decorationForId: (id) -> @displayBuffer.decorationForId(id) + decorationsForMarkerId: (id) -> + @displayBuffer.decorationsForMarkerId(id) + ### Section: Markers ### @@ -1480,6 +1483,25 @@ class TextEditor extends Model findMarkers: (properties) -> @displayBuffer.findMarkers(properties) + # Extended: Observe changes in the set of markers that intersect a particular + # region of the editor. + # + # * `callback` A {Function} to call whenever one or more {Marker}s appears, + # disappears, or moves within the given region. + # * `event` An {Object} with the following keys: + # * `insert` A {Set} containing the ids of all markers that appeared + # in the range. + # * `update` A {Set} containing the ids of all markers that moved within + # the region. + # * `remove` A {Set} containing the ids of all markers that disappeared + # from the region. + # + # Returns a {MarkerObservationWindow}, which allows you to specify the region + # of interest by calling {MarkerObservationWindow::setBufferRange} or + # {MarkerObservationWindow::setScreenRange}. + observeMarkers: (callback) -> + @displayBuffer.observeMarkers(callback) + # Extended: Get the {Marker} for the given marker id. # # * `id` {Number} id of the marker