Handle updates to line decorations in TextEditorPresenter

This isn’t a super efficient approach, but it is simple and should be
correct. Once we move all state to the presenter we can perform a more
efficient synchronous update when markers change.
This commit is contained in:
Nathan Sobo
2015-01-21 13:48:49 -07:00
parent 5d8f831136
commit 773482467e
3 changed files with 98 additions and 29 deletions

View File

@@ -272,3 +272,55 @@ describe "TextEditorPresenter", ->
text: line3.text
tokens: line3.tokens
}
describe "when line decorations are added, updated, or destroyed", ->
it "updates the .decorationClasses of the relevant lines", ->
marker1 = editor.markBufferRange([[4, 0], [6, 0]], invalidate: 'touch')
decoration1 = editor.decorateMarker(marker1, type: 'line', class: 'a')
presenter = new TextEditorPresenter(model: editor, clientHeight: 130, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 0)
marker2 = editor.markBufferRange([[4, 0], [6, 0]], invalidate: 'touch')
decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
editor.getBuffer().insert([5, 0], 'x')
expect(marker1.isValid()).toBe false
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
editor.undo()
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
marker1.setBufferRange([[2, 0], [4, 0]])
expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
decoration1.destroy()
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
marker2.destroy()
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()

View File

@@ -4,7 +4,6 @@ React = require 'react-atom-fork'
{debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
{$$} = require 'space-pen'
Decoration = require './decoration'
CursorsComponent = require './cursors-component'
HighlightsComponent = require './highlights-component'
OverlayManager = require './overlay-manager'
@@ -140,13 +139,12 @@ LinesComponent = React.createClass
buildLineHTML: (id) ->
{presenter, lineDecorations} = @props
{scrollWidth} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel} = @newState.lines[id]
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id]
classes = ''
if decorations = lineDecorations[screenRow]
for decorationId, decoration of decorations
if Decoration.isType(decoration, 'line')
classes += decoration.class + ' '
if decorationClasses?
for decorationClass in decorationClasses
classes += decorationClass + ' '
classes += 'line'
lineHTML = "<div class=\"#{classes}\" style=\"position: absolute; top: #{top}px; width: #{scrollWidth}px;\" data-screen-row=\"#{screenRow}\">"
@@ -243,18 +241,18 @@ LinesComponent = React.createClass
lineNode = @lineNodesByLineId[id]
decorations = lineDecorations[screenRow]
previousDecorations = @renderedDecorationsByLineId[id]
newDecorationClasses = @newState.lines[id].decorationClasses
oldDecorationClasses = @oldState.lines[id].decorationClasses
if previousDecorations?
for decorationId, decoration of previousDecorations
if Decoration.isType(decoration, 'line') and not @hasDecoration(decorations, decoration)
lineNode.classList.remove(decoration.class)
if oldDecorationClasses?
for decorationClass in oldDecorationClasses
unless newDecorationClasses? and decorationClass in newDecorationClasses
lineNode.classList.remove(decorationClass)
if decorations?
for decorationId, decoration of decorations
if Decoration.isType(decoration, 'line') and not @hasDecoration(previousDecorations, decoration)
lineNode.classList.add(decoration.class)
if newDecorationClasses?
for decorationClass in newDecorationClasses
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
lineNode.classList.add(decorationClass)
lineNode.style.width = scrollWidth + 'px'
lineNode.style.top = top + 'px'
@@ -262,9 +260,6 @@ LinesComponent = React.createClass
@screenRowsByLineId[id] = screenRow
@lineIdsByScreenRow[screenRow] = id
hasDecoration: (decorations, decoration) ->
decorations? and decorations[decoration.id] is decoration
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]

View File

@@ -15,10 +15,10 @@ class TextEditorPresenter
observeModel: ->
@disposables.add @model.onDidChange(@updateLinesState.bind(this))
@disposables.add @model.onDidChangeSoftWrapped =>
@updateContentState()
@updateLinesState()
@disposables.add @model.onDidChangeSoftWrapped(@updateState.bind(this))
@disposables.add @model.onDidChangeGrammar(@updateContentState.bind(this))
@disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
@observeDecoration(decoration) for decoration in @model.getLineDecorations()
observeConfig: ->
@disposables.add atom.config.onDidChange 'editor.showIndentGuide', scope: @model.getRootScopeDescriptor(), @updateContentState.bind(this)
@@ -37,6 +37,10 @@ class TextEditorPresenter
@state.content.lines = {}
@updateLinesState()
updateState: ->
@updateContentState()
@updateLinesState()
updateContentState: ->
@state.content.scrollWidth = @computeScrollWidth()
@state.content.indentGuidesVisible = atom.config.get('editor.showIndentGuide', scope: @model.getRootScopeDescriptor())
@@ -64,14 +68,9 @@ class TextEditorPresenter
lineState = @state.content.lines[line.id]
lineState.screenRow = row
lineState.top = row * @getLineHeight()
lineState.decorationClasses = @lineDecorationClassesForRow(row)
buildLineState: (row, line) ->
decorationClasses = null
for markerId, decorations of @model.decorationsForScreenRowRange(row, row) when @model.getMarker(markerId).isValid()
for decoration in decorations when decoration.isType('line')
decorationClasses ?= []
decorationClasses.push(decoration.getProperties().class)
@state.content.lines[line.id] =
screenRow: row
text: line.text
@@ -81,7 +80,7 @@ class TextEditorPresenter
tabLength: line.tabLength
fold: line.fold
top: row * @getLineHeight()
decorationClasses: decorationClasses
decorationClasses: @lineDecorationClassesForRow(row)
getStartRow: ->
startRow = Math.floor(@getScrollTop() / @getLineHeight()) - @lineOverdrawMargin
@@ -98,6 +97,14 @@ class TextEditorPresenter
contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
Math.max(contentWidth, @getClientWidth())
lineDecorationClassesForRow: (row) ->
decorationClasses = null
for markerId, decorations of @model.decorationsForScreenRowRange(row, row) when @model.getMarker(markerId).isValid()
for decoration in decorations when decoration.isType('line')
decorationClasses ?= []
decorationClasses.push(decoration.getProperties().class)
decorationClasses
setScrollTop: (@scrollTop) ->
@updateLinesState()
@@ -186,3 +193,18 @@ class TextEditorPresenter
left += charWidths[char] ? baseCharacterWidth unless char is '\0'
column += charLength
{top, left}
observeDecoration: (decoration) ->
markerChangeDisposable = decoration.getMarker().onDidChange(@updateLinesState.bind(this))
destroyDisposable = decoration.onDidDestroy =>
@disposables.remove(markerChangeDisposable)
@disposables.remove(destroyDisposable)
@updateLinesState()
@disposables.add(markerChangeDisposable)
@disposables.add(destroyDisposable)
didAddDecoration: (decoration) ->
if decoration.isType('line')
@observeDecoration(decoration)
@updateLinesState()