diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 0e40ea6b6..e0da60ec8 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -877,6 +877,68 @@ describe "TextEditorPresenter", -> flashCount: 2 } + describe ".overlays", -> + stateForOverlay = (presenter, decoration) -> + presenter.state.content.overlays[decoration.id] + + it "contains state for overlay decorations both initially and when their markers move", -> + item = {} + marker = editor.markBufferPosition([2, 13], invalidate: 'touch') + decoration = editor.decorateMarker(marker, {type: 'overlay', item}) + presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + # Initial state + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 2 * 10, left: 13 * 10} + } + + # Change range + expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]]) + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 4 * 10, left: 6 * 10} + } + + # Valid -> invalid + expectStateUpdate presenter, -> editor.getBuffer().insert([2, 14], 'x') + expect(stateForOverlay(presenter, decoration)).toBeUndefined() + + # Invalid -> valid + expectStateUpdate presenter, -> editor.undo() + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 4 * 10, left: 6 * 10} + } + + # Reverse direction + expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]], reversed: true) + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 2 * 10, left: 13 * 10} + } + + # Destroy + decoration.destroy() + expect(stateForOverlay(presenter, decoration)).toBeUndefined() + + # Add + decoration2 = editor.decorateMarker(marker, {type: 'overlay', item}) + expectValues stateForOverlay(presenter, decoration2), { + item: item + pixelPosition: {top: 2 * 10, left: 13 * 10} + } + + it "honors the 'position' option on overlay decorations", -> + item = {} + marker = editor.markBufferRange([[2, 13], [4, 14]], invalidate: 'touch') + decoration = editor.decorateMarker(marker, {type: 'overlay', position: 'tail', item}) + presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 2 * 10, left: 13 * 10} + } + describe ".gutter", -> describe ".lineNumbers", -> lineNumberStateForScreenRow = (presenter, screenRow) -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 4cd81e923..6315ce67a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -34,6 +34,7 @@ class TextEditorPresenter @observeLineDecoration(decoration) for decoration in @model.getLineDecorations() @observeLineNumberDecoration(decoration) for decoration in @model.getLineNumberDecorations() @observeHighlightDecoration(decoration) for decoration in @model.getHighlightDecorations() + @observeOverlayDecoration(decoration) for decoration in @model.getOverlayDecorations() @observeCursor(cursor) for cursor in @model.getCursors() observeConfig: -> @@ -42,9 +43,12 @@ class TextEditorPresenter buildState: -> @state = content: - lines: {} blinkCursorsOff: false - gutter: {} + lines: {} + highlights: {} + overlays: {} + gutter: + lineNumbers: {} @updateState() updateState: -> @@ -53,6 +57,7 @@ class TextEditorPresenter @updateLinesState() @updateCursorsState() @updateHighlightsState() + @updateOverlaysState() @updateLineNumbersState() updateVerticalScrollState: -> @@ -118,8 +123,6 @@ class TextEditorPresenter @emitter.emit 'did-update-state' updateHighlightsState: -> - @state.content.highlights ?= {} - startRow = @getStartRow() endRow = @getEndRow() visibleHighlights = {} @@ -154,12 +157,33 @@ class TextEditorPresenter @emitter.emit 'did-update-state' + updateOverlaysState: -> + visibleDecorationIds = {} + + for decoration in @model.getOverlayDecorations() + continue unless decoration.getMarker().isValid() + + {item, position} = decoration.getProperties() + if position is 'tail' + screenPosition = decoration.getMarker().getTailScreenPosition() + else + screenPosition = decoration.getMarker().getHeadScreenPosition() + + @state.content.overlays[decoration.id] ?= {item} + @state.content.overlays[decoration.id].pixelPosition = @pixelPositionForScreenPosition(screenPosition) + visibleDecorationIds[decoration.id] = true + + for id of @state.content.overlays + delete @state.content.overlays[id] unless visibleDecorationIds[id] + + @emitter.emit "did-update-state" + updateLineNumbersState: -> - @state.gutter.lineNumbers = {} startRow = @getStartRow() endRow = @getEndRow() lastBufferRow = null wrapCount = 0 + visibleLineNumberIds = {} for bufferRow, i in @model.bufferRowsForScreenRows(startRow, endRow - 1) screenRow = startRow + i @@ -167,16 +191,20 @@ class TextEditorPresenter if bufferRow is lastBufferRow wrapCount++ softWrapped = true - key = bufferRow + '-' + wrapCount + id = bufferRow + '-' + wrapCount else wrapCount = 0 softWrapped = false lastBufferRow = bufferRow - key = bufferRow + id = bufferRow decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) - @state.gutter.lineNumbers[key] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} + @state.gutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} + visibleLineNumberIds[id] = true + + for id of @state.gutter.lineNumbers + delete @state.gutter.lineNumbers[id] unless visibleLineNumberIds[id] @emitter.emit 'did-update-state' @@ -444,6 +472,15 @@ class TextEditorPresenter decorationState.flashDuration = flash.duration @emitter.emit "did-update-state" + observeOverlayDecoration: (decoration) -> + decorationDisposables = new CompositeDisposable + decorationDisposables.add decoration.getMarker().onDidChange(@updateOverlaysState.bind(this)) + decorationDisposables.add decoration.onDidChangeProperties(@updateOverlaysState.bind(this)) + decorationDisposables.add decoration.onDidDestroy => + @disposables.remove(decorationDisposables) + @updateOverlaysState() + @disposables.add(decorationDisposables) + didAddDecoration: (decoration) -> if decoration.isType('line') @observeLineDecoration(decoration) @@ -454,6 +491,9 @@ class TextEditorPresenter else if decoration.isType('highlight') @observeHighlightDecoration(decoration) @updateHighlightsState() + else if decoration.isType('overlay') + @observeOverlayDecoration(decoration) + @updateOverlaysState() observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition =>