From 8ebd057b0c953ea0d36841dade3f1ebdecd67d01 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 23 Jan 2015 17:48:27 -0700 Subject: [PATCH] Use presenter to render highlights Signed-off-by: Max Brunsfeld --- spec/text-editor-component-spec.coffee | 2 +- spec/text-editor-presenter-spec.coffee | 41 +++++++++++ src/highlight-component.coffee | 97 +++++++------------------- src/highlights-component.coffee | 9 +-- src/lines-component.coffee | 2 +- src/text-editor-presenter.coffee | 38 ++++++---- 6 files changed, 95 insertions(+), 94 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 0f6aee2ad..0c84703eb 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1104,7 +1104,7 @@ describe "TextEditorComponent", -> # Nothing when outside the rendered row range expect(regions.length).toBe 0 - verticalScrollbarNode.scrollTop = 3.5 * lineHeightInPixels + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 130e9fba2..d58f425e9 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -666,6 +666,16 @@ describe "TextEditorPresenter", -> expect(stateForHighlight(presenter, highlight7)).toBeUndefined() expect(stateForHighlight(presenter, highlight8)).toBeUndefined() + it "does not include highlights for invalid markers", -> + marker = editor.markBufferRange([[2, 2], [2, 4]], invalidate: 'touch') + highlight = editor.decorateMarker(marker, type: 'highlight', class: 'h') + + presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + expect(stateForHighlight(presenter, highlight)).toBeDefined() + expectStateUpdate presenter, -> editor.getBuffer().insert([2, 2], "stuff") + expect(stateForHighlight(presenter, highlight)).toBeUndefined() + it "updates when ::scrollTop changes", -> editor.setSelectedBufferRanges([ [[6, 2], [6, 4]], @@ -803,3 +813,34 @@ describe "TextEditorPresenter", -> destroyedSelection = editor.getSelections()[2] expectStateUpdate presenter, -> destroyedSelection.destroy() expect(stateForHighlight(presenter, destroyedSelection.decoration)).toBeUndefined() + + it "updates when highlight decorations' properties are updated", -> + marker = editor.markBufferRange([[2, 2], [2, 4]]) + highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a') + + presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + expectValues stateForHighlight(presenter, highlight), {class: 'a'} + expectStateUpdate presenter, -> highlight.setProperties(class: 'b', type: 'highlight') + expectValues stateForHighlight(presenter, highlight), {class: 'b'} + + it "increments the .flashCount and sets the .flashClass and .flashDuration when the highlight model flashes", -> + presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + marker = editor.markBufferRange([[2, 2], [2, 4]]) + highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a') + expectStateUpdate presenter, -> highlight.flash('b', 500) + + expectValues stateForHighlight(presenter, highlight), { + flashClass: 'b' + flashDuration: 500 + flashCount: 1 + } + + expectStateUpdate presenter, -> highlight.flash('c', 600) + + expectValues stateForHighlight(presenter, highlight), { + flashClass: 'c' + flashDuration: 600 + flashCount: 2 + } diff --git a/src/highlight-component.coffee b/src/highlight-component.coffee index ec8aebf7e..53075ab0a 100644 --- a/src/highlight-component.coffee +++ b/src/highlight-component.coffee @@ -5,94 +5,45 @@ React = require 'react-atom-fork' module.exports = HighlightComponent = React.createClass displayName: 'HighlightComponent' + lastFlashCount: 0 + lastFlashClass: null render: -> - {startPixelPosition, endPixelPosition, decoration} = @props + {editor, state} = @props className = 'highlight' - className += " #{decoration.class}" if decoration.class? + className += " #{state.class}" if state.class? div {className}, - if endPixelPosition.top is startPixelPosition.top - @renderSingleLineRegions(decoration.deprecatedRegionClass) - else - @renderMultiLineRegions(decoration.deprecatedRegionClass) + for region, i in state.regions + regionClassName = 'region' + regionClassName += " #{state.deprecatedRegionClass}" if state.deprecatedRegionClass? + div className: regionClassName, key: i, style: region + + componentDidUpdate: -> + if @props.state.flashCount > @lastFlashCount + @startFlashAnimation() + @lastFlashCount = @props.state.flashCount + @lastFlashClass = @props.state.flashClass componentDidMount: -> - {editor, decoration} = @props - if decoration.id? - @decoration = editor.decorationForId(decoration.id) - @decorationDisposable = @decoration.onDidFlash @startFlashAnimation - @startFlashAnimation() + {key} = @props + presenter.onDidFlashHighlight @startFlashAnimation.bind(this) componentWillUnmount: -> @decorationDisposable?.dispose() @decorationDisposable = null startFlashAnimation: -> - return unless flash = @decoration.consumeNextFlash() - node = @getDOMNode() - node.classList.remove(flash.class) + + if @lastFlashClass? + clearTimeout(@flashTimeoutId) + node.classList.remove(@lastFlashClass) + @lastFlashClass = null requestAnimationFrame => - node.classList.add(flash.class) - clearTimeout(@flashTimeoutId) - removeFlashClass = -> node.classList.remove(flash.class) + flashClass = @props.state.flashClass + node.classList.add(flashClass) + removeFlashClass = -> node.classList.remove(flashClass) @flashTimeoutId = setTimeout(removeFlashClass, flash.duration) - - renderSingleLineRegions: (regionClass) -> - {startPixelPosition, endPixelPosition, lineHeightInPixels} = @props - - className = 'region' - className += " #{regionClass}" if regionClass? - - [ - div className: className, key: 0, style: - top: startPixelPosition.top - height: lineHeightInPixels - left: startPixelPosition.left - width: endPixelPosition.left - startPixelPosition.left - ] - - renderMultiLineRegions: (regionClass) -> - {startPixelPosition, endPixelPosition, lineHeightInPixels} = @props - - className = 'region' - className += " #{regionClass}" if regionClass? - - regions = [] - index = 0 - - # First row, extending from selection start to the right side of screen - regions.push( - div className: className, key: index++, style: - top: startPixelPosition.top - left: startPixelPosition.left - height: lineHeightInPixels - right: 0 - ) - - # Middle rows, extending from left side to right side of screen - if endPixelPosition.top - startPixelPosition.top > lineHeightInPixels - regions.push( - div className: className, key: index++, style: - top: startPixelPosition.top + lineHeightInPixels - height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels - left: 0 - right: 0 - ) - - # Last row, extending from left side of screen to selection end - regions.push( - div className: className, key: index, style: - top: endPixelPosition.top - height: lineHeightInPixels - left: 0 - width: endPixelPosition.left - ) - - regions - - shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'startPixelPosition', 'endPixelPosition', 'lineHeightInPixels', 'decoration') diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index 8820830bd..e4d465b67 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -12,13 +12,10 @@ HighlightsComponent = React.createClass @renderHighlights() if @props.performedInitialMeasurement renderHighlights: -> - {editor, highlightDecorations, lineHeightInPixels} = @props - + {editor, presenter} = @props highlightComponents = [] - for markerId, {startPixelPosition, endPixelPosition, decorations} of highlightDecorations - for decoration in decorations - highlightComponents.push(HighlightComponent({editor, key: "#{markerId}-#{decoration.id}", startPixelPosition, endPixelPosition, decoration, lineHeightInPixels})) - + for key, state of presenter.state.content.highlights + highlightComponents.push(HighlightComponent({editor, key, state})) highlightComponents componentDidMount: -> diff --git a/src/lines-component.coffee b/src/lines-component.coffee index b110e6399..37d6339df 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -40,7 +40,7 @@ LinesComponent = React.createClass HighlightsComponent { editor, highlightDecorations, lineHeightInPixels, defaultCharWidth, - scopedCharacterWidthsChangeCount, performedInitialMeasurement + scopedCharacterWidthsChangeCount, performedInitialMeasurement, presenter } getTransform: -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index cd5ae92b6..d096e6266 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -115,6 +115,7 @@ class TextEditorPresenter @state.content.highlights = {} for decoration in @model.getHighlightDecorations() + continue unless decoration.getMarker().isValid() screenRange = decoration.getMarker().getScreenRange() if screenRange.intersectsRowRange(startRow, endRow - 1) if screenRange.start.row < startRow @@ -126,7 +127,12 @@ class TextEditorPresenter continue if screenRange.isEmpty() @state.content.highlights[decoration.id] = class: decoration.getProperties().class + deprecatedRegionClass: decoration.getProperties().deprecatedRegionClass regions: @buildHighlightRegions(screenRange) + flashCount: 0 + flashDuration: null + flashClass: null + @emitter.emit 'did-update-state' buildHighlightRegions: (screenRange) -> lineHeightInPixels = @getLineHeight() @@ -331,24 +337,30 @@ class TextEditorPresenter {top, left, width, height} observeLineDecoration: (decoration) -> - markerDidChangeDisposable = decoration.getMarker().onDidChange(@updateLinesState.bind(this)) - didDestroyDisposable = decoration.onDidDestroy => - @disposables.remove(markerDidChangeDisposable) - @disposables.remove(didDestroyDisposable) + decorationDisposables = new CompositeDisposable + decorationDisposables.add decoration.getMarker().onDidChange(@updateLinesState.bind(this)) + decorationDisposables.add decoration.onDidDestroy => + @disposables.remove(decorationDisposables) @updateLinesState() - - @disposables.add(markerDidChangeDisposable) - @disposables.add(didDestroyDisposable) + @disposables.add(decorationDisposables) observeHighlightDecoration: (decoration) -> - markerDidChangeDisposable = decoration.getMarker().onDidChange(@updateHighlightsState.bind(this)) - didDestroyDisposable = decoration.onDidDestroy => - @disposables.remove(markerDidChangeDisposable) - @disposables.remove(didDestroyDisposable) + decorationDisposables = new CompositeDisposable + decorationDisposables.add decoration.getMarker().onDidChange(@updateHighlightsState.bind(this)) + decorationDisposables.add decoration.onDidChangeProperties(@updateHighlightsState.bind(this)) + decorationDisposables.add decoration.onDidFlash(@highlightDidFlash.bind(this, decoration)) + decorationDisposables.add decoration.onDidDestroy => + @disposables.remove(decorationDisposables) @updateHighlightsState() + @disposables.add(decorationDisposables) - @disposables.add(markerDidChangeDisposable) - @disposables.add(didDestroyDisposable) + highlightDidFlash: (decoration) -> + flash = decoration.consumeNextFlash() + decorationState = @state.content.highlights[decoration.id] + decorationState.flashCount++ + decorationState.flashClass = flash.class + decorationState.flashDuration = flash.duration + @emitter.emit "did-update-state" didAddDecoration: (decoration) -> if decoration.isType('line')