From b8ac8516fecd80c116714dd1346732f962a17ea6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 8 May 2014 21:21:59 -0600 Subject: [PATCH 01/59] Don't preserve rows when scrolling --- src/editor-component.coffee | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index a4a2eadef..ee1e9e629 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -20,7 +20,6 @@ EditorComponent = React.createClass batchingUpdates: false updateRequested: false cursorsMoved: false - preservedRowRange: null scrollingVertically: false gutterWidth: 0 refreshingScrollbars: false @@ -32,7 +31,7 @@ EditorComponent = React.createClass maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() - renderedRowRange = @getRenderedRowRange() + renderedRowRange = editor.getVisibleRowRange() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -92,13 +91,6 @@ EditorComponent = React.createClass height: horizontalScrollbarHeight width: verticalScrollbarWidth - getRenderedRowRange: -> - renderedRowRange = @props.editor.getVisibleRowRange() - if @preservedRowRange? - renderedRowRange[0] = Math.min(@preservedRowRange[0], renderedRowRange[0]) - renderedRowRange[1] = Math.max(@preservedRowRange[1], renderedRowRange[1]) - renderedRowRange - getInitialState: -> {} getDefaultProps: -> @@ -357,13 +349,6 @@ EditorComponent = React.createClass # if the editor's content and dimensions require them to be visible. @requestUpdate() - clearPreservedRowRange: -> - @preservedRowRange = null - @scrollingVertically = false - @requestUpdate() - - clearPreservedRowRangeAfterDelay: null # Created lazily - onBatchedUpdatesStarted: -> @batchingUpdates = true @@ -384,10 +369,7 @@ EditorComponent = React.createClass @requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection) onScrollTopChanged: -> - @preservedRowRange = @getRenderedRowRange() @scrollingVertically = true - @clearPreservedRowRangeAfterDelay ?= debounce(@clearPreservedRowRange, 200) - @clearPreservedRowRangeAfterDelay() @requestUpdate() onSelectionRemoved: (selection) -> From 308960309d9fe9e0db263363b196421936cffedf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 8 May 2014 21:22:23 -0600 Subject: [PATCH 02/59] Overdraw lines to discourage Blink from repainting the entire editor --- src/display-buffer.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 98225df6e..6adb0f9f7 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -34,6 +34,7 @@ class DisplayBuffer extends Model horizontalScrollMargin: 6 horizontalScrollbarHeight: 15 verticalScrollbarWidth: 15 + lineOverdraw: 8 constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) -> super @@ -241,7 +242,9 @@ class DisplayBuffer extends Model heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1 startRow = Math.floor(@getScrollTop() / @getLineHeight()) - endRow = Math.min(@getLineCount(), Math.ceil(startRow + heightInLines)) + endRow = Math.min(@getLineCount(), startRow + heightInLines + @lineOverdraw) + startRow = Math.max(0, startRow - @lineOverdraw) + [startRow, endRow] intersectsVisibleRowRange: (startRow, endRow) -> From 0ae8765a8a068e2577382619cfc6d38a2537271d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 9 May 2014 12:16:06 -0600 Subject: [PATCH 03/59] Update scroll position directly on mousewheel events Previously, we were updating the scrollbars and relying on an async scroll events to fire. But updating the scrollbars is expensive, so this updates the model directly when the next animation frame fires instead. --- src/editor-component.coffee | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index ee1e9e629..2c01aff3d 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -24,6 +24,8 @@ EditorComponent = React.createClass gutterWidth: 0 refreshingScrollbars: false measuringScrollbars: true + pendingVerticalScrollDelta: 0 + pendingHorizontalScrollDelta: 0 render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state @@ -311,14 +313,24 @@ EditorComponent = React.createClass @pendingScrollLeft = null onMouseWheel: (event) -> + event.preventDefault() + + animationFramePending = @pendingHorizontalScrollDelta isnt 0 or @pendingVerticalScrollDelta isnt 0 + # Only scroll in one direction at a time {wheelDeltaX, wheelDeltaY} = event if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) - @refs.horizontalScrollbar.getDOMNode().scrollLeft -= wheelDeltaX + @pendingHorizontalScrollDelta -= wheelDeltaX else - @refs.verticalScrollbar.getDOMNode().scrollTop -= wheelDeltaY + @pendingVerticalScrollDelta -= wheelDeltaY - event.preventDefault() + unless animationFramePending + requestAnimationFrame => + {editor} = @props + editor.setScrollTop(editor.getScrollTop() + @pendingVerticalScrollDelta) + editor.setScrollLeft(editor.getScrollLeft() + @pendingHorizontalScrollDelta) + @pendingVerticalScrollDelta = 0 + @pendingHorizontalScrollDelta = 0 onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) From 9f2c8c175611f055403fcb348469c54374be77bb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 9 May 2014 12:42:13 -0600 Subject: [PATCH 04/59] Measure characters in new lines when vertically scrolling stops --- src/editor-component.coffee | 8 ++++++++ src/lines-component.coffee | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 2c01aff3d..97074ba9f 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -383,6 +383,14 @@ EditorComponent = React.createClass onScrollTopChanged: -> @scrollingVertically = true @requestUpdate() + @stopScrollingAfterDelay ?= debounce(@onStoppedScrolling, 100) + @stopScrollingAfterDelay() + + onStoppedScrolling: -> + @scrollingVertically = false + @requestUpdate() + + stopScrollingAfterDelay: null # created lazily onSelectionRemoved: (selection) -> {editor} = @props diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 844670f32..555695ad8 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -28,7 +28,7 @@ LinesComponent = React.createClass @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide') + return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide', 'scrollingVertically') {renderedRowRange, pendingChanges} = newProps for change in pendingChanges From bf9f8597a76dde2265fb322a110a95f4c3ae4831 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 9 May 2014 16:42:51 -0600 Subject: [PATCH 05/59] Give each line its own layer on the GPU --- src/display-buffer.coffee | 4 +++- src/editor-component.coffee | 5 +++-- src/editor-scroll-view-component.coffee | 11 +++-------- src/editor.coffee | 1 + src/lines-component.coffee | 25 ++++++++++++++++--------- static/editor.less | 4 ++++ 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 6adb0f9f7..5075b44da 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -34,7 +34,7 @@ class DisplayBuffer extends Model horizontalScrollMargin: 6 horizontalScrollbarHeight: 15 verticalScrollbarWidth: 15 - lineOverdraw: 8 + lineOverdraw: 4 constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) -> super @@ -247,6 +247,8 @@ class DisplayBuffer extends Model [startRow, endRow] + getLineOverdraw: -> @lineOverdraw + intersectsVisibleRowRange: (startRow, endRow) -> [visibleStart, visibleEnd] = @getVisibleRowRange() not (endRow <= visibleStart or visibleEnd <= startRow) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 97074ba9f..bd48ed2fc 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -34,6 +34,7 @@ EditorComponent = React.createClass if @isMounted() renderedRowRange = editor.getVisibleRowRange() + lineOverdraw = editor.getLineOverdraw() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -56,8 +57,8 @@ EditorComponent = React.createClass EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide - scrollHeight, scrollWidth, lineHeight: lineHeightInPixels, - renderedRowRange, @pendingChanges, @scrollingVertically, @cursorsMoved, + lineHeight: lineHeightInPixels, renderedRowRange, lineOverdraw, @pendingChanges + scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index bc04b41f3..e361802a6 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,18 +17,13 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {scrollHeight, scrollWidth, renderedRowRange, pendingChanges, scrollingVertically} = @props + {renderedRowRange, lineOverdraw, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() inputStyle = @getHiddenInputPosition() inputStyle.WebkitTransform = 'translateZ(0)' - contentStyle = - height: scrollHeight - minWidth: scrollWidth - WebkitTransform: "translate3d(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px, 0)" - div className: 'scroll-view', InputComponent ref: 'input' @@ -38,11 +33,11 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - div className: 'scroll-view-content', style: contentStyle, onMouseDown: @onMouseDown, + div className: 'scroll-view-content', style: {top: -lineOverdraw * lineHeight}, onMouseDown: @onMouseDown, CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - renderedRowRange, pendingChanges, scrollingVertically + renderedRowRange, lineOverdraw, pendingChanges, scrollTop, scrollLeft, scrollingVertically } div className: 'underlayer', SelectionsComponent({editor}) diff --git a/src/editor.coffee b/src/editor.coffee index 467f13e32..a7e71262e 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1883,6 +1883,7 @@ class Editor extends Model getScrollWidth: (scrollWidth) -> @displayBuffer.getScrollWidth(scrollWidth) getVisibleRowRange: -> @displayBuffer.getVisibleRowRange() + getLineOverdraw: -> @displayBuffer.getLineOverdraw() intersectsVisibleRowRange: (startRow, endRow) -> @displayBuffer.intersectsVisibleRowRange(startRow, endRow) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 555695ad8..316dd41f8 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,12 +12,20 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, renderedRowRange, lineHeight, showIndentGuide} = @props + {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props [startRow, endRow] = renderedRowRange + firstVisibleRow = Math.floor(scrollTop / lineHeight) + + + offset = -scrollTop % lineHeight + + if firstVisibleRow < lineOverdraw + offset += (lineOverdraw - firstVisibleRow) * lineHeight lines = - for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1) - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, screenRow: startRow + i}) + for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) + screenRow = startRow + index + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, offset, screenRow}) div {className: 'lines'}, lines @@ -28,7 +36,7 @@ LinesComponent = React.createClass @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide', 'scrollingVertically') + return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') {renderedRowRange, pendingChanges} = newProps for change in pendingChanges @@ -103,11 +111,10 @@ LineComponent = React.createClass displayName: 'LineComponent' render: -> - {screenRow, lineHeight} = @props + {index, screenRow, offset, lineHeight} = @props - style = - top: screenRow * lineHeight - position: 'absolute' + top = index * lineHeight + offset + style = WebkitTransform: "translate3d(0px, #{top}px, 0px)" div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} @@ -137,4 +144,4 @@ LineComponent = React.createClass "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow') + not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'offset') diff --git a/static/editor.less b/static/editor.less index f70e7c3d0..b259d7652 100644 --- a/static/editor.less +++ b/static/editor.less @@ -14,6 +14,10 @@ .lines { z-index: -1; + + .line { + position: absolute; + } } .horizontal-scrollbar { From 7d8256d343b2106a04aa52ab9265b6a658813270 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 13:32:50 -0600 Subject: [PATCH 06/59] Drop lineOverdraw and scroll-view-content div --- src/display-buffer.coffee | 6 +----- src/editor-component.coffee | 3 +-- src/editor-scroll-view-component.coffee | 19 +++++++++---------- src/editor.coffee | 1 - src/lines-component.coffee | 18 ++++++------------ 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 5075b44da..5c6a05d1e 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -34,7 +34,6 @@ class DisplayBuffer extends Model horizontalScrollMargin: 6 horizontalScrollbarHeight: 15 verticalScrollbarWidth: 15 - lineOverdraw: 4 constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) -> super @@ -242,13 +241,10 @@ class DisplayBuffer extends Model heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1 startRow = Math.floor(@getScrollTop() / @getLineHeight()) - endRow = Math.min(@getLineCount(), startRow + heightInLines + @lineOverdraw) - startRow = Math.max(0, startRow - @lineOverdraw) + endRow = Math.min(@getLineCount(), startRow + heightInLines) [startRow, endRow] - getLineOverdraw: -> @lineOverdraw - intersectsVisibleRowRange: (startRow, endRow) -> [visibleStart, visibleEnd] = @getVisibleRowRange() not (endRow <= visibleStart or visibleEnd <= startRow) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index bd48ed2fc..8f89707a8 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -34,7 +34,6 @@ EditorComponent = React.createClass if @isMounted() renderedRowRange = editor.getVisibleRowRange() - lineOverdraw = editor.getLineOverdraw() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -57,7 +56,7 @@ EditorComponent = React.createClass EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide - lineHeight: lineHeightInPixels, renderedRowRange, lineOverdraw, @pendingChanges + lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index e361802a6..88faf8f3c 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,14 +17,14 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {renderedRowRange, lineOverdraw, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props + {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() inputStyle = @getHiddenInputPosition() inputStyle.WebkitTransform = 'translateZ(0)' - div className: 'scroll-view', + div className: 'scroll-view', onMouseDown: @onMouseDown, InputComponent ref: 'input' className: 'hidden-input' @@ -33,14 +33,13 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - div className: 'scroll-view-content', style: {top: -lineOverdraw * lineHeight}, onMouseDown: @onMouseDown, - CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) - LinesComponent { - ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - renderedRowRange, lineOverdraw, pendingChanges, scrollTop, scrollLeft, scrollingVertically - } - div className: 'underlayer', - SelectionsComponent({editor}) + CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) + LinesComponent { + ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, + renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically + } + div className: 'underlayer', + SelectionsComponent({editor}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/editor.coffee b/src/editor.coffee index a7e71262e..467f13e32 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1883,7 +1883,6 @@ class Editor extends Model getScrollWidth: (scrollWidth) -> @displayBuffer.getScrollWidth(scrollWidth) getVisibleRowRange: -> @displayBuffer.getVisibleRowRange() - getLineOverdraw: -> @displayBuffer.getLineOverdraw() intersectsVisibleRowRange: (startRow, endRow) -> @displayBuffer.intersectsVisibleRowRange(startRow, endRow) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 316dd41f8..cc4ace194 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,20 +12,14 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props + {editor, renderedRowRange, scrollTop, lineHeight, showIndentGuide} = @props [startRow, endRow] = renderedRowRange - firstVisibleRow = Math.floor(scrollTop / lineHeight) - - - offset = -scrollTop % lineHeight - - if firstVisibleRow < lineOverdraw - offset += (lineOverdraw - firstVisibleRow) * lineHeight + scrollOffset = -scrollTop % lineHeight lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, offset, screenRow}) + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, scrollOffset, screenRow}) div {className: 'lines'}, lines @@ -111,9 +105,9 @@ LineComponent = React.createClass displayName: 'LineComponent' render: -> - {index, screenRow, offset, lineHeight} = @props + {index, screenRow, scrollOffset, lineHeight} = @props - top = index * lineHeight + offset + top = index * lineHeight + scrollOffset style = WebkitTransform: "translate3d(0px, #{top}px, 0px)" div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} @@ -144,4 +138,4 @@ LineComponent = React.createClass "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'offset') + not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'scrollOffset') From e3d1a6aef8f485b834d73bc3def8ce847cf90b7d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 14:26:23 -0600 Subject: [PATCH 07/59] Render each line number on its own layer --- src/gutter-component.coffee | 61 ++++++++++++++++--------------------- static/editor.less | 6 +--- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 16903d5fd..c05f117b2 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -11,36 +11,29 @@ GutterComponent = React.createClass lastMeasuredWidth: null render: -> + if @isMounted() + {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props + [startRow, endRow] = renderedRowRange + scrollOffset = -scrollTop % lineHeight + maxLineNumberDigits = editor.getLineCount().toString().length + + wrapCount = 0 + lineNumbers = + for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) + if bufferRow is lastBufferRow + lineNumber = '•' + key = "#{bufferRow + 1}-#{++wrapCount}" + else + lastBufferRow = bufferRow + wrapCount = 0 + lineNumber = "#{bufferRow + 1}" + key = lineNumber + + LineNumberComponent({key, lineNumber, maxLineNumberDigits, index, lineHeight, scrollOffset}) + div className: 'gutter', - @renderLineNumbers() if @isMounted() - - renderLineNumbers: -> - {editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight} = @props - [startRow, endRow] = renderedRowRange - charWidth = editor.getDefaultCharWidth() - lineHeight = editor.getLineHeight() - style = - width: charWidth * (maxLineNumberDigits + 1.5) - height: scrollHeight - WebkitTransform: "translate3d(0, #{-scrollTop}px, 0)" - - lineNumbers = [] - tokenizedLines = editor.linesForScreenRows(startRow, endRow - 1) - tokenizedLines.push({id: 0}) if tokenizedLines.length is 0 - for bufferRow, i in editor.bufferRowsForScreenRows(startRow, endRow - 1) - if bufferRow is lastBufferRow - lineNumber = '•' - else - lastBufferRow = bufferRow - lineNumber = (bufferRow + 1).toString() - - key = tokenizedLines[i].id - screenRow = startRow + i - lineNumbers.push(LineNumberComponent({key, lineNumber, maxLineNumberDigits, bufferRow, screenRow, lineHeight})) - lastBufferRow = bufferRow - - div className: 'line-numbers', style: style, - lineNumbers + div className: 'line-numbers', + lineNumbers # 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 @@ -65,12 +58,10 @@ LineNumberComponent = React.createClass displayName: 'LineNumberComponent' render: -> - {bufferRow, screenRow, lineHeight} = @props + {index, lineHeight, scrollOffset} = @props div - className: "line-number line-number-#{bufferRow}" - style: {top: screenRow * lineHeight} - 'data-buffer-row': bufferRow - 'data-screen-row': screenRow + className: "line-number" + style: {WebkitTransform: "translate3d(0px, #{index * lineHeight + scrollOffset}px, 0px)"} dangerouslySetInnerHTML: {__html: @buildInnerHTML()} buildInnerHTML: -> @@ -84,4 +75,4 @@ LineNumberComponent = React.createClass iconDivHTML: '
' shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'lineHeight', 'screenRow', 'maxLineNumberDigits') + not isEqualForProperties(newProps, @props, 'index', 'lineHeight', 'scrollOffset') diff --git a/static/editor.less b/static/editor.less index b259d7652..9ce364108 100644 --- a/static/editor.less +++ b/static/editor.less @@ -57,14 +57,10 @@ } .gutter { - padding-left: 0.5em; - padding-right: 0.5em; + width: 100px; .line-number { position: absolute; - left: 0; - right: 0; - padding: 0; white-space: nowrap; .icon-right { From a36163ce86968446393d4cd7d1acf19b849be7d4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 15:30:52 -0600 Subject: [PATCH 08/59] Manually set the gutter width to the width of a line number We need to absolutely position line numbers to minimize repaints, but the gutter needs to be wide enough to show them. --- src/editor-component.coffee | 2 +- src/gutter-component.coffee | 59 ++++++++++++++++++++++++------------- static/editor.less | 3 +- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 8f89707a8..784a557c5 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -51,7 +51,7 @@ EditorComponent = React.createClass GutterComponent { editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, - onWidthChanged: @onGutterWidthChanged + width: @gutterWidth, onWidthChanged: @onGutterWidthChanged } EditorScrollViewComponent { diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index c05f117b2..2f180badf 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -11,29 +11,46 @@ GutterComponent = React.createClass lastMeasuredWidth: null render: -> - if @isMounted() - {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props - [startRow, endRow] = renderedRowRange - scrollOffset = -scrollTop % lineHeight - maxLineNumberDigits = editor.getLineCount().toString().length + {width} = @props - wrapCount = 0 - lineNumbers = - for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) - if bufferRow is lastBufferRow - lineNumber = '•' - key = "#{bufferRow + 1}-#{++wrapCount}" - else - lastBufferRow = bufferRow - wrapCount = 0 - lineNumber = "#{bufferRow + 1}" - key = lineNumber + div className: 'gutter', style: {width}, + div className: 'line-numbers', ref: 'lineNumbers', + if @isMounted() + @renderLineNumbers() + else + @renderLineNumberForMeasurement() - LineNumberComponent({key, lineNumber, maxLineNumberDigits, index, lineHeight, scrollOffset}) + renderLineNumbers: -> + {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props + [startRow, endRow] = renderedRowRange + maxLineNumberDigits = @getMaxLineNumberDigits() + scrollOffset = -scrollTop % lineHeight + wrapCount = 0 - div className: 'gutter', - div className: 'line-numbers', - lineNumbers + for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) + if bufferRow is lastBufferRow + lineNumber = '•' + key = "#{bufferRow + 1}-#{++wrapCount}" + else + lastBufferRow = bufferRow + wrapCount = 0 + lineNumber = "#{bufferRow + 1}" + key = lineNumber + + LineNumberComponent({key, lineNumber, maxLineNumberDigits, index, lineHeight, scrollOffset}) + + renderLineNumberForMeasurement: -> + LineNumberComponent( + key: 'forMeasurement' + lineNumber: '•' + maxLineNumberDigits: @getMaxLineNumberDigits() + index: 0 + lineHeight: 0 + scrollOffset: 0 + ) + + getMaxLineNumberDigits: -> + @props.editor.getLineCount().toString().length # 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 @@ -49,7 +66,7 @@ GutterComponent = React.createClass componentDidUpdate: (oldProps) -> unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') - width = @getDOMNode().offsetWidth + width = @refs.lineNumbers.getDOMNode().firstChild.offsetWidth if width isnt @lastMeasuredWidth @lastMeasuredWidth = width @props.onWidthChanged(width) diff --git a/static/editor.less b/static/editor.less index 9ce364108..613723c28 100644 --- a/static/editor.less +++ b/static/editor.less @@ -57,11 +57,10 @@ } .gutter { - width: 100px; - .line-number { position: absolute; white-space: nowrap; + padding: 0 .5em; .icon-right { padding: 0; From c8e9282557460e380b22a58f718869ef4b8b37df Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 16:12:58 -0600 Subject: [PATCH 09/59] Position cursors as layers relative to the viewport --- src/cursor-component.coffee | 9 +++++++-- src/cursors-component.coffee | 4 ++-- src/editor-scroll-view-component.coffee | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/cursor-component.coffee b/src/cursor-component.coffee index fcc6c2022..ebd82fa00 100644 --- a/src/cursor-component.coffee +++ b/src/cursor-component.coffee @@ -6,8 +6,13 @@ CursorComponent = React.createClass displayName: 'CursorComponent' render: -> - {top, left, height, width} = @props.cursor.getPixelRect() + {cursor, scrollTop} = @props + {top, left, height, width} = cursor.getPixelRect() + top -= scrollTop + className = 'cursor' className += ' blink-off' if @props.blinkOff - div className: className, style: {top, left, height, width} + WebkitTransform = "translate3d(#{left}px, #{top}px, 0px)" + + div className: className, style: {height, width, WebkitTransform} diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index e3143f1f1..104cb935a 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -13,7 +13,7 @@ CursorsComponent = React.createClass cursorBlinkIntervalHandle: null render: -> - {editor} = @props + {editor, scrollTop} = @props blinkOff = @state.blinkCursorsOff div className: 'cursors', @@ -21,7 +21,7 @@ CursorsComponent = React.createClass for selection in editor.getSelections() if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) {cursor} = selection - CursorComponent({key: cursor.id, cursor, blinkOff}) + CursorComponent({key: cursor.id, cursor, scrollTop, blinkOff}) getInitialState: -> blinkCursorsOff: false diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 88faf8f3c..c43d60330 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -33,7 +33,7 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) + CursorsComponent({editor, scrollTop, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically From cfc08e8b98e5279367450074f99fa6dbaf5b8698 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 16:24:52 -0600 Subject: [PATCH 10/59] Allow horizontal scrolling --- src/lines-component.coffee | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index cc4ace194..c51f92bff 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,14 +12,15 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, renderedRowRange, scrollTop, lineHeight, showIndentGuide} = @props + {editor, renderedRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide} = @props [startRow, endRow] = renderedRowRange - scrollOffset = -scrollTop % lineHeight + verticalScrollOffset = -scrollTop % lineHeight + horizontalScrollOffset = -scrollLeft lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, scrollOffset, screenRow}) + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow}) div {className: 'lines'}, lines @@ -105,10 +106,11 @@ LineComponent = React.createClass displayName: 'LineComponent' render: -> - {index, screenRow, scrollOffset, lineHeight} = @props + {index, screenRow, verticalScrollOffset, horizontalScrollOffset, lineHeight} = @props - top = index * lineHeight + scrollOffset - style = WebkitTransform: "translate3d(0px, #{top}px, 0px)" + top = index * lineHeight + verticalScrollOffset + left = horizontalScrollOffset + style = WebkitTransform: "translate3d(#{left}px, #{top}px, 0px)" div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} @@ -138,4 +140,4 @@ LineComponent = React.createClass "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'scrollOffset') + not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') From 757ae6de3904f1435a18c852d0427f985898f146 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 16:58:19 -0600 Subject: [PATCH 11/59] Position selections relative to viewport This is getting closer, but lines still need to be opaque. Multi-line selections will still need to be rendered behind the line layers so they can extend to the edge of the viewport, so this code still has value. --- src/editor-scroll-view-component.coffee | 2 +- src/selection-component.coffee | 9 ++++++++- src/selections-component.coffee | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index c43d60330..6fdf54196 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -39,7 +39,7 @@ EditorScrollViewComponent = React.createClass renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically } div className: 'underlayer', - SelectionsComponent({editor}) + SelectionsComponent({editor, scrollTop, scrollLeft}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/selection-component.coffee b/src/selection-component.coffee index e3dd22e3a..3e0228079 100644 --- a/src/selection-component.coffee +++ b/src/selection-component.coffee @@ -6,6 +6,13 @@ SelectionComponent = React.createClass displayName: 'SelectionComponent' render: -> + {scrollTop, scrollLeft} = @props + div className: 'selection', for regionRect, i in @props.selection.getRegionRects() - div className: 'region', key: i, style: regionRect + {top, left, right, width, height} = regionRect + top -= scrollTop + left -= scrollLeft + right -= scrollLeft + WebkitTransform = "translate3d(0px, #{top}px, 0px)" + div className: 'region', key: i, style: {left, right, width, height, WebkitTransform} diff --git a/src/selections-component.coffee b/src/selections-component.coffee index 616fc62dd..e3de8e36f 100644 --- a/src/selections-component.coffee +++ b/src/selections-component.coffee @@ -7,10 +7,10 @@ SelectionsComponent = React.createClass displayName: 'SelectionsComponent' render: -> - {editor} = @props + {editor, scrollTop, scrollLeft} = @props div className: 'selections', if @isMounted() for selection in editor.getSelections() if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) - SelectionComponent({key: selection.id, selection}) + SelectionComponent({key: selection.id, selection, scrollTop, scrollLeft}) From a22480d8572ec9f1dcf77e9b1da7b223cd3a713d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 11:36:54 -0600 Subject: [PATCH 12/59] Don't give lines a negative z-index Removing the z-index makes them accessible via mouse in the inspector. --- static/editor.less | 2 -- 1 file changed, 2 deletions(-) diff --git a/static/editor.less b/static/editor.less index 613723c28..1529c33e2 100644 --- a/static/editor.less +++ b/static/editor.less @@ -13,8 +13,6 @@ } .lines { - z-index: -1; - .line { position: absolute; } From 63488997eeefd30ddd5a83a1915e5b155b185b2f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 12:22:11 -0600 Subject: [PATCH 13/59] Give lines and line numbers an opaque background to support sub-pixel AA Since lines and line numbers are now on the GPU, their text won't be properly anti-aliased on low-resolution displays unless their layers have a solid background. --- src/gutter-component.coffee | 2 +- src/lines-component.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 2f180badf..04bcfa7d6 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -77,7 +77,7 @@ LineNumberComponent = React.createClass render: -> {index, lineHeight, scrollOffset} = @props div - className: "line-number" + className: "line-number editor-colors" style: {WebkitTransform: "translate3d(0px, #{index * lineHeight + scrollOffset}px, 0px)"} dangerouslySetInnerHTML: {__html: @buildInnerHTML()} diff --git a/src/lines-component.coffee b/src/lines-component.coffee index c51f92bff..b1cd995c2 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -112,7 +112,7 @@ LineComponent = React.createClass left = horizontalScrollOffset style = WebkitTransform: "translate3d(#{left}px, #{top}px, 0px)" - div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} + div className: 'line editor-colors', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} buildInnerHTML: -> if @props.tokenizedLine.text.length is 0 From 191bc115cff5642c493b3d745671e331450acec0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 12:22:26 -0600 Subject: [PATCH 14/59] Use explicit descendant selector for styling lines --- static/editor.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/editor.less b/static/editor.less index 1529c33e2..9d20f625b 100644 --- a/static/editor.less +++ b/static/editor.less @@ -13,7 +13,7 @@ } .lines { - .line { + > .line { position: absolute; } } From 1aee276b4518c322419c2285caf4828a82202f65 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 12:40:19 -0600 Subject: [PATCH 15/59] Update line rendering specs for new layer scheme --- spec/editor-component-spec.coffee | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 23cd6a2d1..9e6b3505e 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -45,7 +45,7 @@ describe "EditorComponent", -> contentNode.style.width = '' describe "line rendering", -> - it "renders only the currently-visible lines", -> + it "renders only the currently-visible lines, translated relative to the scroll position", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() @@ -57,28 +57,27 @@ describe "EditorComponent", -> verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - expect(node.querySelector('.scroll-view-content').style['-webkit-transform']).toBe "translate3d(0px, #{-2.5 * lineHeightInPixels}px, 0)" - lineNodes = node.querySelectorAll('.line') expect(lineNodes.length).toBe 6 - expect(lineNodes[0].offsetTop).toBe 2 * lineHeightInPixels + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{-.5 * lineHeightInPixels}px, 0px)" expect(lineNodes[0].textContent).toBe editor.lineForScreenRow(2).text expect(lineNodes[5].textContent).toBe editor.lineForScreenRow(7).text + expect(lineNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{4.5 * lineHeightInPixels}px, 0px)" - it "updates absolute positions of subsequent lines when lines are inserted or removed", -> + it "updates the translation of subsequent lines when lines are inserted or removed", -> editor.getBuffer().deleteRows(0, 1) lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].offsetTop).toBe 0 - expect(lineNodes[1].offsetTop).toBe 1 * lineHeightInPixels - expect(lineNodes[2].offsetTop).toBe 2 * lineHeightInPixels + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" + expect(lineNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" editor.getBuffer().insert([0, 0], '\n\n') lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].offsetTop).toBe 0 - expect(lineNodes[1].offsetTop).toBe 1 * lineHeightInPixels - expect(lineNodes[2].offsetTop).toBe 2 * lineHeightInPixels - expect(lineNodes[3].offsetTop).toBe 3 * lineHeightInPixels - expect(lineNodes[4].offsetTop).toBe 4 * lineHeightInPixels + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" + expect(lineNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" + expect(lineNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" + expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" describe "when indent guides are enabled", -> beforeEach -> From 8d25da9474a5646a91f73168e4ad89527100a191 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 12:45:58 -0600 Subject: [PATCH 16/59] Update line number rendering specs for new layer scheme --- spec/editor-component-spec.coffee | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 9e6b3505e..140b34dfb 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -143,40 +143,41 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() - lines = node.querySelectorAll('.line-number') - expect(lines.length).toBe 6 - expect(lines[0].textContent).toBe "#{nbsp}1" - expect(lines[5].textContent).toBe "#{nbsp}6" + lineNumberNodes = node.querySelectorAll('.line-number') + expect(lineNumberNodes.length).toBe 6 + expect(lineNumberNodes[0].textContent).toBe "#{nbsp}1" + expect(lineNumberNodes[5].textContent).toBe "#{nbsp}6" verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - expect(node.querySelector('.line-numbers').style['-webkit-transform']).toBe "translate3d(0, #{-2.5 * lineHeightInPixels}px, 0)" - lineNumberNodes = node.querySelectorAll('.line-number') expect(lineNumberNodes.length).toBe 6 - expect(lineNumberNodes[0].offsetTop).toBe 2 * lineHeightInPixels - expect(lineNumberNodes[5].offsetTop).toBe 7 * lineHeightInPixels + expect(lineNumberNodes[0].textContent).toBe "#{nbsp}3" + expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{-.5 * lineHeightInPixels}px, 0px)" expect(lineNumberNodes[5].textContent).toBe "#{nbsp}8" + expect(lineNumberNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{4.5 * lineHeightInPixels}px, 0px)" - it "updates absolute positions of subsequent line numbers when lines are inserted or removed", -> + it "updates the translation of subsequent line numbers when lines are inserted or removed", -> editor.getBuffer().insert([0, 0], '\n\n') lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes[0].offsetTop).toBe 0 - expect(lineNumberNodes[1].offsetTop).toBe 1 * lineHeightInPixels - expect(lineNumberNodes[2].offsetTop).toBe 2 * lineHeightInPixels - expect(lineNumberNodes[3].offsetTop).toBe 3 * lineHeightInPixels - expect(lineNumberNodes[4].offsetTop).toBe 4 * lineHeightInPixels + expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNumberNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" editor.getBuffer().insert([0, 0], '\n\n') lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes[0].offsetTop).toBe 0 - expect(lineNumberNodes[1].offsetTop).toBe 1 * lineHeightInPixels - expect(lineNumberNodes[2].offsetTop).toBe 2 * lineHeightInPixels - expect(lineNumberNodes[3].offsetTop).toBe 3 * lineHeightInPixels - expect(lineNumberNodes[4].offsetTop).toBe 4 * lineHeightInPixels + expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNumberNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{5 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[6].style['-webkit-transform']).toBe "translate3d(0px, #{6 * lineHeightInPixels}px, 0px)" it "renders • characters for soft-wrapped lines", -> editor.setSoftWrap(true) From f3efd7d60be7148c16cd9b9cdccf90b122837b72 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 15:13:56 -0600 Subject: [PATCH 17/59] Position cursors relative to scrollLeft and fix specs --- spec/editor-component-spec.coffee | 25 +++++++++++-------------- src/cursor-component.coffee | 3 ++- src/cursors-component.coffee | 4 ++-- src/editor-scroll-view-component.coffee | 2 +- static/editor.less | 7 +++++++ 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 140b34dfb..684a0ed25 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -209,19 +209,19 @@ describe "EditorComponent", -> expect(node.textContent).toBe "#{i + 1}" describe "cursor rendering", -> - it "renders the currently visible cursors", -> + it "renders the currently visible cursors, translated relative to the scroll position", -> cursor1 = editor.getCursor() cursor1.setScreenPosition([0, 5]) node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 20 * lineHeightInPixels + 'px' component.measureHeightAndWidth() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels expect(cursorNodes[0].offsetWidth).toBe charWidth - expect(cursorNodes[0].offsetTop).toBe 0 - expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)" cursor2 = editor.addCursorAtScreenPosition([6, 11]) cursor3 = editor.addCursorAtScreenPosition([4, 10]) @@ -229,25 +229,23 @@ describe "EditorComponent", -> cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 expect(cursorNodes[0].offsetTop).toBe 0 - expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth - expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels - expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{10 * charWidth}px, #{4 * lineHeightInPixels}px, 0px)" verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + horizontalScrollbarNode.scrollLeft = 3.5 * charWidth + horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels - expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth - expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels - expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 2.5) * lineHeightInPixels}px, 0px)" cursor3.destroy() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels - expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)" it "accounts for character widths when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') @@ -330,8 +328,7 @@ describe "EditorComponent", -> cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels - expect(cursorNodes[0].offsetLeft).toBe 8 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{8 * charWidth}px, #{6 * lineHeightInPixels}px, 0px)" describe "selection rendering", -> scrollViewClientLeft = null diff --git a/src/cursor-component.coffee b/src/cursor-component.coffee index ebd82fa00..5dc0eccbf 100644 --- a/src/cursor-component.coffee +++ b/src/cursor-component.coffee @@ -6,9 +6,10 @@ CursorComponent = React.createClass displayName: 'CursorComponent' render: -> - {cursor, scrollTop} = @props + {cursor, scrollTop, scrollLeft} = @props {top, left, height, width} = cursor.getPixelRect() top -= scrollTop + left -= scrollLeft className = 'cursor' className += ' blink-off' if @props.blinkOff diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 104cb935a..6883b10d5 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -13,7 +13,7 @@ CursorsComponent = React.createClass cursorBlinkIntervalHandle: null render: -> - {editor, scrollTop} = @props + {editor, scrollTop, scrollLeft} = @props blinkOff = @state.blinkCursorsOff div className: 'cursors', @@ -21,7 +21,7 @@ CursorsComponent = React.createClass for selection in editor.getSelections() if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) {cursor} = selection - CursorComponent({key: cursor.id, cursor, scrollTop, blinkOff}) + CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft, blinkOff}) getInitialState: -> blinkCursorsOff: false diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 6fdf54196..76d0fd11e 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -33,7 +33,7 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, scrollTop, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically diff --git a/static/editor.less b/static/editor.less index 9d20f625b..f7e0d8188 100644 --- a/static/editor.less +++ b/static/editor.less @@ -13,11 +13,17 @@ } .lines { + z-index: 0; + > .line { position: absolute; } } + .cursor { + z-index: 1; + } + .horizontal-scrollbar { position: absolute; left: 0; @@ -47,6 +53,7 @@ .scroll-view { overflow: hidden; + z-index: 0; } .scroll-view-content { From d53f97ecfe8dbf3e72592fac57be8e33a506e176 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 15:18:28 -0600 Subject: [PATCH 18/59] Fix horizontal scrolling spec --- spec/editor-component-spec.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 684a0ed25..22aef3aab 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -524,16 +524,18 @@ describe "EditorComponent", -> editor.setScrollTop(10) expect(verticalScrollbarNode.scrollTop).toBe 10 - it "updates the horizontal scrollbar and scroll view content x transform based on the scrollLeft of the model", -> + it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> node.style.width = 30 * charWidth + 'px' component.measureHeightAndWidth() - scrollViewContentNode = node.querySelector('.scroll-view-content') - expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0)" + lineNodes = node.querySelectorAll('.line') + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 0 editor.setScrollLeft(100) - expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0)" + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" + expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(-100px, #{4 * lineHeightInPixels}px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> From e44027b18677888233504c8940e5585bb8bb0d65 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 15:30:21 -0600 Subject: [PATCH 19/59] Fix the height/width of the editor in spec Now that everything is absolutely position, the editor no longer assumes a "natural" height and width. This can be addressed later if we want to allow editors to expand based on their content. --- spec/editor-component-spec.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 22aef3aab..b7afea0e6 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -41,6 +41,10 @@ describe "EditorComponent", -> verticalScrollbarNode = node.querySelector('.vertical-scrollbar') horizontalScrollbarNode = node.querySelector('.horizontal-scrollbar') + node.style.height = editor.getLineCount() * lineHeightInPixels + 'px' + node.style.width = '1000px' + component.measureHeightAndWidth() + afterEach -> contentNode.style.width = '' From 01622140e34f65c83c148c11c6c785d3350c5753 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 15:38:12 -0600 Subject: [PATCH 20/59] Rename renderedRowRange to visibleRowRange We only render visible rows now, so this makes more sense. --- src/editor-component.coffee | 6 +++--- src/editor-scroll-view-component.coffee | 4 ++-- src/gutter-component.coffee | 10 +++++----- src/lines-component.coffee | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 784a557c5..c3b4b37ac 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -33,7 +33,7 @@ EditorComponent = React.createClass maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() - renderedRowRange = editor.getVisibleRowRange() + visibleRowRange = editor.getVisibleRowRange() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -49,14 +49,14 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, + editor, visibleRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, width: @gutterWidth, onWidthChanged: @onGutterWidthChanged } EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide - lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges + lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 76d0fd11e..727a7e11c 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props + {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -36,7 +36,7 @@ EditorScrollViewComponent = React.createClass CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically + visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically } div className: 'underlayer', SelectionsComponent({editor, scrollTop, scrollLeft}) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 04bcfa7d6..214b1af52 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -21,8 +21,8 @@ GutterComponent = React.createClass @renderLineNumberForMeasurement() renderLineNumbers: -> - {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props - [startRow, endRow] = renderedRowRange + {editor, visibleRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props + [startRow, endRow] = visibleRowRange maxLineNumberDigits = @getMaxLineNumberDigits() scrollOffset = -scrollTop % lineHeight wrapCount = 0 @@ -56,11 +56,11 @@ GutterComponent = React.createClass # non-zero-delta change to the screen lines has occurred within the current # visible row range. shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'scrollTop', 'lineHeight', 'fontSize') + return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'scrollTop', 'lineHeight', 'fontSize') - {renderedRowRange, pendingChanges} = newProps + {visibleRowRange, pendingChanges} = newProps for change in pendingChanges when change.screenDelta > 0 or change.bufferDelta > 0 - return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start + return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start false diff --git a/src/lines-component.coffee b/src/lines-component.coffee index b1cd995c2..c7d6ee3f7 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,8 +12,8 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, renderedRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide} = @props - [startRow, endRow] = renderedRowRange + {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide} = @props + [startRow, endRow] = visibleRowRange verticalScrollOffset = -scrollTop % lineHeight horizontalScrollOffset = -scrollLeft @@ -31,11 +31,11 @@ LinesComponent = React.createClass @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') + return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') - {renderedRowRange, pendingChanges} = newProps + {visibleRowRange, pendingChanges} = newProps for change in pendingChanges - return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start + return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start false @@ -56,7 +56,7 @@ LinesComponent = React.createClass editor.setDefaultCharWidth(charWidth) measureCharactersInNewLines: -> - [visibleStartRow, visibleEndRow] = @props.renderedRowRange + [visibleStartRow, visibleEndRow] = @props.visibleRowRange node = @getDOMNode() for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) From 7a9278e6a76058a2b446d3520694f5cca86725dc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 16:47:39 -0600 Subject: [PATCH 21/59] Render selection fragments on opaque lines Because lines are opaque on the GPU for sub pixel antialiasing, the lines obscure the selections which were formerly rendered behind the lines. This commit renders selection fragments *on* each opaque line layer so the selections look correct again. Still needs cleanup and optimization. --- src/editor-component.coffee | 22 ++++++++++------------ src/editor-scroll-view-component.coffee | 5 +++-- src/editor.coffee | 3 +++ src/lines-component.coffee | 22 +++++++++++++++++----- src/selection.coffee | 19 +++++++++++++++++++ 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index c3b4b37ac..0dd622579 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -20,6 +20,7 @@ EditorComponent = React.createClass batchingUpdates: false updateRequested: false cursorsMoved: false + selectionChanged: false scrollingVertically: false gutterWidth: 0 refreshingScrollbars: false @@ -55,9 +56,9 @@ EditorComponent = React.createClass } EditorScrollViewComponent { - ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide - lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges - scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, + ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, + lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, + scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, @selectionChanged, cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } @@ -124,6 +125,7 @@ EditorComponent = React.createClass componentDidUpdate: -> @pendingChanges.length = 0 @cursorsMoved = false + @selectionChanged = false @refreshingScrollbars = false @measureScrollbars() if @measuringScrollbars @props.parentView.trigger 'editor:display-updated' @@ -134,9 +136,7 @@ EditorComponent = React.createClass @subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded @subscribe editor, 'screen-lines-changed', @onScreenLinesChanged @subscribe editor, 'cursors-moved', @onCursorsMoved - @subscribe editor, 'selection-screen-range-changed', @requestUpdate - @subscribe editor, 'selection-added', @onSelectionAdded - @subscribe editor, 'selection-removed', @onSelectionAdded + @subscribe editor, 'selection-added selection-removed selection-screen-range-changed', @onSelectionChanged @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.changes, @requestUpdate @subscribe editor.$height.changes, @requestUpdate @@ -376,9 +376,11 @@ EditorComponent = React.createClass @pendingChanges.push(change) @requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events - onSelectionAdded: (selection) -> + onSelectionChanged: (selection) -> {editor} = @props - @requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection) + if editor.selectionIntersectsVisibleRowRange(selection) + @selectionChanged = true + @requestUpdate() onScrollTopChanged: -> @scrollingVertically = true @@ -392,10 +394,6 @@ EditorComponent = React.createClass stopScrollingAfterDelay: null # created lazily - onSelectionRemoved: (selection) -> - {editor} = @props - @requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection) - onCursorsMoved: -> @cursorsMoved = true diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 727a7e11c..51f62d720 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props + {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged} = @props {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -36,7 +36,8 @@ EditorScrollViewComponent = React.createClass CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically + visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + selectionChanged } div className: 'underlayer', SelectionsComponent({editor, scrollTop, scrollLeft}) diff --git a/src/editor.coffee b/src/editor.coffee index 467f13e32..3dc51555d 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1259,6 +1259,9 @@ class Editor extends Model # Returns: An {Array} of {Selection}s. getSelections: -> new Array(@selections...) + selectionsForScreenRows: (startRow, endRow) -> + @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) + # Public: Get the most recent {Selection} or the selection at the given # index. # diff --git a/src/lines-component.coffee b/src/lines-component.coffee index c7d6ee3f7..e13133402 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,15 +12,17 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide} = @props + {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange + visibleSelections = editor.selectionsForScreenRows(startRow, endRow - 1) verticalScrollOffset = -scrollTop % lineHeight horizontalScrollOffset = -scrollLeft lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow}) + selections = visibleSelections.filter (selection) -> selection.intersectsScreenRow(screenRow) + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selections, selectionChanged}) div {className: 'lines'}, lines @@ -31,6 +33,7 @@ LinesComponent = React.createClass @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> + return true if newProps.selectionChanged return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') {visibleRowRange, pendingChanges} = newProps @@ -112,9 +115,11 @@ LineComponent = React.createClass left = horizontalScrollOffset style = WebkitTransform: "translate3d(#{left}px, #{top}px, 0px)" - div className: 'line editor-colors', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} + div className: 'line editor-colors', style: style, 'data-screen-row': screenRow, + span dangerouslySetInnerHTML: {__html: @buildTokensHTML()} + @renderSelections() - buildInnerHTML: -> + buildTokensHTML: -> if @props.tokenizedLine.text.length is 0 @buildEmptyLineHTML() else @@ -139,5 +144,12 @@ LineComponent = React.createClass else "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" + renderSelections: -> + {selections, screenRow} = @props + for selection in selections + div className: 'selection', key: selection.id, + div className: 'region', style: selection.regionRectForScreenRow(screenRow) + shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') + return true unless isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') + newProps.selectionChanged and (newProps.selections.length > 0 or @props.selections.length > 0) diff --git a/src/selection.coffee b/src/selection.coffee index 00cae8b7c..14d218fb0 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -553,6 +553,12 @@ class Selection extends Model intersectsBufferRange: (bufferRange) -> @getBufferRange().intersectsWith(bufferRange) + intersectsScreenRowRange: (startRow, endRow) -> + @getScreenRange().intersectsRowRange(startRow, endRow) + + intersectsScreenRow: (screenRow) -> + @getScreenRange().intersectsRow(screenRow) + # Public: Identifies if a selection intersects with another selection. # # otherSelection - A {Selection} to check against. @@ -626,6 +632,19 @@ class Selection extends Model rects + regionRectForScreenRow: (screenRow) -> + {start, end} = @getScreenRange() + region = {height: @editor.getLineHeight(), top: 0, left: 0} + + if screenRow is start.row + region.left = @editor.pixelPositionForScreenPosition(start).left + + if screenRow is end.row + region.width = @editor.pixelPositionForScreenPosition(end).left - region.left + + region.right = 0 unless region.width? + region + screenRangeChanged: -> screenRange = @getScreenRange() @emit 'screen-range-changed', screenRange From ce9fe90217e480f30111ba9eddd4ad10eb4206a1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 20:05:44 -0600 Subject: [PATCH 22/59] Make multi-line selections appear to span the screen with a single div Because lines are opaque and any area of a selection that overlaps a line is actually rendered on the line itself, the screen-spanning background of a multi-line selection can actually be rendered as a single div spanning the entire screen from the first row to the penultimate row of the selection. --- src/editor-scroll-view-component.coffee | 4 +- src/selection-backgrounds-component.coffee | 18 ++++++++ src/selection-component.coffee | 18 -------- src/selection.coffee | 52 +++++----------------- src/selections-component.coffee | 16 ------- 5 files changed, 31 insertions(+), 77 deletions(-) create mode 100644 src/selection-backgrounds-component.coffee delete mode 100644 src/selection-component.coffee delete mode 100644 src/selections-component.coffee diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 51f62d720..817980189 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -5,7 +5,7 @@ React = require 'react' InputComponent = require './input-component' LinesComponent = require './lines-component' CursorsComponent = require './cursors-component' -SelectionsComponent = require './selections-component' +SelectionBackgroundsComponent = require './selection-backgrounds-component' module.exports = EditorScrollViewComponent = React.createClass @@ -40,7 +40,7 @@ EditorScrollViewComponent = React.createClass selectionChanged } div className: 'underlayer', - SelectionsComponent({editor, scrollTop, scrollLeft}) + SelectionBackgroundsComponent({editor, scrollTop, scrollLeft}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/selection-backgrounds-component.coffee b/src/selection-backgrounds-component.coffee new file mode 100644 index 000000000..62152cc09 --- /dev/null +++ b/src/selection-backgrounds-component.coffee @@ -0,0 +1,18 @@ +React = require 'react' +{div} = require 'reactionary' + +module.exports = +SelectionBackgroundsComponent = React.createClass + displayName: 'SelectionBackgroundsComponent' + + render: -> + {editor, scrollTop} = @props + + div className: 'selections', + if @isMounted() + for selection in editor.getSelections() + if backgroundRect = selection.getBackgroundRect() + {top, left, right, height} = backgroundRect + WebkitTransform = "translate3d(0px, #{top - scrollTop}px, 0px)" + div className: 'selection', key: selection.id, + div className: 'region', style: {left, right, height, WebkitTransform} diff --git a/src/selection-component.coffee b/src/selection-component.coffee deleted file mode 100644 index 3e0228079..000000000 --- a/src/selection-component.coffee +++ /dev/null @@ -1,18 +0,0 @@ -React = require 'react' -{div} = require 'reactionary' - -module.exports = -SelectionComponent = React.createClass - displayName: 'SelectionComponent' - - render: -> - {scrollTop, scrollLeft} = @props - - div className: 'selection', - for regionRect, i in @props.selection.getRegionRects() - {top, left, right, width, height} = regionRect - top -= scrollTop - left -= scrollLeft - right -= scrollLeft - WebkitTransform = "translate3d(0px, #{top}px, 0px)" - div className: 'region', key: i, style: {left, right, width, height, WebkitTransform} diff --git a/src/selection.coffee b/src/selection.coffee index 14d218fb0..fdccb0242 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -591,47 +591,6 @@ class Selection extends Model compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) - # Get the pixel dimensions of rectangular regions that cover selection's area - # on the screen. Used by SelectionComponent for rendering. - getRegionRects: -> - lineHeight = @editor.getLineHeight() - {start, end} = @getScreenRange() - rowCount = end.row - start.row + 1 - startPixelPosition = @editor.pixelPositionForScreenPosition(start) - endPixelPosition = @editor.pixelPositionForScreenPosition(end) - - if rowCount is 1 - # Single line selection - rects = [{ - top: startPixelPosition.top - height: lineHeight - left: startPixelPosition.left - width: endPixelPosition.left - startPixelPosition.left - }] - else - # Multi-line selection - rects = [] - - # First row, extending from selection start to the right side of screen - rects.push { - top: startPixelPosition.top - left: startPixelPosition.left - height: lineHeight - right: 0 - } - if rowCount > 2 - # Middle rows, extending from left side to right side of screen - rects.push { - top: startPixelPosition.top + lineHeight - height: (rowCount - 2) * lineHeight - left: 0 - right: 0 - } - # Last row, extending from left side of screen to selection end - rects.push {top: endPixelPosition.top, height: lineHeight, left: 0, width: endPixelPosition.left } - - rects - regionRectForScreenRow: (screenRow) -> {start, end} = @getScreenRange() region = {height: @editor.getLineHeight(), top: 0, left: 0} @@ -645,6 +604,17 @@ class Selection extends Model region.right = 0 unless region.width? region + getBackgroundRect: -> + {start, end} = @getScreenRange() + return if start.row is end.row + + lineHeight = @editor.getLineHeight() + height = (end.row - start.row) * lineHeight + top = start.row * lineHeight + left = 0 + right = 0 + {top, left, right, height} + screenRangeChanged: -> screenRange = @getScreenRange() @emit 'screen-range-changed', screenRange diff --git a/src/selections-component.coffee b/src/selections-component.coffee deleted file mode 100644 index e3de8e36f..000000000 --- a/src/selections-component.coffee +++ /dev/null @@ -1,16 +0,0 @@ -React = require 'react' -{div} = require 'reactionary' -SelectionComponent = require './selection-component' - -module.exports = -SelectionsComponent = React.createClass - displayName: 'SelectionsComponent' - - render: -> - {editor, scrollTop, scrollLeft} = @props - - div className: 'selections', - if @isMounted() - for selection in editor.getSelections() - if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) - SelectionComponent({key: selection.id, selection, scrollTop, scrollLeft}) From cbcc30b384797c62dee253200a5b5310c6c9cd98 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 10:31:56 -0600 Subject: [PATCH 23/59] Don't render empty selections --- src/lines-component.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index e13133402..9f2b926b1 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -21,7 +21,7 @@ LinesComponent = React.createClass lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - selections = visibleSelections.filter (selection) -> selection.intersectsScreenRow(screenRow) + selections = visibleSelections.filter (selection) -> not selection.isEmpty() and selection.intersectsScreenRow(screenRow) LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selections, selectionChanged}) div {className: 'lines'}, lines From 9001d34ddf0e5c02de0bfe5aeacf0f4c11a1824b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 10:32:20 -0600 Subject: [PATCH 24/59] Change selection specs to match new rendering scheme --- spec/editor-component-spec.coffee | 85 +++++++++++++++---------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index b7afea0e6..ff6665cfb 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -335,62 +335,57 @@ describe "EditorComponent", -> expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{8 * charWidth}px, #{6 * lineHeightInPixels}px, 0px)" describe "selection rendering", -> - scrollViewClientLeft = null + [scrollViewNode, scrollViewClientLeft] = [] beforeEach -> + scrollViewNode = node.querySelector('.scroll-view') scrollViewClientLeft = node.querySelector('.scroll-view').getBoundingClientRect().left - it "renders 1 region for 1-line selections", -> - # 1-line selection - editor.setSelectedScreenRange([[1, 6], [1, 10]]) - regions = node.querySelectorAll('.selection .region') + describe "for single line selections", -> + it "renders 1 region on the line and no background region", -> + # 1-line selection + editor.setSelectedScreenRange([[1, 6], [1, 10]]) + lineNodes = node.querySelectorAll('.line') + line1Region = lineNodes[1].querySelector('.selection .region') + regionRect = line1Region.getBoundingClientRect() + expect(regionRect.top).toBe 1 * lineHeightInPixels + expect(regionRect.height).toBe 1 * lineHeightInPixels + expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(regionRect.width).toBe 4 * charWidth - expect(regions.length).toBe 1 - regionRect = regions[0].getBoundingClientRect() - expect(regionRect.top).toBe 1 * lineHeightInPixels - expect(regionRect.height).toBe 1 * lineHeightInPixels - expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(regionRect.width).toBe 4 * charWidth + expect(node.querySelectorAll('.underlayer .selection .region').length).toBe 0 - it "renders 2 regions for 2-line selections", -> - editor.setSelectedScreenRange([[1, 6], [2, 10]]) - regions = node.querySelectorAll('.selection .region') - expect(regions.length).toBe 2 + describe "for multi-line selections", -> + it "renders a region on each line and a full-width background region from the first line to the penultimate line", -> + editor.setSelectedScreenRange([[1, 6], [3, 10]]) - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 1 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(Math.ceil(region1Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed + lineNodes = node.querySelectorAll('.line') + region1Rect = lineNodes[1].querySelector('.selection .region').getBoundingClientRect() + expect(region1Rect.top).toBe 1 * lineHeightInPixels + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(region1Rect.right).toBe scrollViewClientLeft + lineNodes[1].offsetWidth - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 2 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft + 0 - expect(region2Rect.width).toBe 10 * charWidth + region2Rect = lineNodes[2].querySelector('.selection .region').getBoundingClientRect() + expect(region2Rect.top).toBe 2 * lineHeightInPixels + expect(region2Rect.height).toBe 1 * lineHeightInPixels + expect(region2Rect.left).toBe scrollViewClientLeft + expect(region2Rect.width).toBe lineNodes[2].offsetWidth - it "renders 3 regions for selections with more than 2 lines", -> - editor.setSelectedScreenRange([[1, 6], [5, 10]]) - regions = node.querySelectorAll('.selection .region') - expect(regions.length).toBe 3 + region3Rect = lineNodes[3].querySelector('.selection .region').getBoundingClientRect() + expect(region3Rect.top).toBe 3 * lineHeightInPixels + expect(region3Rect.height).toBe 1 * lineHeightInPixels + expect(region3Rect.left).toBe scrollViewClientLeft + 0 + expect(region3Rect.width).toBe 10 * charWidth - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 1 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(Math.ceil(region1Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed + backgroundNodes = node.querySelectorAll('.underlayer .selection .region') + expect(backgroundNodes.length).toBe 1 + backgroundRegionRect = backgroundNodes[0].getBoundingClientRect() - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 2 * lineHeightInPixels - expect(region2Rect.height).toBe 3 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft + 0 - expect(Math.ceil(region2Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed - - region3Rect = regions[2].getBoundingClientRect() - expect(region3Rect.top).toBe 5 * lineHeightInPixels - expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBe scrollViewClientLeft + 0 - expect(region3Rect.width).toBe 10 * charWidth + expect(backgroundRegionRect.top).toBe 1 * lineHeightInPixels + expect(backgroundRegionRect.left).toBe scrollViewClientLeft + expect(backgroundRegionRect.width).toBe scrollViewNode.offsetWidth + expect(backgroundRegionRect.height).toBe 2 * lineHeightInPixels it "does not render empty selections", -> expect(editor.getSelection().isEmpty()).toBe true From 0162247bd724d7f20d21864f7dd28989f1c44618 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 10:49:45 -0600 Subject: [PATCH 25/59] Precompute selection regions for all lines This is easer to reason about and probably more efficient than computing everything on a per-line basis. --- src/lines-component.coffee | 41 +++++++++++++++++++++++++++++--------- src/selection.coffee | 13 ------------ 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 9f2b926b1..1ec920d78 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -14,18 +14,41 @@ LinesComponent = React.createClass if @isMounted() {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange - visibleSelections = editor.selectionsForScreenRows(startRow, endRow - 1) + selectionRegionsByScreenRow = @getVisibleSelectionRegions() verticalScrollOffset = -scrollTop % lineHeight horizontalScrollOffset = -scrollLeft lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - selections = visibleSelections.filter (selection) -> not selection.isEmpty() and selection.intersectsScreenRow(screenRow) - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selections, selectionChanged}) + selectionRegions = selectionRegionsByScreenRow[screenRow] + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selectionRegions}) div {className: 'lines'}, lines + getVisibleSelectionRegions: -> + {editor, visibleRowRange, lineHeight} = @props + [visibleStartRow, visibleEndRow] = visibleRowRange + regions = {} + + for selection in editor.selectionsForScreenRows(visibleStartRow, visibleEndRow - 1) when not selection.isEmpty() + {start, end} = selection.getScreenRange() + + for screenRow in [start.row..end.row] + region = {id: selection.id, top: 0, left: 0, height: lineHeight} + + if screenRow is start.row + region.left = editor.pixelPositionForScreenPosition(start).left + if screenRow is end.row + region.width = editor.pixelPositionForScreenPosition(end).left - region.left + else + region.right = 0 + + regions[screenRow] ?= [] + regions[screenRow].push(region) + + regions + componentWillMount: -> @measuredLines = new WeakSet @@ -145,11 +168,11 @@ LineComponent = React.createClass "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" renderSelections: -> - {selections, screenRow} = @props - for selection in selections - div className: 'selection', key: selection.id, - div className: 'region', style: selection.regionRectForScreenRow(screenRow) + {selectionRegions} = @props + if selectionRegions? + for region in selectionRegions + div className: 'selection', key: region.id, + div className: 'region', style: region shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') - newProps.selectionChanged and (newProps.selections.length > 0 or @props.selections.length > 0) + return true unless isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'selectionRegions', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') diff --git a/src/selection.coffee b/src/selection.coffee index fdccb0242..d02eded36 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -591,19 +591,6 @@ class Selection extends Model compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) - regionRectForScreenRow: (screenRow) -> - {start, end} = @getScreenRange() - region = {height: @editor.getLineHeight(), top: 0, left: 0} - - if screenRow is start.row - region.left = @editor.pixelPositionForScreenPosition(start).left - - if screenRow is end.row - region.width = @editor.pixelPositionForScreenPosition(end).left - region.left - - region.right = 0 unless region.width? - region - getBackgroundRect: -> {start, end} = @getScreenRange() return if start.row is end.row From 9b02055db9c5004d2ccdaf37dc68a765aaa76a4a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 10:59:38 -0600 Subject: [PATCH 26/59] Move selection background region calculation into React component --- src/editor-scroll-view-component.coffee | 2 +- src/selection-backgrounds-component.coffee | 18 ++++++++++++------ src/selection.coffee | 11 ----------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 817980189..02e37b388 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -40,7 +40,7 @@ EditorScrollViewComponent = React.createClass selectionChanged } div className: 'underlayer', - SelectionBackgroundsComponent({editor, scrollTop, scrollLeft}) + SelectionBackgroundsComponent({editor, lineHeight, scrollTop}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/selection-backgrounds-component.coffee b/src/selection-backgrounds-component.coffee index 62152cc09..993007184 100644 --- a/src/selection-backgrounds-component.coffee +++ b/src/selection-backgrounds-component.coffee @@ -6,13 +6,19 @@ SelectionBackgroundsComponent = React.createClass displayName: 'SelectionBackgroundsComponent' render: -> - {editor, scrollTop} = @props + {editor, lineHeight, scrollTop} = @props div className: 'selections', if @isMounted() for selection in editor.getSelections() - if backgroundRect = selection.getBackgroundRect() - {top, left, right, height} = backgroundRect - WebkitTransform = "translate3d(0px, #{top - scrollTop}px, 0px)" - div className: 'selection', key: selection.id, - div className: 'region', style: {left, right, height, WebkitTransform} + {start, end} = selection.getScreenRange() + continue if start.row is end.row + + height = (end.row - start.row) * lineHeight + top = (start.row * lineHeight) - scrollTop + left = 0 + right = 0 + WebkitTransform = "translate3d(0px, #{top}px, 0px)" + + div className: 'selection', key: selection.id, + div className: 'region', style: {left, right, height, WebkitTransform} diff --git a/src/selection.coffee b/src/selection.coffee index d02eded36..5d70d9406 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -591,17 +591,6 @@ class Selection extends Model compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) - getBackgroundRect: -> - {start, end} = @getScreenRange() - return if start.row is end.row - - lineHeight = @editor.getLineHeight() - height = (end.row - start.row) * lineHeight - top = start.row * lineHeight - left = 0 - right = 0 - {top, left, right, height} - screenRangeChanged: -> screenRange = @getScreenRange() @emit 'screen-range-changed', screenRange From 070d239f41781fd8f04e73cdc581ba947853f182 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 12:12:04 -0600 Subject: [PATCH 27/59] Blink cursors with a CSS animation Now that they're on their own layer, I don't think it affects the repaint timing when typing on lines (if it ever did). --- spec/editor-component-spec.coffee | 43 ++++++------------------- src/cursor-component.coffee | 6 +--- src/cursors-component.coffee | 20 ++++++------ src/editor-component.coffee | 7 ++-- src/editor-scroll-view-component.coffee | 4 +-- static/editor.less | 12 +++++++ 6 files changed, 36 insertions(+), 56 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index ff6665cfb..a81a1dc68 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -268,44 +268,19 @@ describe "EditorComponent", -> expect(cursorRect.width).toBe rangeRect.width it "blinks cursors when they aren't moving", -> - editor.addCursorAtScreenPosition([1, 0]) - [cursorNode1, cursorNode2] = node.querySelectorAll('.cursor') - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false + jasmine.unspy(window, 'setTimeout') - advanceClock(component.props.cursorBlinkPeriod / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe true - expect(cursorNode2.classList.contains('blink-off')).toBe true + cursorsNode = node.querySelector('.cursors') + expect(cursorsNode.classList.contains('blinking')).toBe true - advanceClock(component.props.cursorBlinkPeriod / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false - - advanceClock(component.props.cursorBlinkPeriod / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe true - expect(cursorNode2.classList.contains('blink-off')).toBe true - - # Stop blinking immediately when cursors move - advanceClock(component.props.cursorBlinkPeriod / 4) - expect(cursorNode1.classList.contains('blink-off')).toBe true - expect(cursorNode2.classList.contains('blink-off')).toBe true - - # Stop blinking for one full period after moving the cursor + # Stop blinking after moving the cursor editor.moveCursorRight() - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false + expect(cursorsNode.classList.contains('blinking')).toBe false - advanceClock(component.props.cursorBlinkResumeDelay / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false - - advanceClock(component.props.cursorBlinkResumeDelay / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe true - expect(cursorNode2.classList.contains('blink-off')).toBe true - - advanceClock(component.props.cursorBlinkPeriod / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false + # Resume blinking after resume delay passes + waits component.props.cursorBlinkResumeDelay + runs -> + expect(cursorsNode.classList.contains('blinking')).toBe true it "renders the hidden input field at the position of the last cursor if it is on screen", -> inputNode = node.querySelector('.hidden-input') diff --git a/src/cursor-component.coffee b/src/cursor-component.coffee index 5dc0eccbf..36ba33a08 100644 --- a/src/cursor-component.coffee +++ b/src/cursor-component.coffee @@ -10,10 +10,6 @@ CursorComponent = React.createClass {top, left, height, width} = cursor.getPixelRect() top -= scrollTop left -= scrollLeft - - className = 'cursor' - className += ' blink-off' if @props.blinkOff - WebkitTransform = "translate3d(#{left}px, #{top}px, 0px)" - div className: className, style: {height, width, WebkitTransform} + div className: 'cursor', style: {height, width, WebkitTransform} diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 6883b10d5..50d53ad1d 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -4,7 +4,6 @@ React = require 'react' SubscriberMixin = require './subscriber-mixin' CursorComponent = require './cursor-component' - module.exports = CursorsComponent = React.createClass displayName: 'CursorsComponent' @@ -14,21 +13,23 @@ CursorsComponent = React.createClass render: -> {editor, scrollTop, scrollLeft} = @props - blinkOff = @state.blinkCursorsOff + {blinking} = @state - div className: 'cursors', + className = 'cursors' + className += ' blinking' if blinking + + div {className}, if @isMounted() for selection in editor.getSelections() if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) {cursor} = selection - CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft, blinkOff}) + CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft}) getInitialState: -> - blinkCursorsOff: false + blinking: true componentDidMount: -> {editor} = @props - @startBlinkingCursors() componentWillUnmount: -> clearInterval(@cursorBlinkIntervalHandle) @@ -37,14 +38,11 @@ CursorsComponent = React.createClass @pauseCursorBlinking() if cursorsMoved startBlinkingCursors: -> - @cursorBlinkIntervalHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2) + @setState(blinking: true) if @isMounted() startBlinkingCursorsAfterDelay: null # Created lazily - toggleCursorBlink: -> @setState(blinkCursorsOff: not @state.blinkCursorsOff) - pauseCursorBlinking: -> - @state.blinkCursorsOff = false - clearInterval(@cursorBlinkIntervalHandle) + @state.blinking = false @startBlinkingCursorsAfterDelay ?= debounce(@startBlinkingCursors, @props.cursorBlinkResumeDelay) @startBlinkingCursorsAfterDelay() diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 0dd622579..bb24726b4 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -30,7 +30,7 @@ EditorComponent = React.createClass render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state - {editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props + {editor, cursorBlinkResumeDelay} = @props maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() @@ -59,7 +59,7 @@ EditorComponent = React.createClass ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, @selectionChanged, - cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred + cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } ScrollbarComponent @@ -97,8 +97,7 @@ EditorComponent = React.createClass getInitialState: -> {} getDefaultProps: -> - cursorBlinkPeriod: 800 - cursorBlinkResumeDelay: 200 + cursorBlinkResumeDelay: 100 componentWillMount: -> @pendingChanges = [] diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 02e37b388..6290bc52b 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -18,7 +18,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged} = @props - {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props + {cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() inputStyle = @getHiddenInputPosition() @@ -33,7 +33,7 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, diff --git a/static/editor.less b/static/editor.less index f7e0d8188..eb4a57b65 100644 --- a/static/editor.less +++ b/static/editor.less @@ -24,6 +24,18 @@ z-index: 1; } + &.is-focused .cursors.blinking .cursor { + -webkit-animation: blink 0.8s; + -webkit-animation-iteration-count: infinite; + } + + @-webkit-keyframes blink { + 0% { opacity: .7; } + 50% { opacity: .7; } + 51% { opacity: 0; } + 100% { opacity: 0; } + } + .horizontal-scrollbar { position: absolute; left: 0; From f07a832c835325dd3130cec51cb057210d9f2492 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 12:37:55 -0600 Subject: [PATCH 28/59] Sync cursor animations when cursors are added --- src/cursors-component.coffee | 11 ++++++++++- src/editor-component.coffee | 14 ++++++++++++-- src/editor-scroll-view-component.coffee | 4 ++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 50d53ad1d..16213b1b9 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -1,6 +1,6 @@ React = require 'react' {div} = require 'reactionary' -{debounce} = require 'underscore-plus' +{debounce, toArray} = require 'underscore-plus' SubscriberMixin = require './subscriber-mixin' CursorComponent = require './cursor-component' @@ -37,6 +37,9 @@ CursorsComponent = React.createClass componentWillUpdate: ({cursorsMoved}) -> @pauseCursorBlinking() if cursorsMoved + componentDidUpdate: -> + @syncCursorAnimations() if @props.selectionAdded + startBlinkingCursors: -> @setState(blinking: true) if @isMounted() @@ -46,3 +49,9 @@ CursorsComponent = React.createClass @state.blinking = false @startBlinkingCursorsAfterDelay ?= debounce(@startBlinkingCursors, @props.cursorBlinkResumeDelay) @startBlinkingCursorsAfterDelay() + + syncCursorAnimations: -> + node = @getDOMNode() + cursorNodes = toArray(node.children) + node.removeChild(cursorNode) for cursorNode in cursorNodes + node.appendChild(cursorNode) for cursorNode in cursorNodes diff --git a/src/editor-component.coffee b/src/editor-component.coffee index bb24726b4..408216587 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -21,6 +21,7 @@ EditorComponent = React.createClass updateRequested: false cursorsMoved: false selectionChanged: false + selectionAdded: false scrollingVertically: false gutterWidth: 0 refreshingScrollbars: false @@ -59,7 +60,7 @@ EditorComponent = React.createClass ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, @selectionChanged, - cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred + @selectionAdded, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } ScrollbarComponent @@ -125,6 +126,7 @@ EditorComponent = React.createClass @pendingChanges.length = 0 @cursorsMoved = false @selectionChanged = false + @selectionAdded = false @refreshingScrollbars = false @measureScrollbars() if @measuringScrollbars @props.parentView.trigger 'editor:display-updated' @@ -135,7 +137,8 @@ EditorComponent = React.createClass @subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded @subscribe editor, 'screen-lines-changed', @onScreenLinesChanged @subscribe editor, 'cursors-moved', @onCursorsMoved - @subscribe editor, 'selection-added selection-removed selection-screen-range-changed', @onSelectionChanged + @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged + @subscribe editor, 'selection-added', @onSelectionAdded @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.changes, @requestUpdate @subscribe editor.$height.changes, @requestUpdate @@ -381,6 +384,13 @@ EditorComponent = React.createClass @selectionChanged = true @requestUpdate() + onSelectionAdded: (selection) -> + {editor} = @props + if editor.selectionIntersectsVisibleRowRange(selection) + @selectionChanged = true + @selectionAdded = true + @requestUpdate() + onScrollTopChanged: -> @scrollingVertically = true @requestUpdate() diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 6290bc52b..98ae26fcf 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged} = @props + {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged, selectionAdded} = @props {cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -33,7 +33,7 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkResumeDelay}) + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, From 8148e4e50db9104f749fa83d30bfc095007b43aa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 14:22:47 -0600 Subject: [PATCH 29/59] Skip selection restoration on our fork of react --- src/input-component.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input-component.coffee b/src/input-component.coffee index d441c2bce..d569dbd0d 100644 --- a/src/input-component.coffee +++ b/src/input-component.coffee @@ -10,7 +10,7 @@ InputComponent = React.createClass render: -> {className, style, onFocus, onBlur} = @props - input {className, style, onFocus, onBlur} + input {className, style, onFocus, onBlur, 'data-react-skip-selection-restoration': true} getInitialState: -> {lastChar: ''} From 4f9108980fa3a8690a29bee8e8517ef010269676 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 15:30:53 -0600 Subject: [PATCH 30/59] WIP: Manually update line nodes when scrolling --- src/lines-component.coffee | 79 ++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 1ec920d78..dd2e41c7b 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -3,6 +3,8 @@ React = require 'react' {debounce, isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus' {$$} = require 'space-pen' +EditorView = require './editor-view' + DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} @@ -11,20 +13,7 @@ LinesComponent = React.createClass displayName: 'LinesComponent' render: -> - if @isMounted() - {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props - [startRow, endRow] = visibleRowRange - selectionRegionsByScreenRow = @getVisibleSelectionRegions() - verticalScrollOffset = -scrollTop % lineHeight - horizontalScrollOffset = -scrollLeft - - lines = - for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) - screenRow = startRow + index - selectionRegions = selectionRegionsByScreenRow[screenRow] - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selectionRegions}) - - div {className: 'lines'}, lines + div {className: 'lines'} getVisibleSelectionRegions: -> {editor, visibleRowRange, lineHeight} = @props @@ -51,6 +40,7 @@ LinesComponent = React.createClass componentWillMount: -> @measuredLines = new WeakSet + @lineNodesByLineId = {} componentDidMount: -> @measureLineHeightAndCharWidth() @@ -66,10 +56,67 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> + @updateRenderedLines() @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically + updateRenderedLines: -> + {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props + [startRow, endRow] = visibleRowRange + verticalScrollOffset = -scrollTop % lineHeight + horizontalScrollOffset = -scrollLeft + + node = @getDOMNode() + + currentLineIds = new Set + lines = editor.linesForScreenRows(startRow, endRow - 1) + for line in lines + currentLineIds.add(line.id.toString()) + + for id, domNode of @lineNodesByLineId + unless currentLineIds.has(id) + delete @lineNodesByLineId[id] + node.removeChild(domNode) + + for line, index in lines + top = (index * lineHeight) + verticalScrollOffset + left = horizontalScrollOffset + screenRow = startRow + index + + if @hasNodeForLine(line.id) + @updateNodeForLine(line, screenRow, top, left) + else + @buildNodeForLine(line, screenRow, top, left) + + hasNodeForLine: (id) -> + @lineNodesByLineId[id]? + + buildNodeForLine: (tokenizedLine, screenRow, top, left) -> + {editor} = @props + {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = tokenizedLine + if fold + attributes = {class: 'fold line', 'fold-id': fold.id} + else + attributes = {class: 'line'} + + invisibles = {} + eolInvisibles = {} + htmlEolInvisibles = [] + indentation = indentLevel + + wrapper = document.createElement('div') + wrapper.innerHTML = EditorView.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, indentation, editor}) + lineNode = wrapper.children[0] + lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" + + @lineNodesByLineId[tokenizedLine.id] = lineNode + @getDOMNode().appendChild(lineNode) + + updateNodeForLine: (tokenizedLine, screenRow, top, left) -> + lineNode = @lineNodesByLineId[tokenizedLine.id] + lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" + measureLineHeightAndCharWidth: -> node = @getDOMNode() node.appendChild(DummyLineNode) @@ -85,9 +132,9 @@ LinesComponent = React.createClass [visibleStartRow, visibleEndRow] = @props.visibleRowRange node = @getDOMNode() - for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) + for tokenizedLine in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) unless @measuredLines.has(tokenizedLine) - lineNode = node.children[i] + lineNode = @lineNodesByLineId[tokenizedLine.id] @measureCharactersInLine(tokenizedLine, lineNode) measureCharactersInLine: (tokenizedLine, lineNode) -> From ea5c5c9e84d4b2e8c2399e569a571b9a6eab3ca8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 20:46:15 -0600 Subject: [PATCH 31/59] Move line HTML generation into lines component --- src/lines-component.coffee | 174 ++++++++++++++++++------------------- 1 file changed, 86 insertions(+), 88 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index dd2e41c7b..37d319d6c 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -1,12 +1,13 @@ React = require 'react' {div, span} = require 'reactionary' -{debounce, isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus' +{debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' {$$} = require 'space-pen' EditorView = require './editor-view' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} +WrapperDiv = document.createElement('div') module.exports = LinesComponent = React.createClass @@ -62,58 +63,105 @@ LinesComponent = React.createClass @measureCharactersInNewLines() unless @props.scrollingVertically updateRenderedLines: -> - {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props + {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange + visibleLines = editor.linesForScreenRows(startRow, endRow - 1) + @removeNonVisibleLineNodes(visibleLines) + @appendOrUpdateVisibleLineNodes(visibleLines) + + removeNonVisibleLineNodes: (visibleLines) -> + visibleLineIds = new Set + visibleLineIds.add(line.id.toString()) for line in visibleLines + node = @getDOMNode() + for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId) + delete @lineNodesByLineId[lineId] + node.removeChild(lineNode) + + appendOrUpdateVisibleLineNodes: (visibleLines) -> + {scrollTop, scrollLeft, lineHeight} = @props + newLines = null + newLinesHTML = null verticalScrollOffset = -scrollTop % lineHeight horizontalScrollOffset = -scrollLeft - node = @getDOMNode() - - currentLineIds = new Set - lines = editor.linesForScreenRows(startRow, endRow - 1) - for line in lines - currentLineIds.add(line.id.toString()) - - for id, domNode of @lineNodesByLineId - unless currentLineIds.has(id) - delete @lineNodesByLineId[id] - node.removeChild(domNode) - - for line, index in lines + for line, index in visibleLines top = (index * lineHeight) + verticalScrollOffset left = horizontalScrollOffset - screenRow = startRow + index - if @hasNodeForLine(line.id) - @updateNodeForLine(line, screenRow, top, left) + if @hasLineNode(line.id) + @updateLineNode(line, top, left) else - @buildNodeForLine(line, screenRow, top, left) + newLines ?= [] + newLinesHTML ?= "" + newLines.push(line) + newLinesHTML += @buildLineHTML(line, top, left) - hasNodeForLine: (id) -> - @lineNodesByLineId[id]? + return unless newLines? - buildNodeForLine: (tokenizedLine, screenRow, top, left) -> - {editor} = @props - {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = tokenizedLine - if fold - attributes = {class: 'fold line', 'fold-id': fold.id} + WrapperDiv.innerHTML = newLinesHTML + newLineNodes = toArray(WrapperDiv.children) + node = @getDOMNode() + for line, i in newLines + lineNode = newLineNodes[i] + @lineNodesByLineId[line.id] = lineNode + node.appendChild(lineNode) + + hasLineNode: (lineId) -> + @lineNodesByLineId.hasOwnProperty(lineId) + + buildTranslate3d: (top, left) -> + "translate3d(#{left}px, #{top}px, 0px)" + + buildLineHTML: (line, top, left) -> + {editor, mini, showIndentGuide, invisibles} = @props + {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line + translate3d = @buildTranslate3d(top, left) + line = "
" + + if text is "" + line += " " else - attributes = {class: 'line'} + scopeStack = [] + firstTrailingWhitespacePosition = text.search(/\s*$/) + lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 + for token in tokens + line += @updateScopeStack(scopeStack, token.scopes) + hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly) + line += token.getValueAsHtml({invisibles, hasIndentGuide}) + line += @popScope(scopeStack) while scopeStack.length > 0 - invisibles = {} - eolInvisibles = {} - htmlEolInvisibles = [] - indentation = indentLevel + # line.push(htmlEolInvisibles) unless text == '' + # line.push("") if fold - wrapper = document.createElement('div') - wrapper.innerHTML = EditorView.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, indentation, editor}) - lineNode = wrapper.children[0] - lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" + line += "
" + line - @lineNodesByLineId[tokenizedLine.id] = lineNode - @getDOMNode().appendChild(lineNode) + updateScopeStack: (scopeStack, desiredScopes) -> + html = "" - updateNodeForLine: (tokenizedLine, screenRow, top, left) -> + # Find a common prefix + for scope, i in desiredScopes + break unless scopeStack[i]?.scope is desiredScopes[i] + + # Pop scopes until we're at the common prefx + until scopeStack.length is i + html += @popScope(scopeStack) + + # Push onto common prefix until scopeStack equals desiredScopes + for j in [i...desiredScopes.length] + html += @pushScope(scopeStack, desiredScopes[j]) + + html + + popScope: (scopeStack) -> + scopeStack.pop() + "" + + pushScope: (scopeStack, scope) -> + scopeStack.push(scope) + "" + + updateLineNode: (tokenizedLine, top, left) -> lineNode = @lineNodesByLineId[tokenizedLine.id] lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" @@ -173,53 +221,3 @@ LinesComponent = React.createClass clearScopedCharWidths: -> @measuredLines.clear() @props.editor.clearScopedCharWidths() - - -LineComponent = React.createClass - displayName: 'LineComponent' - - render: -> - {index, screenRow, verticalScrollOffset, horizontalScrollOffset, lineHeight} = @props - - top = index * lineHeight + verticalScrollOffset - left = horizontalScrollOffset - style = WebkitTransform: "translate3d(#{left}px, #{top}px, 0px)" - - div className: 'line editor-colors', style: style, 'data-screen-row': screenRow, - span dangerouslySetInnerHTML: {__html: @buildTokensHTML()} - @renderSelections() - - buildTokensHTML: -> - if @props.tokenizedLine.text.length is 0 - @buildEmptyLineHTML() - else - @buildScopeTreeHTML(@props.tokenizedLine.getScopeTree()) - - buildEmptyLineHTML: -> - {showIndentGuide, tokenizedLine} = @props - {indentLevel, tabLength} = tokenizedLine - - if showIndentGuide and indentLevel > 0 - indentSpan = "#{multiplyString(' ', tabLength)}" - multiplyString(indentSpan, indentLevel + 1) - else - " " - - buildScopeTreeHTML: (scopeTree) -> - if scopeTree.children? - html = "" - html += @buildScopeTreeHTML(child) for child in scopeTree.children - html += "" - html - else - "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" - - renderSelections: -> - {selectionRegions} = @props - if selectionRegions? - for region in selectionRegions - div className: 'selection', key: region.id, - div className: 'region', style: region - - shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'selectionRegions', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') From 695f8da3c353114dd4011ecb7d0280d90d614150 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 21:12:47 -0600 Subject: [PATCH 32/59] :lipstick: extract buildLineInnerHTML method --- src/lines-component.coffee | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 37d319d6c..3c9168bc8 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -113,28 +113,33 @@ LinesComponent = React.createClass "translate3d(#{left}px, #{top}px, 0px)" buildLineHTML: (line, top, left) -> - {editor, mini, showIndentGuide, invisibles} = @props + {editor, mini, showIndentGuide} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line translate3d = @buildTranslate3d(top, left) - line = "
" + lineHTML = "
" if text is "" - line += " " + lineHTML += " " else - scopeStack = [] - firstTrailingWhitespacePosition = text.search(/\s*$/) - lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 - for token in tokens - line += @updateScopeStack(scopeStack, token.scopes) - hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly) - line += token.getValueAsHtml({invisibles, hasIndentGuide}) - line += @popScope(scopeStack) while scopeStack.length > 0 + lineHTML += @buildLineInnerHTML(line) - # line.push(htmlEolInvisibles) unless text == '' - # line.push("") if fold + lineHTML += "
" + lineHTML - line += "
" - line + buildLineInnerHTML: (line) -> + {invisibles, mini, showIndentGuide} = @props + {tokens, text} = line + innerHTML = "" + + scopeStack = [] + firstTrailingWhitespacePosition = text.search(/\s*$/) + lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 + for token in tokens + innerHTML += @updateScopeStack(scopeStack, token.scopes) + hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly) + innerHTML += token.getValueAsHtml({invisibles, hasIndentGuide}) + innerHTML += @popScope(scopeStack) while scopeStack.length > 0 + innerHTML updateScopeStack: (scopeStack, desiredScopes) -> html = "" From e9bff37e06ef3263a05931d4e92f682e2a7a8ea5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 08:47:15 -0600 Subject: [PATCH 33/59] Render line numbers manually --- src/gutter-component.coffee | 144 +++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 59 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 214b1af52..30a487cf3 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -1,8 +1,10 @@ React = require 'react' {div} = require 'reactionary' -{isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus' +{isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' SubscriberMixin = require './subscriber-mixin' +WrapperDiv = document.createElement('div') + module.exports = GutterComponent = React.createClass displayName: 'GutterComponent' @@ -14,43 +16,10 @@ GutterComponent = React.createClass {width} = @props div className: 'gutter', style: {width}, - div className: 'line-numbers', ref: 'lineNumbers', - if @isMounted() - @renderLineNumbers() - else - @renderLineNumberForMeasurement() + div className: 'line-numbers', ref: 'lineNumbers' - renderLineNumbers: -> - {editor, visibleRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props - [startRow, endRow] = visibleRowRange - maxLineNumberDigits = @getMaxLineNumberDigits() - scrollOffset = -scrollTop % lineHeight - wrapCount = 0 - - for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) - if bufferRow is lastBufferRow - lineNumber = '•' - key = "#{bufferRow + 1}-#{++wrapCount}" - else - lastBufferRow = bufferRow - wrapCount = 0 - lineNumber = "#{bufferRow + 1}" - key = lineNumber - - LineNumberComponent({key, lineNumber, maxLineNumberDigits, index, lineHeight, scrollOffset}) - - renderLineNumberForMeasurement: -> - LineNumberComponent( - key: 'forMeasurement' - lineNumber: '•' - maxLineNumberDigits: @getMaxLineNumberDigits() - index: 0 - lineHeight: 0 - scrollOffset: 0 - ) - - getMaxLineNumberDigits: -> - @props.editor.getLineCount().toString().length + componentWillMount: -> + @lineNumberNodesById = {} # 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 @@ -59,37 +28,94 @@ GutterComponent = React.createClass return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'scrollTop', 'lineHeight', 'fontSize') {visibleRowRange, pendingChanges} = newProps - for change in pendingChanges when change.screenDelta > 0 or change.bufferDelta > 0 + for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0 return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start false componentDidUpdate: (oldProps) -> + @updateLineNumbers() unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') - width = @refs.lineNumbers.getDOMNode().firstChild.offsetWidth - if width isnt @lastMeasuredWidth - @lastMeasuredWidth = width - @props.onWidthChanged(width) + @measureWidth() -LineNumberComponent = React.createClass - displayName: 'LineNumberComponent' + updateLineNumbers: -> + visibleLineNumberIds = @appendOrUpdateVisibleLineNumberNodes() + @removeNonVisibleLineNumberNodes(visibleLineNumberIds) - render: -> - {index, lineHeight, scrollOffset} = @props - div - className: "line-number editor-colors" - style: {WebkitTransform: "translate3d(0px, #{index * lineHeight + scrollOffset}px, 0px)"} - dangerouslySetInnerHTML: {__html: @buildInnerHTML()} + appendOrUpdateVisibleLineNumberNodes: -> + {editor, visibleRowRange, scrollTop, lineHeight} = @props + [startRow, endRow] = visibleRowRange + maxLineNumberDigits = editor.getLineCount().toString().length + verticalScrollOffset = -scrollTop % lineHeight + newLineNumberIds = null + newLineNumbersHTML = null + visibleLineNumberIds = new Set - buildInnerHTML: -> - {lineNumber, maxLineNumberDigits} = @props - if lineNumber.length < maxLineNumberDigits - padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length) - padding + lineNumber + @iconDivHTML + wrapCount = 0 + for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) + if bufferRow is lastBufferRow + id = "#{bufferRow}-#{wrapCount++}" + else + id = bufferRow.toString() + lastBufferRow = bufferRow + wrapCount = 0 + + visibleLineNumberIds.add(id) + + top = (index * lineHeight) + verticalScrollOffset + + if @hasLineNumberNode(id) + @updateLineNumberNode(id, top) + else + newLineNumberIds ?= [] + newLineNumbersHTML ?= "" + newLineNumberIds.push(id) + newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, top) + + if newLineNumberIds? + WrapperDiv.innerHTML = newLineNumbersHTML + newLineNumberNodes = toArray(WrapperDiv.children) + + node = @refs.lineNumbers.getDOMNode() + for lineNumberId, i in newLineNumberIds + lineNumberNode = newLineNumberNodes[i] + @lineNumberNodesById[lineNumberId] = lineNumberNode + node.appendChild(lineNumberNode) + + visibleLineNumberIds + + removeNonVisibleLineNumberNodes: (visibleLineNumberIds) -> + node = @refs.lineNumbers.getDOMNode() + for id, lineNumberNode of @lineNumberNodesById when not visibleLineNumberIds.has(id) + delete @lineNumberNodesById[id] + node.removeChild(lineNumberNode) + + buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> + if softWrapped + lineNumber = "•" else - lineNumber + @iconDivHTML + lineNumber = (bufferRow + 1).toString() - iconDivHTML: '
' + padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length) + iconHTML = '
' + innerHTML = padding + lineNumber + iconHTML + translate3d = @buildTranslate3d(top) - shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'index', 'lineHeight', 'scrollOffset') + "
#{innerHTML}
" + + updateLineNumberNode: (lineNumberId, top) -> + @lineNumberNodesById[lineNumberId].style['-webkit-transform'] = @buildTranslate3d(top) + + hasLineNumberNode: (lineNumberId) -> + @lineNumberNodesById.hasOwnProperty(lineNumberId) + + buildTranslate3d: (top) -> + "translate3d(0px, #{top}px, 0px)" + + measureWidth: -> + lineNumberNode = @refs.lineNumbers.getDOMNode().firstChild + # return unless lineNumberNode? + + width = lineNumberNode.offsetWidth + if width isnt @lastMeasuredWidth + @props.onWidthChanged(@lastMeasuredWidth = width) From c60e5d90fd84ef17347806b11146241bcf2d83b8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 10:16:29 -0600 Subject: [PATCH 34/59] :lipstick: --- src/lines-component.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 3c9168bc8..092bb842c 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -57,12 +57,12 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> - @updateRenderedLines() + @updateLines() @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically - updateRenderedLines: -> + updateLines: -> {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange visibleLines = editor.linesForScreenRows(startRow, endRow - 1) From 3a2de9c6985fb5c9cf51654be667b78bcf517a7c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 12:35:45 -0600 Subject: [PATCH 35/59] Don't render every line on the GPU Opaque lines are turning out to be a total pain, plus they ruin absolute positioning on the lines div. The slight speed boost isn't seeming worth it anymore. --- src/lines-component.coffee | 39 +++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 092bb842c..febed6140 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -14,7 +14,14 @@ LinesComponent = React.createClass displayName: 'LinesComponent' render: -> - div {className: 'lines'} + if @isMounted() + {editor, scrollTop, scrollLeft} = @props + style = + height: editor.getScrollHeight() + width: editor.getScrollWidth() + WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" + + div {className: 'lines editor-colors', style} getVisibleSelectionRegions: -> {editor, visibleRowRange, lineHeight} = @props @@ -65,9 +72,13 @@ LinesComponent = React.createClass updateLines: -> {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange + + startRow = Math.max(0, startRow - 8) + endRow = Math.min(editor.getLineCount(), endRow + 8) + visibleLines = editor.linesForScreenRows(startRow, endRow - 1) @removeNonVisibleLineNodes(visibleLines) - @appendOrUpdateVisibleLineNodes(visibleLines) + @appendOrUpdateVisibleLineNodes(visibleLines, startRow) removeNonVisibleLineNodes: (visibleLines) -> visibleLineIds = new Set @@ -77,24 +88,22 @@ LinesComponent = React.createClass delete @lineNodesByLineId[lineId] node.removeChild(lineNode) - appendOrUpdateVisibleLineNodes: (visibleLines) -> - {scrollTop, scrollLeft, lineHeight} = @props + appendOrUpdateVisibleLineNodes: (visibleLines, startRow) -> + {lineHeight} = @props newLines = null newLinesHTML = null - verticalScrollOffset = -scrollTop % lineHeight - horizontalScrollOffset = -scrollLeft for line, index in visibleLines - top = (index * lineHeight) + verticalScrollOffset - left = horizontalScrollOffset + screenRow = startRow + index + top = (screenRow * lineHeight) if @hasLineNode(line.id) - @updateLineNode(line, top, left) + @updateLineNode(line, top) else newLines ?= [] newLinesHTML ?= "" newLines.push(line) - newLinesHTML += @buildLineHTML(line, top, left) + newLinesHTML += @buildLineHTML(line, top) return unless newLines? @@ -109,14 +118,14 @@ LinesComponent = React.createClass hasLineNode: (lineId) -> @lineNodesByLineId.hasOwnProperty(lineId) - buildTranslate3d: (top, left) -> - "translate3d(#{left}px, #{top}px, 0px)" + buildTranslate3d: (top) -> + "translate3d(0px, #{top}px, 0px)" buildLineHTML: (line, top, left) -> {editor, mini, showIndentGuide} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line translate3d = @buildTranslate3d(top, left) - lineHTML = "
" + lineHTML = "
" if text is "" lineHTML += " " @@ -166,9 +175,9 @@ LinesComponent = React.createClass scopeStack.push(scope) "" - updateLineNode: (tokenizedLine, top, left) -> + updateLineNode: (tokenizedLine, top) -> lineNode = @lineNodesByLineId[tokenizedLine.id] - lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" + lineNode.style.top = top + 'px' measureLineHeightAndCharWidth: -> node = @getDOMNode() From a118cdd32b3e5f01d212ca7383d3191e26bc26a7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 13:50:25 -0600 Subject: [PATCH 36/59] Put selections and lines on the GPU together in sibling divs --- src/editor-component.coffee | 5 +- src/editor-scroll-view-component.coffee | 26 +++++---- src/lines-component.coffee | 31 +---------- src/selection-backgrounds-component.coffee | 24 -------- src/selection-component.coffee | 65 ++++++++++++++++++++++ src/selections-component.coffee | 16 ++++++ static/editor.less | 9 +++ 7 files changed, 110 insertions(+), 66 deletions(-) delete mode 100644 src/selection-backgrounds-component.coffee create mode 100644 src/selection-component.coffee create mode 100644 src/selections-component.coffee diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 408216587..353ad6281 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -59,8 +59,9 @@ EditorComponent = React.createClass EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, - scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, @selectionChanged, - @selectionAdded, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred + scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, + @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay, + @onInputFocused, @onInputBlurred } ScrollbarComponent diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 98ae26fcf..934c90cab 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -5,7 +5,7 @@ React = require 'react' InputComponent = require './input-component' LinesComponent = require './lines-component' CursorsComponent = require './cursors-component' -SelectionBackgroundsComponent = require './selection-backgrounds-component' +SelectionsComponent = require './selections-component' module.exports = EditorScrollViewComponent = React.createClass @@ -17,12 +17,16 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged, selectionAdded} = @props - {cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props + {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props + {selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() inputStyle = @getHiddenInputPosition() inputStyle.WebkitTransform = 'translateZ(0)' + contentStyle = + height: scrollHeight + width: scrollWidth + WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" div className: 'scroll-view', onMouseDown: @onMouseDown, InputComponent @@ -33,14 +37,14 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) - LinesComponent { - ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, - selectionChanged - } - div className: 'underlayer', - SelectionBackgroundsComponent({editor, lineHeight, scrollTop}) + div className: 'scroll-view-content editor-colors', style: contentStyle, + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) + LinesComponent { + ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, + visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + selectionChanged + } + SelectionsComponent({editor, lineHeight}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/lines-component.coffee b/src/lines-component.coffee index febed6140..e8182ecca 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -16,35 +16,8 @@ LinesComponent = React.createClass render: -> if @isMounted() {editor, scrollTop, scrollLeft} = @props - style = - height: editor.getScrollHeight() - width: editor.getScrollWidth() - WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" - div {className: 'lines editor-colors', style} - - getVisibleSelectionRegions: -> - {editor, visibleRowRange, lineHeight} = @props - [visibleStartRow, visibleEndRow] = visibleRowRange - regions = {} - - for selection in editor.selectionsForScreenRows(visibleStartRow, visibleEndRow - 1) when not selection.isEmpty() - {start, end} = selection.getScreenRange() - - for screenRow in [start.row..end.row] - region = {id: selection.id, top: 0, left: 0, height: lineHeight} - - if screenRow is start.row - region.left = editor.pixelPositionForScreenPosition(start).left - if screenRow is end.row - region.width = editor.pixelPositionForScreenPosition(end).left - region.left - else - region.right = 0 - - regions[screenRow] ?= [] - regions[screenRow].push(region) - - regions + div {className: 'lines'} componentWillMount: -> @measuredLines = new WeakSet @@ -125,7 +98,7 @@ LinesComponent = React.createClass {editor, mini, showIndentGuide} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line translate3d = @buildTranslate3d(top, left) - lineHTML = "
" + lineHTML = "
" if text is "" lineHTML += " " diff --git a/src/selection-backgrounds-component.coffee b/src/selection-backgrounds-component.coffee deleted file mode 100644 index 993007184..000000000 --- a/src/selection-backgrounds-component.coffee +++ /dev/null @@ -1,24 +0,0 @@ -React = require 'react' -{div} = require 'reactionary' - -module.exports = -SelectionBackgroundsComponent = React.createClass - displayName: 'SelectionBackgroundsComponent' - - render: -> - {editor, lineHeight, scrollTop} = @props - - div className: 'selections', - if @isMounted() - for selection in editor.getSelections() - {start, end} = selection.getScreenRange() - continue if start.row is end.row - - height = (end.row - start.row) * lineHeight - top = (start.row * lineHeight) - scrollTop - left = 0 - right = 0 - WebkitTransform = "translate3d(0px, #{top}px, 0px)" - - div className: 'selection', key: selection.id, - div className: 'region', style: {left, right, height, WebkitTransform} diff --git a/src/selection-component.coffee b/src/selection-component.coffee new file mode 100644 index 000000000..57eb6d0b8 --- /dev/null +++ b/src/selection-component.coffee @@ -0,0 +1,65 @@ +React = require 'react' +{div} = require 'reactionary' + +module.exports = +SelectionComponent = React.createClass + displayName: 'SelectionComponent' + + render: -> + {editor, selection, lineHeight} = @props + {start, end} = selection.getScreenRange() + rowCount = end.row - start.row + 1 + startPixelPosition = editor.pixelPositionForScreenPosition(start) + endPixelPosition = editor.pixelPositionForScreenPosition(end) + + div className: 'selection', + if rowCount is 1 + @renderSingleLineRegions(startPixelPosition, endPixelPosition) + else + @renderMultiLineRegions(startPixelPosition, endPixelPosition, rowCount) + + renderSingleLineRegions: (startPixelPosition, endPixelPosition) -> + {lineHeight} = @props + + [ + div className: 'region', key: 0, style: + top: startPixelPosition.top + height: lineHeight + left: startPixelPosition.left + width: endPixelPosition.left - startPixelPosition.left + ] + + renderMultiLineRegions: (startPixelPosition, endPixelPosition, rowCount) -> + {lineHeight} = @props + regions = [] + index = 0 + + # First row, extending from selection start to the right side of screen + regions.push( + div className: 'region', key: index++, style: + top: startPixelPosition.top + left: startPixelPosition.left + height: lineHeight + right: 0 + ) + + # Middle rows, extending from left side to right side of screen + if rowCount > 2 + regions.push( + div className: 'region', key: index++, style: + top: startPixelPosition.top + lineHeight + height: (rowCount - 2) * lineHeight + left: 0 + right: 0 + ) + + # Last row, extending from left side of screen to selection end + regions.push( + div className: 'region', key: index, style: + top: endPixelPosition.top + height: lineHeight + left: 0 + width: endPixelPosition.left + ) + + regions diff --git a/src/selections-component.coffee b/src/selections-component.coffee new file mode 100644 index 000000000..ad9ed61e1 --- /dev/null +++ b/src/selections-component.coffee @@ -0,0 +1,16 @@ +React = require 'react' +{div} = require 'reactionary' +SelectionComponent = require './selection-component' + +module.exports = +SelectionsComponent = React.createClass + displayName: 'SelectionsComponent' + + render: -> + {editor, lineHeight} = @props + + div className: 'selections', + if @isMounted() + for selection in editor.getSelections() + if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) + SelectionComponent({key: selection.id, selection, editor, lineHeight}) diff --git a/static/editor.less b/static/editor.less index eb4a57b65..f31b44206 100644 --- a/static/editor.less +++ b/static/editor.less @@ -12,6 +12,15 @@ z-index: -2; } + .selections { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: -1; + } + .lines { z-index: 0; From d15fd34f7a5bc0171b561757eedf1ce8f80a990d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 18:05:54 -0600 Subject: [PATCH 37/59] Render selections on lines layer; don't put each line number on GPU --- src/editor-scroll-view-component.coffee | 18 ++++++------------ src/gutter-component.coffee | 17 +++++++++++------ src/lines-component.coffee | 12 ++++++++---- src/selections-component.coffee | 5 +++-- static/editor.less | 9 --------- 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 934c90cab..2985910b9 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -23,10 +23,6 @@ EditorScrollViewComponent = React.createClass if @isMounted() inputStyle = @getHiddenInputPosition() inputStyle.WebkitTransform = 'translateZ(0)' - contentStyle = - height: scrollHeight - width: scrollWidth - WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" div className: 'scroll-view', onMouseDown: @onMouseDown, InputComponent @@ -37,14 +33,12 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - div className: 'scroll-view-content editor-colors', style: contentStyle, - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) - LinesComponent { - ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, - selectionChanged - } - SelectionsComponent({editor, lineHeight}) + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) + LinesComponent { + ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, + visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + selectionChanged, scrollHeight, scrollWidth + } componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 30a487cf3..0a68ccd04 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -13,10 +13,12 @@ GutterComponent = React.createClass lastMeasuredWidth: null render: -> - {width} = @props + {width, scrollHeight, scrollTop} = @props div className: 'gutter', style: {width}, - div className: 'line-numbers', ref: 'lineNumbers' + div className: 'line-numbers', ref: 'lineNumbers', style: + height: scrollHeight + WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)" componentWillMount: -> @lineNumberNodesById = {} @@ -45,8 +47,10 @@ GutterComponent = React.createClass appendOrUpdateVisibleLineNumberNodes: -> {editor, visibleRowRange, scrollTop, lineHeight} = @props [startRow, endRow] = visibleRowRange + startRow = Math.max(0, startRow - 8) + endRow = Math.min(editor.getLineCount(), endRow + 8) + maxLineNumberDigits = editor.getLineCount().toString().length - verticalScrollOffset = -scrollTop % lineHeight newLineNumberIds = null newLineNumbersHTML = null visibleLineNumberIds = new Set @@ -62,7 +66,8 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) - top = (index * lineHeight) + verticalScrollOffset + screenRow = startRow + index + top = screenRow * lineHeight if @hasLineNumberNode(id) @updateLineNumberNode(id, top) @@ -101,10 +106,10 @@ GutterComponent = React.createClass innerHTML = padding + lineNumber + iconHTML translate3d = @buildTranslate3d(top) - "
#{innerHTML}
" + "
#{innerHTML}
" updateLineNumberNode: (lineNumberId, top) -> - @lineNumberNodesById[lineNumberId].style['-webkit-transform'] = @buildTranslate3d(top) + @lineNumberNodesById[lineNumberId].style.top = top + 'px' hasLineNumberNode: (lineNumberId) -> @lineNumberNodesById.hasOwnProperty(lineNumberId) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index e8182ecca..a7ba5dba6 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -3,7 +3,7 @@ React = require 'react' {debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' {$$} = require 'space-pen' -EditorView = require './editor-view' +SelectionsComponent = require './selections-component' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} @@ -15,9 +15,14 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, scrollTop, scrollLeft} = @props + {editor, scrollTop, scrollLeft, scrollHeight, scrollWidth, lineHeight} = @props + style = + height: scrollHeight + width: scrollWidth + WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" - div {className: 'lines'} + div {className: 'lines', style}, + SelectionsComponent({editor, lineHeight}) if @isMounted componentWillMount: -> @measuredLines = new WeakSet @@ -45,7 +50,6 @@ LinesComponent = React.createClass updateLines: -> {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange - startRow = Math.max(0, startRow - 8) endRow = Math.min(editor.getLineCount(), endRow + 8) diff --git a/src/selections-component.coffee b/src/selections-component.coffee index ad9ed61e1..014600e4f 100644 --- a/src/selections-component.coffee +++ b/src/selections-component.coffee @@ -11,6 +11,7 @@ SelectionsComponent = React.createClass div className: 'selections', if @isMounted() - for selection in editor.getSelections() - if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) + for selection, index in editor.getSelections() + # Rendering artifacts occur on the lines GPU layer if we remove the last selection + if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)) SelectionComponent({key: selection.id, selection, editor, lineHeight}) diff --git a/static/editor.less b/static/editor.less index f31b44206..eb4a57b65 100644 --- a/static/editor.less +++ b/static/editor.less @@ -12,15 +12,6 @@ z-index: -2; } - .selections { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: -1; - } - .lines { z-index: 0; From 89bd241a7870f5bdebaca8e97036f52364e5b901 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 18:15:47 -0600 Subject: [PATCH 38/59] Always run react in dev mode for now --- src/editor.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 3dc51555d..bc8b72759 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -214,10 +214,10 @@ class Editor extends Model @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... getViewClass: -> - if atom.config.get('core.useReactEditor') - require './react-editor-view' - else - require './editor-view' + # if atom.config.get('core.useReactEditor') + require './react-editor-view' + # else + # require './editor-view' destroyed: -> @unsubscribe() From c87bc57f9eae6d2658bf85194a599068a6771687 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 19:58:50 -0600 Subject: [PATCH 39/59] Don't update top positions of lines/lineNodes unless they have changed --- src/gutter-component.coffee | 7 ++++++- src/lines-component.coffee | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 0a68ccd04..a099936e3 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -22,6 +22,7 @@ GutterComponent = React.createClass componentWillMount: -> @lineNumberNodesById = {} + @lineNumberNodeTopPositions = {} # 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 @@ -76,6 +77,7 @@ GutterComponent = React.createClass newLineNumbersHTML ?= "" newLineNumberIds.push(id) newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, top) + @lineNumberNodeTopPositions[id] = top if newLineNumberIds? WrapperDiv.innerHTML = newLineNumbersHTML @@ -93,6 +95,7 @@ GutterComponent = React.createClass node = @refs.lineNumbers.getDOMNode() for id, lineNumberNode of @lineNumberNodesById when not visibleLineNumberIds.has(id) delete @lineNumberNodesById[id] + delete @lineNumberNodeTopPositions[id] node.removeChild(lineNumberNode) buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> @@ -109,7 +112,9 @@ GutterComponent = React.createClass "
#{innerHTML}
" updateLineNumberNode: (lineNumberId, top) -> - @lineNumberNodesById[lineNumberId].style.top = top + 'px' + unless @lineNumberNodeTopPositions[lineNumberId] is top + @lineNumberNodesById[lineNumberId].style.top = top + 'px' + @lineNumberNodeTopPositions[lineNumberId] = top hasLineNumberNode: (lineNumberId) -> @lineNumberNodesById.hasOwnProperty(lineNumberId) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index a7ba5dba6..51bae0044 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -27,6 +27,7 @@ LinesComponent = React.createClass componentWillMount: -> @measuredLines = new WeakSet @lineNodesByLineId = {} + @lineNodeTopPositions = {} componentDidMount: -> @measureLineHeightAndCharWidth() @@ -63,6 +64,7 @@ LinesComponent = React.createClass node = @getDOMNode() for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId) delete @lineNodesByLineId[lineId] + delete @lineNodeTopPositions[lineId] node.removeChild(lineNode) appendOrUpdateVisibleLineNodes: (visibleLines, startRow) -> @@ -81,6 +83,7 @@ LinesComponent = React.createClass newLinesHTML ?= "" newLines.push(line) newLinesHTML += @buildLineHTML(line, top) + @lineNodeTopPositions[line.id] = top return unless newLines? @@ -152,9 +155,11 @@ LinesComponent = React.createClass scopeStack.push(scope) "" - updateLineNode: (tokenizedLine, top) -> - lineNode = @lineNodesByLineId[tokenizedLine.id] - lineNode.style.top = top + 'px' + updateLineNode: (line, top) -> + unless @lineNodeTopPositions[line.id] is top + lineNode = @lineNodesByLineId[line.id] + lineNode.style.top = top + 'px' + @lineNodeTopPositions[line.id] = top measureLineHeightAndCharWidth: -> node = @getDOMNode() From 7dfe829fc848d0f5be8b0418537adbd3754a8a03 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 11:00:39 -0600 Subject: [PATCH 40/59] Style lines with inline styles for performance --- src/lines-component.coffee | 2 +- static/editor.less | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 51bae0044..3fe9e741c 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -105,7 +105,7 @@ LinesComponent = React.createClass {editor, mini, showIndentGuide} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line translate3d = @buildTranslate3d(top, left) - lineHTML = "
" + lineHTML = "
" if text is "" lineHTML += " " diff --git a/static/editor.less b/static/editor.less index eb4a57b65..4bd80a1e6 100644 --- a/static/editor.less +++ b/static/editor.less @@ -12,14 +12,6 @@ z-index: -2; } - .lines { - z-index: 0; - - > .line { - position: absolute; - } - } - .cursor { z-index: 1; } From 3f01e2f7484eb5b72884aa96a151ee664041c18e Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 15 May 2014 11:50:25 -0600 Subject: [PATCH 41/59] Implement shouldComponentUpdate for SelectionsComponent --- src/lines-component.coffee | 2 +- src/selection-component.coffee | 4 ++-- src/selections-component.coffee | 38 +++++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 3fe9e741c..89d992db3 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -22,7 +22,7 @@ LinesComponent = React.createClass WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" div {className: 'lines', style}, - SelectionsComponent({editor, lineHeight}) if @isMounted + SelectionsComponent({editor, lineHeight}) if @isMounted() componentWillMount: -> @measuredLines = new WeakSet diff --git a/src/selection-component.coffee b/src/selection-component.coffee index 57eb6d0b8..8f4a1f20a 100644 --- a/src/selection-component.coffee +++ b/src/selection-component.coffee @@ -6,8 +6,8 @@ SelectionComponent = React.createClass displayName: 'SelectionComponent' render: -> - {editor, selection, lineHeight} = @props - {start, end} = selection.getScreenRange() + {editor, screenRange, lineHeight} = @props + {start, end} = screenRange rowCount = end.row - start.row + 1 startPixelPosition = editor.pixelPositionForScreenPosition(start) endPixelPosition = editor.pixelPositionForScreenPosition(end) diff --git a/src/selections-component.coffee b/src/selections-component.coffee index 014600e4f..524f7236a 100644 --- a/src/selections-component.coffee +++ b/src/selections-component.coffee @@ -7,11 +7,37 @@ SelectionsComponent = React.createClass displayName: 'SelectionsComponent' render: -> + div className: 'selections', @renderSelections() + + renderSelections: -> {editor, lineHeight} = @props - div className: 'selections', - if @isMounted() - for selection, index in editor.getSelections() - # Rendering artifacts occur on the lines GPU layer if we remove the last selection - if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)) - SelectionComponent({key: selection.id, selection, editor, lineHeight}) + selectionComponents = [] + for selectionId, screenRange of @selectionRanges + selectionComponents.push(SelectionComponent({key: selectionId, screenRange, editor, lineHeight})) + selectionComponents + + componentWillMount: -> + @selectionRanges = {} + + shouldComponentUpdate: -> + {editor} = @props + oldSelectionRanges = @selectionRanges + newSelectionRanges = {} + @selectionRanges = newSelectionRanges + + for selection, index in editor.getSelections() + # Rendering artifacts occur on the lines GPU layer if we remove the last selection + if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)) + newSelectionRanges[selection.id] = selection.getScreenRange() + + for id, range of newSelectionRanges + if oldSelectionRanges.hasOwnProperty(id) + return true unless range.isEqual(oldSelectionRanges[id]) + else + return true + + for id of oldSelectionRanges + return true unless newSelectionRanges.hasOwnProperty(id) + + false From bc8a1756f3ae8b2b399f5fcf8d90e26ae65228ca Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 14:49:28 -0600 Subject: [PATCH 42/59] Use the .selections layer as the underlayer --- src/react-editor-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index 9b91eb384..b9766f099 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -41,7 +41,7 @@ class ReactEditorView extends View node = @component.getDOMNode() - @underlayer = $(node).find('.underlayer') + @underlayer = $(node).find('.selections') @gutter = $(node).find('.gutter') @gutter.removeClassFromAllLines = (klass) => From c5fa2bf12dddfe48ed3d3c24b9dff94ef6bbde22 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 14:53:14 -0600 Subject: [PATCH 43/59] Attach views to .lines instead of defunct .scroll-view-content --- src/react-editor-view.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index b9766f099..e2f3fd3df 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -64,7 +64,8 @@ class ReactEditorView extends View appendToLinesView: (view) -> view.css('position', 'absolute') - @find('.scroll-view-content').prepend(view) + view.css('z-index', 1) + @find('.lines').prepend(view) beforeRemove: -> React.unmountComponentAtNode(@element) From 54cec0a5ff8af4e97d0d7d0403ede1dfea02ef5a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 17:11:54 -0600 Subject: [PATCH 44/59] Hold the gutter's width with a dummy line number --- src/gutter-component.coffee | 39 +++++++++++++++++++++++++++---------- static/editor.less | 1 - 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index a099936e3..07cd36a75 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -13,9 +13,9 @@ GutterComponent = React.createClass lastMeasuredWidth: null render: -> - {width, scrollHeight, scrollTop} = @props + {scrollHeight, scrollTop} = @props - div className: 'gutter', style: {width}, + div className: 'gutter', div className: 'line-numbers', ref: 'lineNumbers', style: height: scrollHeight WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)" @@ -24,6 +24,9 @@ GutterComponent = React.createClass @lineNumberNodesById = {} @lineNumberNodeTopPositions = {} + componentDidMount: -> + @appendDummyLineNumber() + # 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 # visible row range. @@ -37,21 +40,31 @@ GutterComponent = React.createClass false componentDidUpdate: (oldProps) -> + @updateDummyLineNumber() if oldProps.maxLineNumberDigits isnt @props.maxLineNumberDigits + @measureWidth() unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') @updateLineNumbers() - unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') - @measureWidth() + + # This dummy line number element holds the gutter to the appropriate width, + # since the real line numbers are absolutely positioned for performance reasons. + appendDummyLineNumber: -> + {maxLineNumberDigits} = @props + WrapperDiv.innerHTML = @buildLineNumberHTML(0, false, maxLineNumberDigits) + @dummyLineNumberNode = WrapperDiv.children[0] + @refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode) + + updateDummyLineNumber: -> + WrapperDiv.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits) updateLineNumbers: -> visibleLineNumberIds = @appendOrUpdateVisibleLineNumberNodes() @removeNonVisibleLineNumberNodes(visibleLineNumberIds) appendOrUpdateVisibleLineNumberNodes: -> - {editor, visibleRowRange, scrollTop, lineHeight} = @props + {editor, visibleRowRange, scrollTop, lineHeight, maxLineNumberDigits} = @props [startRow, endRow] = visibleRowRange startRow = Math.max(0, startRow - 8) endRow = Math.min(editor.getLineCount(), endRow + 8) - maxLineNumberDigits = editor.getLineCount().toString().length newLineNumberIds = null newLineNumbersHTML = null visibleLineNumberIds = new Set @@ -99,6 +112,15 @@ GutterComponent = React.createClass node.removeChild(lineNumberNode) buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> + innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) + if top? + style = "position: absolute; top: #{top}px;" + else + style = "visibility: hidden;" + + "
#{innerHTML}
" + + buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> if softWrapped lineNumber = "•" else @@ -106,10 +128,7 @@ GutterComponent = React.createClass padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length) iconHTML = '
' - innerHTML = padding + lineNumber + iconHTML - translate3d = @buildTranslate3d(top) - - "
#{innerHTML}
" + padding + lineNumber + iconHTML updateLineNumberNode: (lineNumberId, top) -> unless @lineNumberNodeTopPositions[lineNumberId] is top diff --git a/static/editor.less b/static/editor.less index 4bd80a1e6..7784299bb 100644 --- a/static/editor.less +++ b/static/editor.less @@ -67,7 +67,6 @@ .gutter { .line-number { - position: absolute; white-space: nowrap; padding: 0 .5em; From 03341776968c5112597f7780fbf8eeb7639668d9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 17:36:35 -0600 Subject: [PATCH 45/59] Make lineOverdrawMargin a property --- src/editor-component.coffee | 11 ++++++----- src/editor-scroll-view-component.coffee | 4 ++-- src/gutter-component.coffee | 6 +++--- src/lines-component.coffee | 7 ++++--- src/react-editor-view.coffee | 6 ++++-- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 353ad6281..9aa964247 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -31,7 +31,7 @@ EditorComponent = React.createClass render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state - {editor, cursorBlinkResumeDelay} = @props + {editor, cursorBlinkResumeDelay, lineOverdrawMargin} = @props maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() @@ -51,14 +51,14 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - editor, visibleRowRange, maxLineNumberDigits, scrollTop, scrollHeight, - lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, - width: @gutterWidth, onWidthChanged: @onGutterWidthChanged + editor, visibleRowRange, lineOverdrawMargin, maxLineNumberDigits, scrollTop, + scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, + @pendingChanges, onWidthChanged: @onGutterWidthChanged } EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, - lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, + lineHeight: lineHeightInPixels, visibleRowRange, lineOverdrawMargin, @pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred @@ -100,6 +100,7 @@ EditorComponent = React.createClass getDefaultProps: -> cursorBlinkResumeDelay: 100 + lineOverdrawMargin: 8 componentWillMount: -> @pendingChanges = [] diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 2985910b9..72cc7d34a 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props + {visibleRowRange, lineOverdrawMargin, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props {selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -36,7 +36,7 @@ EditorScrollViewComponent = React.createClass CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + visibleRowRange, lineOverdrawMargin, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged, scrollHeight, scrollWidth } diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 07cd36a75..669d6e6a2 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -60,10 +60,10 @@ GutterComponent = React.createClass @removeNonVisibleLineNumberNodes(visibleLineNumberIds) appendOrUpdateVisibleLineNumberNodes: -> - {editor, visibleRowRange, scrollTop, lineHeight, maxLineNumberDigits} = @props + {editor, visibleRowRange, scrollTop, lineHeight, maxLineNumberDigits, lineOverdrawMargin} = @props [startRow, endRow] = visibleRowRange - startRow = Math.max(0, startRow - 8) - endRow = Math.min(editor.getLineCount(), endRow + 8) + startRow = Math.max(0, startRow - lineOverdrawMargin) + endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) newLineNumberIds = null newLineNumbersHTML = null diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 89d992db3..ac052ae78 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -49,10 +49,11 @@ LinesComponent = React.createClass @measureCharactersInNewLines() unless @props.scrollingVertically updateLines: -> - {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props + {editor, visibleRowRange, showIndentGuide, selectionChanged, lineOverdrawMargin} = @props [startRow, endRow] = visibleRowRange - startRow = Math.max(0, startRow - 8) - endRow = Math.min(editor.getLineCount(), endRow + 8) + + startRow = Math.max(0, startRow - lineOverdrawMargin) + endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) visibleLines = editor.linesForScreenRows(startRow, endRow - 1) @removeNonVisibleLineNodes(visibleLines) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index e2f3fd3df..87e5d0693 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -1,6 +1,7 @@ {View, $} = require 'space-pen' React = require 'react' EditorComponent = require './editor-component' +{defaults} = require 'underscore-plus' module.exports = class ReactEditorView extends View @@ -8,7 +9,7 @@ class ReactEditorView extends View focusOnAttach: false - constructor: (@editor) -> + constructor: (@editor, @props) -> super getEditor: -> @editor @@ -37,7 +38,8 @@ class ReactEditorView extends View afterAttach: (onDom) -> return unless onDom @attached = true - @component = React.renderComponent(EditorComponent({@editor, parentView: this}), @element) + props = defaults({@editor, parentView: this}, @props) + @component = React.renderComponent(EditorComponent(props), @element) node = @component.getDOMNode() From 6017b73acfc86ceeeac4214d98d6421aaf8d3d23 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 10:34:57 -0600 Subject: [PATCH 46/59] Add ability to look up line nodes by screen row --- src/editor-component.coffee | 2 ++ src/editor-scroll-view-component.coffee | 2 ++ src/lines-component.coffee | 43 +++++++++++++++++-------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 9aa964247..ba2fc1f58 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -422,3 +422,5 @@ EditorComponent = React.createClass consolidateSelections: (e) -> e.abortKeyBinding() unless @props.editor.consolidateSelections() + + lineNodeForScreenRow: (screenRow) -> @refs.scrollView.lineNodeForScreenRow(screenRow) diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 72cc7d34a..fdccc23cb 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -191,3 +191,5 @@ EditorScrollViewComponent = React.createClass focus: -> @refs.input.focus() + + lineNodeForScreenRow: (screenRow) -> @refs.lines.lineNodeForScreenRow(screenRow) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index ac052ae78..1078a3b3b 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -27,7 +27,8 @@ LinesComponent = React.createClass componentWillMount: -> @measuredLines = new WeakSet @lineNodesByLineId = {} - @lineNodeTopPositions = {} + @screenRowsByLineId = {} + @lineIdsByScreenRow = {} componentDidMount: -> @measureLineHeightAndCharWidth() @@ -43,11 +44,18 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> + unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') + @clearScreenRowCaches() + @measureLineHeightAndCharWidth() + @updateLines() - @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically + clearScreenRowCaches: -> + @screenRowsByLineId = {} + @lineIdsByScreenRow = {} + updateLines: -> {editor, visibleRowRange, showIndentGuide, selectionChanged, lineOverdrawMargin} = @props [startRow, endRow] = visibleRowRange @@ -65,7 +73,9 @@ LinesComponent = React.createClass node = @getDOMNode() for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId) delete @lineNodesByLineId[lineId] - delete @lineNodeTopPositions[lineId] + screenRow = @screenRowsByLineId[lineId] + delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId + delete @screenRowsByLineId[lineId] node.removeChild(lineNode) appendOrUpdateVisibleLineNodes: (visibleLines, startRow) -> @@ -75,16 +85,16 @@ LinesComponent = React.createClass for line, index in visibleLines screenRow = startRow + index - top = (screenRow * lineHeight) if @hasLineNode(line.id) - @updateLineNode(line, top) + @updateLineNode(line, screenRow) else newLines ?= [] newLinesHTML ?= "" newLines.push(line) - newLinesHTML += @buildLineHTML(line, top) - @lineNodeTopPositions[line.id] = top + newLinesHTML += @buildLineHTML(line, screenRow) + @screenRowsByLineId[line.id] = screenRow + @lineIdsByScreenRow[screenRow] = line.id return unless newLines? @@ -102,10 +112,10 @@ LinesComponent = React.createClass buildTranslate3d: (top) -> "translate3d(0px, #{top}px, 0px)" - buildLineHTML: (line, top, left) -> - {editor, mini, showIndentGuide} = @props + buildLineHTML: (line, screenRow) -> + {editor, mini, showIndentGuide, lineHeight} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line - translate3d = @buildTranslate3d(top, left) + top = screenRow * lineHeight lineHTML = "
" if text is "" @@ -156,11 +166,16 @@ LinesComponent = React.createClass scopeStack.push(scope) "" - updateLineNode: (line, top) -> - unless @lineNodeTopPositions[line.id] is top + updateLineNode: (line, screenRow) -> + unless @screenRowsByLineId[line.id] is screenRow + {lineHeight} = @props lineNode = @lineNodesByLineId[line.id] - lineNode.style.top = top + 'px' - @lineNodeTopPositions[line.id] = top + lineNode.style.top = screenRow * lineHeight + 'px' + @screenRowsByLineId[line.id] = screenRow + @lineIdsByScreenRow[screenRow] = line.id + + lineNodeForScreenRow: (screenRow) -> + @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] measureLineHeightAndCharWidth: -> node = @getDOMNode() From 0ad27303531cc0bfd8e957ab62cb58fb64e303f2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 10:44:49 -0600 Subject: [PATCH 47/59] Update specs for new line node rendering approach Lines are no longer translated on the GPU, and they aren't inserted into the DOM in an order that reflects their order in the buffer. --- spec/editor-component-spec.coffee | 73 +++++++++++++++---------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index a81a1dc68..9516317d6 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -4,9 +4,11 @@ nbsp = String.fromCharCode(160) describe "EditorComponent", -> [contentNode, editor, wrapperView, component, node, verticalScrollbarNode, horizontalScrollbarNode] = [] - [lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame] = [] + [lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame, lineOverdrawMargin] = [] beforeEach -> + lineOverdrawMargin = 2 + waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -29,7 +31,7 @@ describe "EditorComponent", -> contentNode = document.querySelector('#jasmine-content') contentNode.style.width = '1000px' - wrapperView = new ReactEditorView(editor) + wrapperView = new ReactEditorView(editor, {lineOverdrawMargin}) wrapperView.attachToDom() {component} = wrapperView component.setLineHeight(1.3) @@ -49,52 +51,54 @@ describe "EditorComponent", -> contentNode.style.width = '' describe "line rendering", -> - it "renders only the currently-visible lines, translated relative to the scroll position", -> + it "renders the currently-visible lines plus the overdraw margin", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() - lines = node.querySelectorAll('.line') - expect(lines.length).toBe 6 - expect(lines[0].textContent).toBe editor.lineForScreenRow(0).text - expect(lines[5].textContent).toBe editor.lineForScreenRow(5).text + linesNode = node.querySelector('.lines') + expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(node.querySelectorAll('.line').length).toBe 6 + 2 # no margin above + expect(component.lineNodeForScreenRow(0).textContent).toBe editor.lineForScreenRow(0).text + expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 + expect(component.lineNodeForScreenRow(5).textContent).toBe editor.lineForScreenRow(5).text + expect(component.lineNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels - verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - lineNodes = node.querySelectorAll('.line') - expect(lineNodes.length).toBe 6 - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{-.5 * lineHeightInPixels}px, 0px)" - expect(lineNodes[0].textContent).toBe editor.lineForScreenRow(2).text - expect(lineNodes[5].textContent).toBe editor.lineForScreenRow(7).text - expect(lineNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{4.5 * lineHeightInPixels}px, 0px)" + expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, #{-4.5 * lineHeightInPixels}px, 0px)" + expect(node.querySelectorAll('.line').length).toBe 6 + 4 # margin above and below + expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNodeForScreenRow(2).textContent).toBe editor.lineForScreenRow(2).text + expect(component.lineNodeForScreenRow(9).offsetTop).toBe 9 * lineHeightInPixels + expect(component.lineNodeForScreenRow(9).textContent).toBe editor.lineForScreenRow(9).text - it "updates the translation of subsequent lines when lines are inserted or removed", -> + it "updates the top position of subsequent lines when lines are inserted or removed", -> editor.getBuffer().deleteRows(0, 1) lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" - expect(lineNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" + expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 + expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels editor.getBuffer().insert([0, 0], '\n\n') lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" - expect(lineNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" - expect(lineNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" - expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" + expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels + expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels + expect(component.lineNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels describe "when indent guides are enabled", -> beforeEach -> component.setShowIndentGuide(true) it "adds an 'indent-guide' class to spans comprising the leading whitespace", -> - lines = node.querySelectorAll('.line') - line1LeafNodes = getLeafNodes(lines[1]) + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe ' ' expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - line2LeafNodes = getLeafNodes(lines[2]) + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line2LeafNodes[0].textContent).toBe ' ' expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line2LeafNodes[1].textContent).toBe ' ' @@ -502,14 +506,12 @@ describe "EditorComponent", -> node.style.width = 30 * charWidth + 'px' component.measureHeightAndWidth() - lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" + linesNode = node.querySelector('.lines') + expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 0 editor.setScrollLeft(100) - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" - expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(-100px, #{4 * lineHeightInPixels}px, 0px)" + expect(linesNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> @@ -527,7 +529,7 @@ describe "EditorComponent", -> node.style.width = 10 * charWidth + 'px' component.measureHeightAndWidth() editor.setScrollBottom(editor.getScrollHeight()) - lastLineNode = last(node.querySelectorAll('.line')) + lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top expect(bottomOfLastLine).toBe topOfHorizontalScrollbar @@ -535,7 +537,6 @@ describe "EditorComponent", -> # Scroll so there's no space below the last line when the horizontal scrollbar disappears node.style.width = 100 * charWidth + 'px' component.measureHeightAndWidth() - lastLineNode = last(node.querySelectorAll('.line')) bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom bottomOfEditor = node.getBoundingClientRect().bottom expect(bottomOfLastLine).toBe bottomOfEditor @@ -547,11 +548,9 @@ describe "EditorComponent", -> editor.setScrollLeft(Infinity) - lineNodes = node.querySelectorAll('.line') - rightOfLongestLine = lineNodes[6].getBoundingClientRect().right + rightOfLongestLine = component.lineNodeForScreenRow(6).getBoundingClientRect().right leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left - - expect(rightOfLongestLine).toBe leftOfVerticalScrollbar - 1 # Leave 1 px so the cursor is visible on the end of the line + expect(Math.round(rightOfLongestLine)).toBe leftOfVerticalScrollbar - 1 # Leave 1 px so the cursor is visible on the end of the line it "only displays dummy scrollbars when scrollable in that direction", -> expect(verticalScrollbarNode.style.display).toBe 'none' From 64c82f1c876d8b04094e7e3ba9cc6bf7d8fd649b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 11:00:05 -0600 Subject: [PATCH 48/59] Update cursor positioning text for simplified token markup --- spec/editor-component-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 9516317d6..f7d88a07f 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -262,7 +262,7 @@ describe "EditorComponent", -> cursor = node.querySelector('.cursor') cursorRect = cursor.getBoundingClientRect() - cursorLocationTextNode = node.querySelector('.storage.type.function.js').firstChild.firstChild + cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild range = document.createRange() range.setStart(cursorLocationTextNode, 0) range.setEnd(cursorLocationTextNode, 1) From b000e8e4a28401c5586c7cb2a6189e16b3e25420 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 11:10:39 -0600 Subject: [PATCH 49/59] Get selection specs passing again --- spec/editor-component-spec.coffee | 91 +++++++++++++++++-------------- static/editor.less | 4 ++ 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index f7d88a07f..2f0786701 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -320,55 +320,64 @@ describe "EditorComponent", -> scrollViewNode = node.querySelector('.scroll-view') scrollViewClientLeft = node.querySelector('.scroll-view').getBoundingClientRect().left - describe "for single line selections", -> - it "renders 1 region on the line and no background region", -> - # 1-line selection - editor.setSelectedScreenRange([[1, 6], [1, 10]]) - lineNodes = node.querySelectorAll('.line') - line1Region = lineNodes[1].querySelector('.selection .region') - regionRect = line1Region.getBoundingClientRect() - expect(regionRect.top).toBe 1 * lineHeightInPixels - expect(regionRect.height).toBe 1 * lineHeightInPixels - expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(regionRect.width).toBe 4 * charWidth + it "renders 1 region for 1-line selections", -> + # 1-line selection + editor.setSelectedScreenRange([[1, 6], [1, 10]]) + regions = node.querySelectorAll('.selection .region') - expect(node.querySelectorAll('.underlayer .selection .region').length).toBe 0 + expect(regions.length).toBe 1 + regionRect = regions[0].getBoundingClientRect() + expect(regionRect.top).toBe 1 * lineHeightInPixels + expect(regionRect.height).toBe 1 * lineHeightInPixels + expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(regionRect.width).toBe 4 * charWidth - describe "for multi-line selections", -> - it "renders a region on each line and a full-width background region from the first line to the penultimate line", -> - editor.setSelectedScreenRange([[1, 6], [3, 10]]) + it "renders 2 regions for 2-line selections", -> + editor.setSelectedScreenRange([[1, 6], [2, 10]]) + regions = node.querySelectorAll('.selection .region') + expect(regions.length).toBe 2 - lineNodes = node.querySelectorAll('.line') - region1Rect = lineNodes[1].querySelector('.selection .region').getBoundingClientRect() - expect(region1Rect.top).toBe 1 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(region1Rect.right).toBe scrollViewClientLeft + lineNodes[1].offsetWidth + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe 1 * lineHeightInPixels + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(region1Rect.right).toBe scrollViewNode.getBoundingClientRect().right - region2Rect = lineNodes[2].querySelector('.selection .region').getBoundingClientRect() - expect(region2Rect.top).toBe 2 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft - expect(region2Rect.width).toBe lineNodes[2].offsetWidth + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe 2 * lineHeightInPixels + expect(region2Rect.height).toBe 1 * lineHeightInPixels + expect(region2Rect.left).toBe scrollViewClientLeft + 0 + expect(region2Rect.width).toBe 10 * charWidth - region3Rect = lineNodes[3].querySelector('.selection .region').getBoundingClientRect() - expect(region3Rect.top).toBe 3 * lineHeightInPixels - expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBe scrollViewClientLeft + 0 - expect(region3Rect.width).toBe 10 * charWidth + it "renders 3 regions for selections with more than 2 lines", -> + editor.setSelectedScreenRange([[1, 6], [5, 10]]) + regions = node.querySelectorAll('.selection .region') + expect(regions.length).toBe 3 - backgroundNodes = node.querySelectorAll('.underlayer .selection .region') - expect(backgroundNodes.length).toBe 1 - backgroundRegionRect = backgroundNodes[0].getBoundingClientRect() + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe 1 * lineHeightInPixels + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(region1Rect.right).toBe scrollViewNode.getBoundingClientRect().right - expect(backgroundRegionRect.top).toBe 1 * lineHeightInPixels - expect(backgroundRegionRect.left).toBe scrollViewClientLeft - expect(backgroundRegionRect.width).toBe scrollViewNode.offsetWidth - expect(backgroundRegionRect.height).toBe 2 * lineHeightInPixels + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe 2 * lineHeightInPixels + expect(region2Rect.height).toBe 3 * lineHeightInPixels + expect(region2Rect.left).toBe scrollViewClientLeft + 0 + expect(region2Rect.right).toBe scrollViewNode.getBoundingClientRect().right - it "does not render empty selections", -> - expect(editor.getSelection().isEmpty()).toBe true - expect(node.querySelectorAll('.selection').length).toBe 0 + region3Rect = regions[2].getBoundingClientRect() + expect(region3Rect.top).toBe 5 * lineHeightInPixels + expect(region3Rect.height).toBe 1 * lineHeightInPixels + expect(region3Rect.left).toBe scrollViewClientLeft + 0 + expect(region3Rect.width).toBe 10 * charWidth + + it "does not render empty selections unless they are the first selection (to prevent a Chromium rendering artifact caused by removing it)", -> + editor.addSelectionForBufferRange([[2, 2], [2, 2]]) + expect(editor.getSelection(0).isEmpty()).toBe true + expect(editor.getSelection(1).isEmpty()).toBe true + + expect(node.querySelectorAll('.selection').length).toBe 1 describe "mouse interactions", -> linesNode = null diff --git a/static/editor.less b/static/editor.less index 7784299bb..fcd7a2575 100644 --- a/static/editor.less +++ b/static/editor.less @@ -12,6 +12,10 @@ z-index: -2; } + .lines { + min-width: 100%; + } + .cursor { z-index: 1; } From fe82e3e30f2befc9975598488d0fb248313baae7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 14:52:12 -0600 Subject: [PATCH 50/59] Only clear screen row caches on lines component if lineHeight changes --- src/lines-component.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 1078a3b3b..384c7257e 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -44,10 +44,8 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> - unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') - @clearScreenRowCaches() - @measureLineHeightAndCharWidth() - + @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') + @clearScreenRowCaches() unless prevProps.lineHeight is @props.lineHeight @updateLines() @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically From e74dfe343804722954ea8939928047ee35d3aa54 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 14:52:24 -0600 Subject: [PATCH 51/59] Fix gutter specs and update lines when digit counts change --- spec/editor-component-spec.coffee | 74 ++++++++++++++----------------- src/editor-component.coffee | 6 ++- src/gutter-component.coffee | 65 ++++++++++++++++----------- 3 files changed, 78 insertions(+), 67 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 2f0786701..facdabd32 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -151,41 +151,39 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() - lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes.length).toBe 6 - expect(lineNumberNodes[0].textContent).toBe "#{nbsp}1" - expect(lineNumberNodes[5].textContent).toBe "#{nbsp}6" + expect(node.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number + expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" + expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}6" verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes.length).toBe 6 + expect(node.querySelectorAll('.line-number').length).toBe 6 + 4 + 1 # line overdraw margin above/below + dummy line number - expect(lineNumberNodes[0].textContent).toBe "#{nbsp}3" - expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{-.5 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[5].textContent).toBe "#{nbsp}8" - expect(lineNumberNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{4.5 * lineHeightInPixels}px, 0px)" + expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}3" + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + return + expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}8" + expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 7 * lineHeightInPixels it "updates the translation of subsequent line numbers when lines are inserted or removed", -> editor.getBuffer().insert([0, 0], '\n\n') lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNumberNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 + expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels editor.getBuffer().insert([0, 0], '\n\n') - lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNumberNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{5 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[6].style['-webkit-transform']).toBe "translate3d(0px, #{6 * lineHeightInPixels}px, 0px)" + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 + expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 6 * lineHeightInPixels it "renders • characters for soft-wrapped lines", -> editor.setSoftWrap(true) @@ -193,28 +191,24 @@ describe "EditorComponent", -> node.style.width = 30 * charWidth + 'px' component.measureHeightAndWidth() - lines = node.querySelectorAll('.line-number') - expect(lines.length).toBe 6 - expect(lines[0].textContent).toBe "#{nbsp}1" - expect(lines[1].textContent).toBe "#{nbsp}•" - expect(lines[2].textContent).toBe "#{nbsp}2" - expect(lines[3].textContent).toBe "#{nbsp}•" - expect(lines[4].textContent).toBe "#{nbsp}3" - expect(lines[5].textContent).toBe "#{nbsp}•" + expect(node.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line node + expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" + expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}•" + expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2" + expect(component.lineNumberNodeForScreenRow(3).textContent).toBe "#{nbsp}•" + expect(component.lineNumberNodeForScreenRow(4).textContent).toBe "#{nbsp}3" + expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}•" - it "pads line numbers to be right justified based on the maximum number of line number digits", -> + it "pads line numbers to be right-justified based on the maximum number of line number digits", -> editor.getBuffer().setText([1..10].join('\n')) - lineNumberNodes = toArray(node.querySelectorAll('.line-number')) - - for node, i in lineNumberNodes[0..8] - expect(node.textContent).toBe "#{nbsp}#{i + 1}" - expect(lineNumberNodes[9].textContent).toBe '10' + for screenRow in [0..8] + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" + expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" # Removes padding when the max number of digits goes down editor.getBuffer().delete([[1, 0], [2, 0]]) - lineNumberNodes = toArray(node.querySelectorAll('.line-number')) - for node, i in lineNumberNodes - expect(node.textContent).toBe "#{i + 1}" + for screenRow in [0..8] + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{screenRow + 1}" describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index ba2fc1f58..13e3afc44 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -51,8 +51,8 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - editor, visibleRowRange, lineOverdrawMargin, maxLineNumberDigits, scrollTop, - scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, + ref: 'gutter', editor, visibleRowRange, lineOverdrawMargin, maxLineNumberDigits, + scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, onWidthChanged: @onGutterWidthChanged } @@ -424,3 +424,5 @@ EditorComponent = React.createClass e.abortKeyBinding() unless @props.editor.consolidateSelections() lineNodeForScreenRow: (screenRow) -> @refs.scrollView.lineNodeForScreenRow(screenRow) + + lineNumberNodeForScreenRow: (screenRow) -> @refs.gutter.lineNumberNodeForScreenRow(screenRow) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 669d6e6a2..593d8a411 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -22,7 +22,8 @@ GutterComponent = React.createClass componentWillMount: -> @lineNumberNodesById = {} - @lineNumberNodeTopPositions = {} + @lineNumberIdsByScreenRow = {} + @screenRowsByLineNumberId = {} componentDidMount: -> @appendDummyLineNumber() @@ -40,10 +41,18 @@ GutterComponent = React.createClass false componentDidUpdate: (oldProps) -> - @updateDummyLineNumber() if oldProps.maxLineNumberDigits isnt @props.maxLineNumberDigits + unless oldProps.maxLineNumberDigits is @props.maxLineNumberDigits + @updateDummyLineNumber() + @removeLineNumberNodes() + @measureWidth() unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') + @clearScreenRowCaches() unless oldProps.lineHeight is @props.lineHeight @updateLineNumbers() + clearScreenRowCaches: -> + @lineNumberIdsByScreenRow = {} + @screenRowsByLineNumberId = {} + # This dummy line number element holds the gutter to the appropriate width, # since the real line numbers are absolutely positioned for performance reasons. appendDummyLineNumber: -> @@ -56,11 +65,11 @@ GutterComponent = React.createClass WrapperDiv.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits) updateLineNumbers: -> - visibleLineNumberIds = @appendOrUpdateVisibleLineNumberNodes() - @removeNonVisibleLineNumberNodes(visibleLineNumberIds) + lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes() + @removeLineNumberNodes(lineNumberIdsToPreserve) appendOrUpdateVisibleLineNumberNodes: -> - {editor, visibleRowRange, scrollTop, lineHeight, maxLineNumberDigits, lineOverdrawMargin} = @props + {editor, visibleRowRange, scrollTop, maxLineNumberDigits, lineOverdrawMargin} = @props [startRow, endRow] = visibleRowRange startRow = Math.max(0, startRow - lineOverdrawMargin) endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) @@ -71,6 +80,8 @@ GutterComponent = React.createClass wrapCount = 0 for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) + screenRow = startRow + index + if bufferRow is lastBufferRow id = "#{bufferRow}-#{wrapCount++}" else @@ -80,17 +91,16 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) - screenRow = startRow + index - top = screenRow * lineHeight if @hasLineNumberNode(id) - @updateLineNumberNode(id, top) + @updateLineNumberNode(id, screenRow) else newLineNumberIds ?= [] newLineNumbersHTML ?= "" newLineNumberIds.push(id) - newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, top) - @lineNumberNodeTopPositions[id] = top + newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow) + @screenRowsByLineNumberId[id] = screenRow + @lineNumberIdsByScreenRow[screenRow] = id if newLineNumberIds? WrapperDiv.innerHTML = newLineNumbersHTML @@ -104,23 +114,26 @@ GutterComponent = React.createClass visibleLineNumberIds - removeNonVisibleLineNumberNodes: (visibleLineNumberIds) -> + removeLineNumberNodes: (lineNumberIdsToPreserve) -> node = @refs.lineNumbers.getDOMNode() - for id, lineNumberNode of @lineNumberNodesById when not visibleLineNumberIds.has(id) - delete @lineNumberNodesById[id] - delete @lineNumberNodeTopPositions[id] + for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId) + delete @lineNumberNodesById[lineNumberId] + screenRow = @screenRowsByLineNumberId[lineNumberId] + delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId + delete @screenRowsByLineNumberId[lineNumberId] node.removeChild(lineNumberNode) - buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> - innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - if top? - style = "position: absolute; top: #{top}px;" + buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) -> + if screenRow? + {lineHeight} = @props + style = "position: absolute; top: #{screenRow * lineHeight}px;" else style = "visibility: hidden;" + innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) "
#{innerHTML}
" - buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> + buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped lineNumber = "•" else @@ -130,16 +143,18 @@ GutterComponent = React.createClass iconHTML = '
' padding + lineNumber + iconHTML - updateLineNumberNode: (lineNumberId, top) -> - unless @lineNumberNodeTopPositions[lineNumberId] is top - @lineNumberNodesById[lineNumberId].style.top = top + 'px' - @lineNumberNodeTopPositions[lineNumberId] = top + updateLineNumberNode: (lineNumberId, screenRow) -> + unless @screenRowsByLineNumberId[lineNumberId] is screenRow + {lineHeight} = @props + @lineNumberNodesById[lineNumberId].style.top = screenRow * lineHeight + 'px' + @screenRowsByLineNumberId[lineNumberId] = screenRow + @lineNumberIdsByScreenRow[screenRow] = lineNumberId hasLineNumberNode: (lineNumberId) -> @lineNumberNodesById.hasOwnProperty(lineNumberId) - buildTranslate3d: (top) -> - "translate3d(0px, #{top}px, 0px)" + lineNumberNodeForScreenRow: (screenRow) -> + @lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]] measureWidth: -> lineNumberNode = @refs.lineNumbers.getDOMNode().firstChild From 9b7547cbe0342ae4c0e5fd591f7f7389017df0d6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 15:21:12 -0600 Subject: [PATCH 52/59] Get indent guide specs passing again --- spec/editor-component-spec.coffee | 13 +++++-------- src/lines-component.coffee | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index facdabd32..bcf1402f5 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -98,7 +98,7 @@ describe "EditorComponent", -> expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes[0].textContent).toBe ' ' expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line2LeafNodes[1].textContent).toBe ' ' @@ -108,8 +108,7 @@ describe "EditorComponent", -> it "renders leading whitespace spans with the 'indent-guide' class for empty lines", -> editor.getBuffer().insert([1, Infinity], '\n') - lines = node.querySelectorAll('.line') - line2LeafNodes = getLeafNodes(lines[2]) + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe 3 expect(line2LeafNodes[0].textContent).toBe ' ' @@ -121,8 +120,7 @@ describe "EditorComponent", -> it "renders indent guides correctly on lines containing only whitespace", -> editor.getBuffer().insert([1, Infinity], '\n ') - lines = node.querySelectorAll('.line') - line2LeafNodes = getLeafNodes(lines[2]) + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe 3 expect(line2LeafNodes[0].textContent).toBe ' ' expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true @@ -132,9 +130,8 @@ describe "EditorComponent", -> expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true it "does not render indent guides in trailing whitespace for lines containing non whitespace characters", -> - editor.getBuffer().setText (" hi ") - lines = node.querySelectorAll('.line') - line0LeafNodes = getLeafNodes(lines[0]) + editor.getBuffer().setText " hi " + line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(line0LeafNodes[0].textContent).toBe ' ' expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line0LeafNodes[1].textContent).toBe ' ' diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 384c7257e..e9c6bb465 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -46,6 +46,7 @@ LinesComponent = React.createClass componentDidUpdate: (prevProps) -> @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScreenRowCaches() unless prevProps.lineHeight is @props.lineHeight + @removeLineNodes() unless prevProps.showIndentGuide is @props.showIndentGuide @updateLines() @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically @@ -62,10 +63,10 @@ LinesComponent = React.createClass endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) visibleLines = editor.linesForScreenRows(startRow, endRow - 1) - @removeNonVisibleLineNodes(visibleLines) + @removeLineNodes(visibleLines) @appendOrUpdateVisibleLineNodes(visibleLines, startRow) - removeNonVisibleLineNodes: (visibleLines) -> + removeLineNodes: (visibleLines=[]) -> visibleLineIds = new Set visibleLineIds.add(line.id.toString()) for line in visibleLines node = @getDOMNode() @@ -117,13 +118,23 @@ LinesComponent = React.createClass lineHTML = "
" if text is "" - lineHTML += " " + lineHTML += @buildEmptyLineInnerHTML(line) else lineHTML += @buildLineInnerHTML(line) lineHTML += "
" lineHTML + buildEmptyLineInnerHTML: (line) -> + {showIndentGuide} = @props + {indentLevel, tabLength} = line + + if showIndentGuide and indentLevel > 0 + indentSpan = "#{multiplyString(' ', tabLength)}" + multiplyString(indentSpan, indentLevel + 1) + else + " " + buildLineInnerHTML: (line) -> {invisibles, mini, showIndentGuide} = @props {tokens, text} = line From 57e6419d1d45038ff5bdb2d03b5773e6de1384f5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 15:30:50 -0600 Subject: [PATCH 53/59] Restore conditional loading of react editor renderer --- src/editor.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index bc8b72759..3dc51555d 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -214,10 +214,10 @@ class Editor extends Model @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... getViewClass: -> - # if atom.config.get('core.useReactEditor') - require './react-editor-view' - # else - # require './editor-view' + if atom.config.get('core.useReactEditor') + require './react-editor-view' + else + require './editor-view' destroyed: -> @unsubscribe() From c05848342237cb6d355bde7eef205e75c75d81c1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 15:56:18 -0600 Subject: [PATCH 54/59] Update the gutter width when the number of digits changes --- spec/editor-component-spec.coffee | 11 +++++++++++ src/gutter-component.coffee | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index bcf1402f5..d5f04e65f 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -202,10 +202,21 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" + gutterNode = node.querySelector('.gutter') + initialGutterWidth = gutterNode.offsetWidth + # Removes padding when the max number of digits goes down editor.getBuffer().delete([[1, 0], [2, 0]]) for screenRow in [0..8] expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{screenRow + 1}" + expect(gutterNode.offsetWidth).toBeLessThan initialGutterWidth + + # Increases padding when the max number of digits goes up + editor.getBuffer().insert([0, 0], '\n\n') + for screenRow in [0..8] + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" + expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" + expect(gutterNode.offsetWidth).toBe initialGutterWidth describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 593d8a411..a758a4607 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -11,6 +11,7 @@ GutterComponent = React.createClass mixins: [SubscriberMixin] lastMeasuredWidth: null + dummyLineNumberNode: null render: -> {scrollHeight, scrollTop} = @props @@ -62,7 +63,7 @@ GutterComponent = React.createClass @refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode) updateDummyLineNumber: -> - WrapperDiv.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits) + @dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits) updateLineNumbers: -> lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes() From 626964f15b8f0902f52b5492d2e5d6ebd8b743d2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 16:07:35 -0600 Subject: [PATCH 55/59] Upgrade go-to-line to fix double toggle on react editor --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c06b5083f..6319ee27f 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "find-and-replace": "0.105.0", "fuzzy-finder": "0.51.0", "git-diff": "0.28.0", - "go-to-line": "0.20.0", + "go-to-line": "0.21.0", "grammar-selector": "0.26.0", "image-view": "0.33.0", "keybinding-resolver": "0.17.0", From 8e65d30a845cc7c49ed4855d40ff9e4446a392ac Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 20:58:40 -0600 Subject: [PATCH 56/59] Compute rendered row range once in EditorComponent and pass it down --- src/editor-component.coffee | 15 +++++++++++---- src/editor-scroll-view-component.coffee | 4 ++-- src/gutter-component.coffee | 13 +++++-------- src/lines-component.coffee | 15 ++++++--------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 13e3afc44..463b47c5e 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -31,11 +31,11 @@ EditorComponent = React.createClass render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state - {editor, cursorBlinkResumeDelay, lineOverdrawMargin} = @props + {editor, cursorBlinkResumeDelay} = @props maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() - visibleRowRange = editor.getVisibleRowRange() + renderedRowRange = @getRenderedRowRange() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -51,14 +51,14 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - ref: 'gutter', editor, visibleRowRange, lineOverdrawMargin, maxLineNumberDigits, + ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, onWidthChanged: @onGutterWidthChanged } EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, - lineHeight: lineHeightInPixels, visibleRowRange, lineOverdrawMargin, @pendingChanges, + lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred @@ -96,6 +96,13 @@ EditorComponent = React.createClass height: horizontalScrollbarHeight width: verticalScrollbarWidth + getRenderedRowRange: -> + {editor, lineOverdrawMargin} = @props + [visibleStartRow, visibleEndRow] = editor.getVisibleRowRange() + renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin) + renderedEndRow = Math.min(editor.getLineCount(), visibleEndRow + lineOverdrawMargin) + [renderedStartRow, renderedEndRow] + getInitialState: -> {} getDefaultProps: -> diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index fdccc23cb..b03200a1c 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, lineOverdrawMargin, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props + {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props {selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -36,7 +36,7 @@ EditorScrollViewComponent = React.createClass CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, lineOverdrawMargin, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged, scrollHeight, scrollWidth } diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index a758a4607..cc20c5c43 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -33,11 +33,11 @@ GutterComponent = React.createClass # non-zero-delta change to the screen lines has occurred within the current # visible row range. shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'scrollTop', 'lineHeight', 'fontSize') + return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'scrollTop', 'lineHeight', 'fontSize') - {visibleRowRange, pendingChanges} = newProps + {renderedRowRange, pendingChanges} = newProps for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0 - return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start + return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start false @@ -70,10 +70,8 @@ GutterComponent = React.createClass @removeLineNumberNodes(lineNumberIdsToPreserve) appendOrUpdateVisibleLineNumberNodes: -> - {editor, visibleRowRange, scrollTop, maxLineNumberDigits, lineOverdrawMargin} = @props - [startRow, endRow] = visibleRowRange - startRow = Math.max(0, startRow - lineOverdrawMargin) - endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) + {editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props + [startRow, endRow] = renderedRowRange newLineNumberIds = null newLineNumbersHTML = null @@ -92,7 +90,6 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) - if @hasLineNumberNode(id) @updateLineNumberNode(id, screenRow) else diff --git a/src/lines-component.coffee b/src/lines-component.coffee index e9c6bb465..2e2bb52d0 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -35,11 +35,11 @@ LinesComponent = React.createClass shouldComponentUpdate: (newProps) -> return true if newProps.selectionChanged - return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') + return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') - {visibleRowRange, pendingChanges} = newProps + {renderedRowRange, pendingChanges} = newProps for change in pendingChanges - return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start + return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start false @@ -56,11 +56,8 @@ LinesComponent = React.createClass @lineIdsByScreenRow = {} updateLines: -> - {editor, visibleRowRange, showIndentGuide, selectionChanged, lineOverdrawMargin} = @props - [startRow, endRow] = visibleRowRange - - startRow = Math.max(0, startRow - lineOverdrawMargin) - endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) + {editor, renderedRowRange, showIndentGuide, selectionChanged} = @props + [startRow, endRow] = renderedRowRange visibleLines = editor.linesForScreenRows(startRow, endRow - 1) @removeLineNodes(visibleLines) @@ -198,7 +195,7 @@ LinesComponent = React.createClass editor.setDefaultCharWidth(charWidth) measureCharactersInNewLines: -> - [visibleStartRow, visibleEndRow] = @props.visibleRowRange + [visibleStartRow, visibleEndRow] = @props.renderedRowRange node = @getDOMNode() for tokenizedLine in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) From 37bdfb716bdd0c550faff2c8119408248c5de7c3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 14:03:34 -0600 Subject: [PATCH 57/59] Preserve the target when scrolling w/ mousewheel in gutter Removing the target of a mouseweel event messes up velocity scrolling with the track pad, so it needs to be preserved until scrolling ceases. --- src/editor-component.coffee | 14 ++++++++++++-- src/gutter-component.coffee | 12 +++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 463b47c5e..de37124c9 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -28,6 +28,7 @@ EditorComponent = React.createClass measuringScrollbars: true pendingVerticalScrollDelta: 0 pendingHorizontalScrollDelta: 0 + mouseWheelScreenRow: null render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state @@ -53,7 +54,7 @@ EditorComponent = React.createClass GutterComponent { ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, - @pendingChanges, onWidthChanged: @onGutterWidthChanged + @pendingChanges, onWidthChanged: @onGutterWidthChanged, @mouseWheelScreenRow } EditorScrollViewComponent { @@ -325,7 +326,8 @@ EditorComponent = React.createClass onMouseWheel: (event) -> event.preventDefault() - + screenRow = @screenRowForNode(event.target) + @mouseWheelScreenRow = screenRow if screenRow? animationFramePending = @pendingHorizontalScrollDelta isnt 0 or @pendingVerticalScrollDelta isnt 0 # Only scroll in one direction at a time @@ -343,6 +345,13 @@ EditorComponent = React.createClass @pendingVerticalScrollDelta = 0 @pendingHorizontalScrollDelta = 0 + screenRowForNode: (node) -> + while node isnt document + if screenRow = node.dataset.screenRow + return parseInt(screenRow) + node = node.parentNode + null + onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) @@ -408,6 +417,7 @@ EditorComponent = React.createClass onStoppedScrolling: -> @scrollingVertically = false + @mouseWheelScreenRow = null @requestUpdate() stopScrollingAfterDelay: null # created lazily diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index cc20c5c43..a78c1b540 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -113,13 +113,15 @@ GutterComponent = React.createClass visibleLineNumberIds removeLineNumberNodes: (lineNumberIdsToPreserve) -> + {mouseWheelScreenRow} = @props node = @refs.lineNumbers.getDOMNode() for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId) - delete @lineNumberNodesById[lineNumberId] screenRow = @screenRowsByLineNumberId[lineNumberId] - delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId - delete @screenRowsByLineNumberId[lineNumberId] - node.removeChild(lineNumberNode) + unless screenRow is mouseWheelScreenRow + delete @lineNumberNodesById[lineNumberId] + delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId + delete @screenRowsByLineNumberId[lineNumberId] + node.removeChild(lineNumberNode) buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) -> if screenRow? @@ -129,7 +131,7 @@ GutterComponent = React.createClass style = "visibility: hidden;" innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - "
#{innerHTML}
" + "
#{innerHTML}
" buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped From 795399e184f1738fefde358afebd95b0a1aa36e8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 14:17:09 -0600 Subject: [PATCH 58/59] Preserve the target when scrolling w/ mousewheel on editor lines --- src/editor-component.coffee | 2 +- src/editor-scroll-view-component.coffee | 4 ++-- src/lines-component.coffee | 12 +++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index de37124c9..113de150d 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -62,7 +62,7 @@ EditorComponent = React.createClass lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay, - @onInputFocused, @onInputBlurred + @onInputFocused, @onInputBlurred, @mouseWheelScreenRow } ScrollbarComponent diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index b03200a1c..1363c5e67 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props + {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically, mouseWheelScreenRow} = @props {selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -37,7 +37,7 @@ EditorScrollViewComponent = React.createClass LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, - selectionChanged, scrollHeight, scrollWidth + selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow } componentDidMount: -> diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 2e2bb52d0..1cae8b8fb 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -64,15 +64,17 @@ LinesComponent = React.createClass @appendOrUpdateVisibleLineNodes(visibleLines, startRow) removeLineNodes: (visibleLines=[]) -> + {mouseWheelScreenRow} = @props visibleLineIds = new Set visibleLineIds.add(line.id.toString()) for line in visibleLines node = @getDOMNode() for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId) - delete @lineNodesByLineId[lineId] screenRow = @screenRowsByLineId[lineId] - delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId - delete @screenRowsByLineId[lineId] - node.removeChild(lineNode) + unless screenRow is mouseWheelScreenRow + delete @lineNodesByLineId[lineId] + delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId + delete @screenRowsByLineId[lineId] + node.removeChild(lineNode) appendOrUpdateVisibleLineNodes: (visibleLines, startRow) -> {lineHeight} = @props @@ -112,7 +114,7 @@ LinesComponent = React.createClass {editor, mini, showIndentGuide, lineHeight} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line top = screenRow * lineHeight - lineHTML = "
" + lineHTML = "
" if text is "" lineHTML += @buildEmptyLineInnerHTML(line) From bfc382c398cc55b47ef83e52e61efd81f86fffdc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 14:33:17 -0600 Subject: [PATCH 59/59] Add specs for line/line-number preservation --- spec/editor-component-spec.coffee | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index d5f04e65f..28f740531 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -653,6 +653,32 @@ describe "EditorComponent", -> expect(verticalScrollbarNode.scrollTop).toBe 10 expect(horizontalScrollbarNode.scrollLeft).toBe 15 + describe "when the mousewheel event's target is a line", -> + it "keeps the line on the DOM if it is scrolled off-screen", -> + node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() + + lineNode = node.querySelector('.line') + wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) + Object.defineProperty(wheelEvent, 'target', get: -> lineNode) + node.dispatchEvent(wheelEvent) + + expect(node.contains(lineNode)).toBe true + + describe "when the mousewheel event's target is a line number", -> + it "keeps the line number on the DOM if it is scrolled off-screen", -> + node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() + + lineNumberNode = node.querySelectorAll('.line-number')[1] + wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) + Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode) + node.dispatchEvent(wheelEvent) + + expect(node.contains(lineNumberNode)).toBe true + describe "input events", -> inputNode = null