diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 561cd6565..d164fb0e7 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -359,7 +359,7 @@ describe "EditorComponent", -> editor.addDecorationToBufferRow(2, type: 'gutter', class: 'fancy-class') editor.addDecorationToBufferRow(2, type: 'someother-type', class: 'nope-class') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'fancy-class')).toBe true expect(lineNumberHasClass(2, 'nope-class')).toBe false @@ -367,7 +367,7 @@ describe "EditorComponent", -> editor.removeDecorationFromBufferRow(2, type: 'gutter', class: 'fancy-class') editor.removeDecorationFromBufferRow(2, type: 'someother-type', class: 'nope-class') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'fancy-class')).toBe false expect(lineNumberHasClass(2, 'nope-class')).toBe false @@ -390,7 +390,7 @@ describe "EditorComponent", -> editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'no-wrap') editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'wrap-me') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'no-wrap')).toBe false expect(lineNumberHasClass(2, 'wrap-me')).toBe false @@ -401,7 +401,7 @@ describe "EditorComponent", -> editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true) - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'no-wrap')).toBe true expect(lineNumberHasClass(2, 'wrap-me')).toBe true @@ -414,7 +414,7 @@ describe "EditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') decoration = {type: 'gutter', class: 'someclass'} editor.addDecorationForMarker(marker, decoration) - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? it "updates line number classes when the marker moves", -> expect(lineNumberHasClass(1, 'someclass')).toBe false @@ -424,7 +424,7 @@ describe "EditorComponent", -> editor.getBuffer().insert([0, 0], '\n') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'someclass')).toBe false expect(lineNumberHasClass(3, 'someclass')).toBe true @@ -433,7 +433,7 @@ describe "EditorComponent", -> editor.getBuffer().deleteRows(0, 1) - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(0, 'someclass')).toBe false expect(lineNumberHasClass(1, 'someclass')).toBe true @@ -443,7 +443,7 @@ describe "EditorComponent", -> it "removes line number classes when a decoration's marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(marker.isValid()).toBe false @@ -454,7 +454,7 @@ describe "EditorComponent", -> editor.getBuffer().undo() - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(marker.isValid()).toBe true expect(lineNumberHasClass(1, 'someclass')).toBe false @@ -465,7 +465,7 @@ describe "EditorComponent", -> it "removes the classes and unsubscribes from the marker when decoration is removed", -> editor.removeDecorationForMarker(marker, decoration) - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(1, 'someclass')).toBe false expect(lineNumberHasClass(2, 'someclass')).toBe false @@ -474,7 +474,7 @@ describe "EditorComponent", -> editor.getBuffer().insert([0, 0], '\n') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'someclass')).toBe false expect(lineNumberHasClass(3, 'someclass')).toBe false @@ -482,7 +482,7 @@ describe "EditorComponent", -> it "removes the line number classes when the decoration's marker is destroyed", -> marker.destroy() - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(1, 'someclass')).toBe false expect(lineNumberHasClass(2, 'someclass')).toBe false diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 1d1006290..1810cf711 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -49,6 +49,7 @@ EditorComponent = React.createClass [renderedStartRow, renderedEndRow] = renderedRowRange cursorScreenRanges = @getCursorScreenRanges(renderedRowRange) selectionScreenRanges = @getSelectionScreenRanges(renderedRowRange) + decorations = @getGutterDecorations(renderedRowRange) scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -71,7 +72,8 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, - scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow + scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow, + decorations } div ref: 'scrollView', className: 'scroll-view', onMouseDown: @onMouseDown, @@ -225,6 +227,19 @@ EditorComponent = React.createClass selectionScreenRanges + getGutterDecorations: (renderedRowRange) -> + {editor} = @props + [renderedStartRow, renderedEndRow] = renderedRowRange + + bufferRows = editor.bufferRowsForScreenRows(renderedStartRow, renderedEndRow - 1) + + decorations = {} + for bufferRow in bufferRows + decorations[bufferRow] = editor.decorationsForBufferRow(bufferRow, 'gutter') + decorations[bufferRow].push {class: 'foldable'} if editor.isFoldableAtBufferRow(bufferRow) + decorations[bufferRow].push {class: 'folded'} if editor.isFoldedAtBufferRow(bufferRow) + decorations + observeEditor: -> {editor} = @props @subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted @@ -233,6 +248,7 @@ EditorComponent = React.createClass @subscribe editor, 'cursors-moved', @onCursorsMoved @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged @subscribe editor, 'selection-added', @onSelectionAdded + @subscribe editor, 'decoration-changed', @onDecorationChanged @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.changes, @requestUpdate @subscribe editor.$height.changes, @requestUpdate @@ -510,6 +526,11 @@ EditorComponent = React.createClass onCursorsMoved: -> @cursorsMoved = true + onDecorationChanged: -> + @decorationChangedImmediate = setImmediate => + @requestUpdate() + @decorationChangedImmediate = null + selectToMousePositionUntilMouseUp: (event) -> {editor} = @props dragging = false diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 84920399e..5dff8609d 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -1,3 +1,4 @@ +_ = require 'underscore-plus' React = require 'react-atom-fork' {div} = require 'reactionary-atom-fork' {isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' @@ -24,17 +25,10 @@ GutterComponent = React.createClass @lineNumberNodesById = {} @lineNumberIdsByScreenRow = {} @screenRowsByLineNumberId = {} - @decoratorUpdates = {} + @previousDecorations = {} componentDidMount: -> @appendDummyLineNumber() - @subscribeToEditor() - - componentWillUnmount: -> - @unsubscribe() - - subscribeToEditor: -> - @subscribe @props.editor, 'decoration-changed', @onDecorationChanged # Only update the gutter if the visible row range has changed or if a # non-zero-delta change to the screen lines has occurred within the current @@ -44,10 +38,12 @@ GutterComponent = React.createClass 'renderedRowRange', 'scrollTop', 'lineHeightInPixels', 'mouseWheelScreenRow' ) - {renderedRowRange, pendingChanges} = newProps + {renderedRowRange, pendingChanges, decorations} = newProps for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0 return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start + return true unless _.isEqual(@previousDecorations, decorations) + false componentDidUpdate: (oldProps) -> @@ -78,7 +74,7 @@ GutterComponent = React.createClass @removeLineNumberNodes(lineNumberIdsToPreserve) appendOrUpdateVisibleLineNumberNodes: -> - {editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props + {editor, renderedRowRange, scrollTop, maxLineNumberDigits, decorations} = @props [startRow, endRow] = renderedRowRange newLineNumberIds = null @@ -99,12 +95,12 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) if @hasLineNumberNode(id) - @updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0) + @updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0, decorations[bufferRow]) else newLineNumberIds ?= [] newLineNumbersHTML ?= "" newLineNumberIds.push(id) - newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow) + newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow, decorations[bufferRow]) @screenRowsByLineNumberId[id] = screenRow @lineNumberIdsByScreenRow[screenRow] = id @@ -118,7 +114,7 @@ GutterComponent = React.createClass @lineNumberNodesById[lineNumberId] = lineNumberNode node.appendChild(lineNumberNode) - @decoratorUpdates = {} + @previousDecorations = decorations visibleLineNumberIds removeLineNumberNodes: (lineNumberIdsToPreserve) -> @@ -132,7 +128,7 @@ GutterComponent = React.createClass delete @screenRowsByLineNumberId[lineNumberId] node.removeChild(lineNumberNode) - buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) -> + buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow, decorations) -> if screenRow? {lineHeightInPixels} = @props style = "position: absolute; top: #{screenRow * lineHeightInPixels}px;" @@ -140,15 +136,13 @@ GutterComponent = React.createClass style = "visibility: hidden;" innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - classes = ['line-number'] - classes.push 'foldable' if not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) - classes.push 'folded' if @props.editor.isFoldedAtBufferRow(bufferRow) + classes = '' + if decorations? + for decoration in decorations + classes += decoration.class + ' ' if not softWrapped or softWrapped and decoration.softWrap + classes += 'line-number' - decorations = @props.editor.decorationsForBufferRow(bufferRow, 'gutter') - for decoration in decorations - classes.push(decoration.class) if not softWrapped or softWrapped and decoration.softWrap - - "
#{innerHTML}
" + "
#{innerHTML}
" buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped @@ -160,18 +154,17 @@ GutterComponent = React.createClass iconHTML = '
' padding + lineNumber + iconHTML - updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped) -> + updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped, decorations) -> node = @lineNumberNodesById[lineNumberId] + previousDecorations = @previousDecorations[bufferRow] - @toggleClass node, 'foldable', not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) - @toggleClass node, 'folded', @props.editor.isFoldedAtBufferRow(bufferRow) + if previousDecorations? + for decoration in previousDecorations + node.classList.remove(decoration.class) if not contains(decorations, decoration) - if @decoratorUpdates[bufferRow]? - for change in @decoratorUpdates[bufferRow] - if change.action == 'add' and (not softWrapped or softWrapped and change.decoration.softWrap) - node.classList.add(change.decoration.class) - else if change.action == 'remove' - node.classList.remove(change.decoration.class) + for decoration in decorations + if not contains(previousDecorations, decoration) and (not softWrapped or softWrapped and decoration.softWrap) + node.classList.add(decoration.class) unless @screenRowsByLineNumberId[lineNumberId] is screenRow {lineHeightInPixels} = @props @@ -186,18 +179,9 @@ GutterComponent = React.createClass lineNumberNodeForScreenRow: (screenRow) -> @lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]] - toggleClass: (node, klass, condition) -> - if condition then node.classList.add(klass) else node.classList.remove(klass) - - onDecorationChanged: (change) -> - if change.decoration.type is 'gutter' - @decoratorUpdates[change.bufferRow] ?= [] - @decoratorUpdates[change.bufferRow].push change - @renderDecorations() - - renderDecorations: -> - clearImmediate(@decorationRenderImmediate) if @decorationRenderImmediate - render = => - @forceUpdate() - @decorationRenderImmediate = null - @decorationRenderImmediate = setImmediate(render) +# Created because underscore uses === not _.isEqual, which we need +contains = (array, target) -> + return false unless array? + for object in array + return true if _.isEqual(object, target) + false