diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 1edefec27..b78803401 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -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() diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 4b233c55d..c8f54c51e 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -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 = "
" @@ -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]] diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 74f97eaa5..00ad78e99 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -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()