mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Merge pull request #2258 from atom/ns-react-scroll-perf
Improve scroll performance of the React editor
This commit is contained in:
@@ -6,8 +6,10 @@ CursorComponent = React.createClass
|
||||
displayName: 'CursorComponent'
|
||||
|
||||
render: ->
|
||||
{top, left, height, width} = @props.cursor.getPixelRect()
|
||||
className = 'cursor'
|
||||
className += ' blink-off' if @props.blinkOff
|
||||
{cursor, scrollTop, scrollLeft} = @props
|
||||
{top, left, height, width} = cursor.getPixelRect()
|
||||
top -= scrollTop
|
||||
left -= scrollLeft
|
||||
WebkitTransform = "translate3d(#{left}px, #{top}px, 0px)"
|
||||
|
||||
div className: className, style: {top, left, height, width}
|
||||
div className: 'cursor', style: {height, width, WebkitTransform}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
React = require 'react'
|
||||
{div} = require 'reactionary'
|
||||
{debounce} = require 'underscore-plus'
|
||||
{debounce, toArray} = require 'underscore-plus'
|
||||
SubscriberMixin = require './subscriber-mixin'
|
||||
CursorComponent = require './cursor-component'
|
||||
|
||||
|
||||
module.exports =
|
||||
CursorsComponent = React.createClass
|
||||
displayName: 'CursorsComponent'
|
||||
@@ -13,22 +12,24 @@ CursorsComponent = React.createClass
|
||||
cursorBlinkIntervalHandle: null
|
||||
|
||||
render: ->
|
||||
{editor} = @props
|
||||
blinkOff = @state.blinkCursorsOff
|
||||
{editor, scrollTop, scrollLeft} = @props
|
||||
{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, blinkOff})
|
||||
CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft})
|
||||
|
||||
getInitialState: ->
|
||||
blinkCursorsOff: false
|
||||
blinking: true
|
||||
|
||||
componentDidMount: ->
|
||||
{editor} = @props
|
||||
@startBlinkingCursors()
|
||||
|
||||
componentWillUnmount: ->
|
||||
clearInterval(@cursorBlinkIntervalHandle)
|
||||
@@ -36,15 +37,21 @@ CursorsComponent = React.createClass
|
||||
componentWillUpdate: ({cursorsMoved}) ->
|
||||
@pauseCursorBlinking() if cursorsMoved
|
||||
|
||||
componentDidUpdate: ->
|
||||
@syncCursorAnimations() if @props.selectionAdded
|
||||
|
||||
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()
|
||||
|
||||
syncCursorAnimations: ->
|
||||
node = @getDOMNode()
|
||||
cursorNodes = toArray(node.children)
|
||||
node.removeChild(cursorNode) for cursorNode in cursorNodes
|
||||
node.appendChild(cursorNode) for cursorNode in cursorNodes
|
||||
|
||||
@@ -241,7 +241,8 @@ 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)
|
||||
|
||||
[startRow, endRow]
|
||||
|
||||
intersectsVisibleRowRange: (startRow, endRow) ->
|
||||
|
||||
@@ -20,15 +20,19 @@ EditorComponent = React.createClass
|
||||
batchingUpdates: false
|
||||
updateRequested: false
|
||||
cursorsMoved: false
|
||||
preservedRowRange: null
|
||||
selectionChanged: false
|
||||
selectionAdded: false
|
||||
scrollingVertically: false
|
||||
gutterWidth: 0
|
||||
refreshingScrollbars: false
|
||||
measuringScrollbars: true
|
||||
pendingVerticalScrollDelta: 0
|
||||
pendingHorizontalScrollDelta: 0
|
||||
mouseWheelScreenRow: null
|
||||
|
||||
render: ->
|
||||
{focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state
|
||||
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
|
||||
{editor, cursorBlinkResumeDelay} = @props
|
||||
maxLineNumberDigits = editor.getScreenLineCount().toString().length
|
||||
|
||||
if @isMounted()
|
||||
@@ -48,16 +52,17 @@ EditorComponent = React.createClass
|
||||
|
||||
div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1,
|
||||
GutterComponent {
|
||||
editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight,
|
||||
lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges,
|
||||
onWidthChanged: @onGutterWidthChanged
|
||||
ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits,
|
||||
scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily,
|
||||
@pendingChanges, onWidthChanged: @onGutterWidthChanged, @mouseWheelScreenRow
|
||||
}
|
||||
|
||||
EditorScrollViewComponent {
|
||||
ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide
|
||||
scrollHeight, scrollWidth, lineHeight: lineHeightInPixels,
|
||||
renderedRowRange, @pendingChanges, @scrollingVertically, @cursorsMoved,
|
||||
cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred
|
||||
ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide,
|
||||
lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges,
|
||||
scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically,
|
||||
@cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay,
|
||||
@onInputFocused, @onInputBlurred, @mouseWheelScreenRow
|
||||
}
|
||||
|
||||
ScrollbarComponent
|
||||
@@ -93,17 +98,17 @@ EditorComponent = React.createClass
|
||||
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
|
||||
{editor, lineOverdrawMargin} = @props
|
||||
[visibleStartRow, visibleEndRow] = editor.getVisibleRowRange()
|
||||
renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin)
|
||||
renderedEndRow = Math.min(editor.getLineCount(), visibleEndRow + lineOverdrawMargin)
|
||||
[renderedStartRow, renderedEndRow]
|
||||
|
||||
getInitialState: -> {}
|
||||
|
||||
getDefaultProps: ->
|
||||
cursorBlinkPeriod: 800
|
||||
cursorBlinkResumeDelay: 200
|
||||
cursorBlinkResumeDelay: 100
|
||||
lineOverdrawMargin: 8
|
||||
|
||||
componentWillMount: ->
|
||||
@pendingChanges = []
|
||||
@@ -130,6 +135,8 @@ EditorComponent = React.createClass
|
||||
componentDidUpdate: ->
|
||||
@pendingChanges.length = 0
|
||||
@cursorsMoved = false
|
||||
@selectionChanged = false
|
||||
@selectionAdded = false
|
||||
@refreshingScrollbars = false
|
||||
@measureScrollbars() if @measuringScrollbars
|
||||
@props.parentView.trigger 'editor:display-updated'
|
||||
@@ -140,9 +147,8 @@ 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-removed selection-screen-range-changed', @onSelectionChanged
|
||||
@subscribe editor, 'selection-added', @onSelectionAdded
|
||||
@subscribe editor, 'selection-removed', @onSelectionAdded
|
||||
@subscribe editor.$scrollTop.changes, @onScrollTopChanged
|
||||
@subscribe editor.$scrollLeft.changes, @requestUpdate
|
||||
@subscribe editor.$height.changes, @requestUpdate
|
||||
@@ -319,14 +325,32 @@ EditorComponent = React.createClass
|
||||
@pendingScrollLeft = null
|
||||
|
||||
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
|
||||
{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
|
||||
|
||||
screenRowForNode: (node) ->
|
||||
while node isnt document
|
||||
if screenRow = node.dataset.screenRow
|
||||
return parseInt(screenRow)
|
||||
node = node.parentNode
|
||||
null
|
||||
|
||||
onStylesheetsChanged: (stylesheet) ->
|
||||
@refreshScrollbars() if @containsScrollbarSelector(stylesheet)
|
||||
@@ -357,13 +381,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
|
||||
|
||||
@@ -379,20 +396,31 @@ EditorComponent = React.createClass
|
||||
@pendingChanges.push(change)
|
||||
@requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events
|
||||
|
||||
onSelectionChanged: (selection) ->
|
||||
{editor} = @props
|
||||
if editor.selectionIntersectsVisibleRowRange(selection)
|
||||
@selectionChanged = true
|
||||
@requestUpdate()
|
||||
|
||||
onSelectionAdded: (selection) ->
|
||||
{editor} = @props
|
||||
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
|
||||
if editor.selectionIntersectsVisibleRowRange(selection)
|
||||
@selectionChanged = true
|
||||
@selectionAdded = true
|
||||
@requestUpdate()
|
||||
|
||||
onScrollTopChanged: ->
|
||||
@preservedRowRange = @getRenderedRowRange()
|
||||
@scrollingVertically = true
|
||||
@clearPreservedRowRangeAfterDelay ?= debounce(@clearPreservedRowRange, 200)
|
||||
@clearPreservedRowRangeAfterDelay()
|
||||
@requestUpdate()
|
||||
@stopScrollingAfterDelay ?= debounce(@onStoppedScrolling, 100)
|
||||
@stopScrollingAfterDelay()
|
||||
|
||||
onStoppedScrolling: ->
|
||||
@scrollingVertically = false
|
||||
@mouseWheelScreenRow = null
|
||||
@requestUpdate()
|
||||
|
||||
onSelectionRemoved: (selection) ->
|
||||
{editor} = @props
|
||||
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
|
||||
stopScrollingAfterDelay: null # created lazily
|
||||
|
||||
onCursorsMoved: ->
|
||||
@cursorsMoved = true
|
||||
@@ -411,3 +439,7 @@ EditorComponent = React.createClass
|
||||
|
||||
consolidateSelections: (e) ->
|
||||
e.abortKeyBinding() unless @props.editor.consolidateSelections()
|
||||
|
||||
lineNodeForScreenRow: (screenRow) -> @refs.scrollView.lineNodeForScreenRow(screenRow)
|
||||
|
||||
lineNumberNodeForScreenRow: (screenRow) -> @refs.gutter.lineNumberNodeForScreenRow(screenRow)
|
||||
|
||||
@@ -17,19 +17,14 @@ EditorScrollViewComponent = React.createClass
|
||||
|
||||
render: ->
|
||||
{editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props
|
||||
{scrollHeight, scrollWidth, renderedRowRange, pendingChanges, scrollingVertically} = @props
|
||||
{cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props
|
||||
{renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically, mouseWheelScreenRow} = @props
|
||||
{selectionChanged, selectionAdded, 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',
|
||||
div className: 'scroll-view', onMouseDown: @onMouseDown,
|
||||
InputComponent
|
||||
ref: 'input'
|
||||
className: 'hidden-input'
|
||||
@@ -38,14 +33,12 @@ EditorScrollViewComponent = React.createClass
|
||||
onFocus: onInputFocused
|
||||
onBlur: onInputBlurred
|
||||
|
||||
div className: 'scroll-view-content', style: contentStyle, onMouseDown: @onMouseDown,
|
||||
CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay})
|
||||
LinesComponent {
|
||||
ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide,
|
||||
renderedRowRange, pendingChanges, scrollingVertically
|
||||
}
|
||||
div className: 'underlayer',
|
||||
SelectionsComponent({editor})
|
||||
CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay})
|
||||
LinesComponent {
|
||||
ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide,
|
||||
renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically,
|
||||
selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow
|
||||
}
|
||||
|
||||
componentDidMount: ->
|
||||
@getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged
|
||||
@@ -198,3 +191,5 @@ EditorScrollViewComponent = React.createClass
|
||||
|
||||
focus: ->
|
||||
@refs.input.focus()
|
||||
|
||||
lineNodeForScreenRow: (screenRow) -> @refs.lines.lineNodeForScreenRow(screenRow)
|
||||
|
||||
@@ -1269,6 +1269,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.
|
||||
#
|
||||
|
||||
@@ -1,46 +1,33 @@
|
||||
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'
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
lastMeasuredWidth: null
|
||||
dummyLineNumberNode: null
|
||||
|
||||
render: ->
|
||||
{scrollHeight, scrollTop} = @props
|
||||
|
||||
div className: 'gutter',
|
||||
@renderLineNumbers() if @isMounted()
|
||||
div className: 'line-numbers', ref: 'lineNumbers', style:
|
||||
height: scrollHeight
|
||||
WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)"
|
||||
|
||||
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)"
|
||||
componentWillMount: ->
|
||||
@lineNumberNodesById = {}
|
||||
@lineNumberIdsByScreenRow = {}
|
||||
@screenRowsByLineNumberId = {}
|
||||
|
||||
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
|
||||
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
|
||||
@@ -49,39 +36,130 @@ GutterComponent = React.createClass
|
||||
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'scrollTop', 'lineHeight', 'fontSize')
|
||||
|
||||
{renderedRowRange, 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 <= renderedRowRange.start or renderedRowRange.end <= change.start
|
||||
|
||||
false
|
||||
|
||||
componentDidUpdate: (oldProps) ->
|
||||
unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily')
|
||||
width = @getDOMNode().offsetWidth
|
||||
if width isnt @lastMeasuredWidth
|
||||
@lastMeasuredWidth = width
|
||||
@props.onWidthChanged(width)
|
||||
unless oldProps.maxLineNumberDigits is @props.maxLineNumberDigits
|
||||
@updateDummyLineNumber()
|
||||
@removeLineNumberNodes()
|
||||
|
||||
LineNumberComponent = React.createClass
|
||||
displayName: 'LineNumberComponent'
|
||||
@measureWidth() unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily')
|
||||
@clearScreenRowCaches() unless oldProps.lineHeight is @props.lineHeight
|
||||
@updateLineNumbers()
|
||||
|
||||
render: ->
|
||||
{bufferRow, screenRow, lineHeight} = @props
|
||||
div
|
||||
className: "line-number line-number-#{bufferRow}"
|
||||
style: {top: screenRow * lineHeight}
|
||||
'data-buffer-row': bufferRow
|
||||
'data-screen-row': screenRow
|
||||
dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
|
||||
clearScreenRowCaches: ->
|
||||
@lineNumberIdsByScreenRow = {}
|
||||
@screenRowsByLineNumberId = {}
|
||||
|
||||
buildInnerHTML: ->
|
||||
{lineNumber, maxLineNumberDigits} = @props
|
||||
if lineNumber.length < maxLineNumberDigits
|
||||
padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length)
|
||||
padding + lineNumber + @iconDivHTML
|
||||
# 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: ->
|
||||
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits)
|
||||
|
||||
updateLineNumbers: ->
|
||||
lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes()
|
||||
@removeLineNumberNodes(lineNumberIdsToPreserve)
|
||||
|
||||
appendOrUpdateVisibleLineNumberNodes: ->
|
||||
{editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props
|
||||
[startRow, endRow] = renderedRowRange
|
||||
|
||||
newLineNumberIds = null
|
||||
newLineNumbersHTML = null
|
||||
visibleLineNumberIds = new Set
|
||||
|
||||
wrapCount = 0
|
||||
for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||
screenRow = startRow + index
|
||||
|
||||
if bufferRow is lastBufferRow
|
||||
id = "#{bufferRow}-#{wrapCount++}"
|
||||
else
|
||||
id = bufferRow.toString()
|
||||
lastBufferRow = bufferRow
|
||||
wrapCount = 0
|
||||
|
||||
visibleLineNumberIds.add(id)
|
||||
|
||||
if @hasLineNumberNode(id)
|
||||
@updateLineNumberNode(id, screenRow)
|
||||
else
|
||||
newLineNumberIds ?= []
|
||||
newLineNumbersHTML ?= ""
|
||||
newLineNumberIds.push(id)
|
||||
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow)
|
||||
@screenRowsByLineNumberId[id] = screenRow
|
||||
@lineNumberIdsByScreenRow[screenRow] = id
|
||||
|
||||
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
|
||||
|
||||
removeLineNumberNodes: (lineNumberIdsToPreserve) ->
|
||||
{mouseWheelScreenRow} = @props
|
||||
node = @refs.lineNumbers.getDOMNode()
|
||||
for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId)
|
||||
screenRow = @screenRowsByLineNumberId[lineNumberId]
|
||||
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?
|
||||
{lineHeight} = @props
|
||||
style = "position: absolute; top: #{screenRow * lineHeight}px;"
|
||||
else
|
||||
lineNumber + @iconDivHTML
|
||||
style = "visibility: hidden;"
|
||||
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits)
|
||||
|
||||
iconDivHTML: '<div class="icon-right"></div>'
|
||||
"<div class=\"line-number editor-colors\" style=\"#{style}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqualForProperties(newProps, @props, 'lineHeight', 'screenRow', 'maxLineNumberDigits')
|
||||
buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) ->
|
||||
if softWrapped
|
||||
lineNumber = "•"
|
||||
else
|
||||
lineNumber = (bufferRow + 1).toString()
|
||||
|
||||
padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length)
|
||||
iconHTML = '<div class="icon-right"></div>'
|
||||
padding + lineNumber + iconHTML
|
||||
|
||||
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)
|
||||
|
||||
lineNumberNodeForScreenRow: (screenRow) ->
|
||||
@lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]]
|
||||
|
||||
measureWidth: ->
|
||||
lineNumberNode = @refs.lineNumbers.getDOMNode().firstChild
|
||||
# return unless lineNumberNode?
|
||||
|
||||
width = lineNumberNode.offsetWidth
|
||||
if width isnt @lastMeasuredWidth
|
||||
@props.onWidthChanged(@lastMeasuredWidth = width)
|
||||
|
||||
@@ -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: ''}
|
||||
|
||||
@@ -1,10 +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'
|
||||
|
||||
SelectionsComponent = require './selections-component'
|
||||
|
||||
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
|
||||
@@ -12,23 +15,27 @@ LinesComponent = React.createClass
|
||||
|
||||
render: ->
|
||||
if @isMounted()
|
||||
{editor, renderedRowRange, lineHeight, showIndentGuide} = @props
|
||||
[startRow, endRow] = renderedRowRange
|
||||
{editor, scrollTop, scrollLeft, scrollHeight, scrollWidth, lineHeight} = @props
|
||||
style =
|
||||
height: scrollHeight
|
||||
width: scrollWidth
|
||||
WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)"
|
||||
|
||||
lines =
|
||||
for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1)
|
||||
LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, screenRow: startRow + i})
|
||||
|
||||
div {className: 'lines'}, lines
|
||||
div {className: 'lines', style},
|
||||
SelectionsComponent({editor, lineHeight}) if @isMounted()
|
||||
|
||||
componentWillMount: ->
|
||||
@measuredLines = new WeakSet
|
||||
@lineNodesByLineId = {}
|
||||
@screenRowsByLineId = {}
|
||||
@lineIdsByScreenRow = {}
|
||||
|
||||
componentDidMount: ->
|
||||
@measureLineHeightAndCharWidth()
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide')
|
||||
return true if newProps.selectionChanged
|
||||
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically')
|
||||
|
||||
{renderedRowRange, pendingChanges} = newProps
|
||||
for change in pendingChanges
|
||||
@@ -38,9 +45,146 @@ 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
|
||||
|
||||
clearScreenRowCaches: ->
|
||||
@screenRowsByLineId = {}
|
||||
@lineIdsByScreenRow = {}
|
||||
|
||||
updateLines: ->
|
||||
{editor, renderedRowRange, showIndentGuide, selectionChanged} = @props
|
||||
[startRow, endRow] = renderedRowRange
|
||||
|
||||
visibleLines = editor.linesForScreenRows(startRow, endRow - 1)
|
||||
@removeLineNodes(visibleLines)
|
||||
@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)
|
||||
screenRow = @screenRowsByLineId[lineId]
|
||||
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
|
||||
newLines = null
|
||||
newLinesHTML = null
|
||||
|
||||
for line, index in visibleLines
|
||||
screenRow = startRow + index
|
||||
|
||||
if @hasLineNode(line.id)
|
||||
@updateLineNode(line, screenRow)
|
||||
else
|
||||
newLines ?= []
|
||||
newLinesHTML ?= ""
|
||||
newLines.push(line)
|
||||
newLinesHTML += @buildLineHTML(line, screenRow)
|
||||
@screenRowsByLineId[line.id] = screenRow
|
||||
@lineIdsByScreenRow[screenRow] = line.id
|
||||
|
||||
return unless newLines?
|
||||
|
||||
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) ->
|
||||
"translate3d(0px, #{top}px, 0px)"
|
||||
|
||||
buildLineHTML: (line, screenRow) ->
|
||||
{editor, mini, showIndentGuide, lineHeight} = @props
|
||||
{tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line
|
||||
top = screenRow * lineHeight
|
||||
lineHTML = "<div class=\"line\" style=\"position: absolute; top: #{top}px;\" data-screen-row=\"#{screenRow}\">"
|
||||
|
||||
if text is ""
|
||||
lineHTML += @buildEmptyLineInnerHTML(line)
|
||||
else
|
||||
lineHTML += @buildLineInnerHTML(line)
|
||||
|
||||
lineHTML += "</div>"
|
||||
lineHTML
|
||||
|
||||
buildEmptyLineInnerHTML: (line) ->
|
||||
{showIndentGuide} = @props
|
||||
{indentLevel, tabLength} = line
|
||||
|
||||
if showIndentGuide and indentLevel > 0
|
||||
indentSpan = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
|
||||
multiplyString(indentSpan, indentLevel + 1)
|
||||
else
|
||||
" "
|
||||
|
||||
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 = ""
|
||||
|
||||
# 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()
|
||||
"</span>"
|
||||
|
||||
pushScope: (scopeStack, scope) ->
|
||||
scopeStack.push(scope)
|
||||
"<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
|
||||
|
||||
updateLineNode: (line, screenRow) ->
|
||||
unless @screenRowsByLineId[line.id] is screenRow
|
||||
{lineHeight} = @props
|
||||
lineNode = @lineNodesByLineId[line.id]
|
||||
lineNode.style.top = screenRow * lineHeight + 'px'
|
||||
@screenRowsByLineId[line.id] = screenRow
|
||||
@lineIdsByScreenRow[screenRow] = line.id
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
|
||||
|
||||
measureLineHeightAndCharWidth: ->
|
||||
node = @getDOMNode()
|
||||
node.appendChild(DummyLineNode)
|
||||
@@ -56,9 +200,9 @@ LinesComponent = React.createClass
|
||||
[visibleStartRow, visibleEndRow] = @props.renderedRowRange
|
||||
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) ->
|
||||
@@ -97,44 +241,3 @@ LinesComponent = React.createClass
|
||||
clearScopedCharWidths: ->
|
||||
@measuredLines.clear()
|
||||
@props.editor.clearScopedCharWidths()
|
||||
|
||||
|
||||
LineComponent = React.createClass
|
||||
displayName: 'LineComponent'
|
||||
|
||||
render: ->
|
||||
{screenRow, lineHeight} = @props
|
||||
|
||||
style =
|
||||
top: screenRow * lineHeight
|
||||
position: 'absolute'
|
||||
|
||||
div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
|
||||
|
||||
buildInnerHTML: ->
|
||||
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 = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
|
||||
multiplyString(indentSpan, indentLevel + 1)
|
||||
else
|
||||
" "
|
||||
|
||||
buildScopeTreeHTML: (scopeTree) ->
|
||||
if scopeTree.children?
|
||||
html = "<span class='#{scopeTree.scope.replace(/\./g, ' ')}'>"
|
||||
html += @buildScopeTreeHTML(child) for child in scopeTree.children
|
||||
html += "</span>"
|
||||
html
|
||||
else
|
||||
"<span>#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}</span>"
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow')
|
||||
|
||||
@@ -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,11 +38,12 @@ 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()
|
||||
|
||||
@underlayer = $(node).find('.underlayer')
|
||||
@underlayer = $(node).find('.selections')
|
||||
|
||||
@gutter = $(node).find('.gutter')
|
||||
@gutter.removeClassFromAllLines = (klass) =>
|
||||
@@ -64,7 +66,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)
|
||||
|
||||
@@ -6,6 +6,60 @@ SelectionComponent = React.createClass
|
||||
displayName: 'SelectionComponent'
|
||||
|
||||
render: ->
|
||||
{editor, screenRange, lineHeight} = @props
|
||||
{start, end} = screenRange
|
||||
rowCount = end.row - start.row + 1
|
||||
startPixelPosition = editor.pixelPositionForScreenPosition(start)
|
||||
endPixelPosition = editor.pixelPositionForScreenPosition(end)
|
||||
|
||||
div className: 'selection',
|
||||
for regionRect, i in @props.selection.getRegionRects()
|
||||
div className: 'region', key: i, style: regionRect
|
||||
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
|
||||
|
||||
@@ -563,6 +563,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.
|
||||
@@ -595,47 +601,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
|
||||
|
||||
screenRangeChanged: ->
|
||||
screenRange = @getScreenRange()
|
||||
@emit 'screen-range-changed', screenRange
|
||||
|
||||
@@ -7,10 +7,37 @@ SelectionsComponent = React.createClass
|
||||
displayName: 'SelectionsComponent'
|
||||
|
||||
render: ->
|
||||
{editor} = @props
|
||||
div className: 'selections', @renderSelections()
|
||||
|
||||
div className: 'selections',
|
||||
if @isMounted()
|
||||
for selection in editor.getSelections()
|
||||
if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||
SelectionComponent({key: selection.id, selection})
|
||||
renderSelections: ->
|
||||
{editor, lineHeight} = @props
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user