_ = require 'underscore-plus' React = require 'react-atom-fork' {div, span} = require 'reactionary-atom-fork' {debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' {$$} = require 'space-pen' CursorsComponent = require './cursors-component' HighlightsComponent = require './highlights-component' OverlayManager = require './overlay-manager' 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 displayName: 'LinesComponent' render: -> {editor, presenter} = @props @oldState ?= {lines: {}} @newState = presenter.state.content {scrollHeight, scrollWidth, backgroundColor, placeholderText} = @newState style = height: scrollHeight width: scrollWidth WebkitTransform: @getTransform() backgroundColor: backgroundColor div {className: 'lines', style}, div className: 'placeholder-text', placeholderText if placeholderText? CursorsComponent {presenter} HighlightsComponent {presenter} getTransform: -> {scrollTop, scrollLeft} = @newState {useHardwareAcceleration} = @props if useHardwareAcceleration "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" else "translate(#{-scrollLeft}px, #{-scrollTop}px)" componentWillMount: -> @measuredLines = new Set @lineNodesByLineId = {} @screenRowsByLineId = {} @lineIdsByScreenRow = {} @renderedDecorationsByLineId = {} componentDidMount: -> if @props.useShadowDOM insertionPoint = document.createElement('content') insertionPoint.setAttribute('select', '.overlayer') @getDOMNode().appendChild(insertionPoint) insertionPoint = document.createElement('content') insertionPoint.setAttribute('select', 'atom-overlay') @overlayManager = new OverlayManager(@props.hostElement) @getDOMNode().appendChild(insertionPoint) else @overlayManager = new OverlayManager(@getDOMNode()) componentDidUpdate: -> {visible, presenter} = @props @removeLineNodes() unless @oldState?.indentGuidesVisible is @newState?.indentGuidesVisible @updateLineNodes() @measureCharactersInNewLines() if visible and not presenter.state.scrollingVertically @overlayManager?.render(@props) @oldState.indentGuidesVisible = @newState.indentGuidesVisible @oldState.scrollWidth = @newState.scrollWidth clearScreenRowCaches: -> @screenRowsByLineId = {} @lineIdsByScreenRow = {} removeLineNodes: -> @removeLineNode(id) for id of @oldState.lines removeLineNode: (id) -> @lineNodesByLineId[id].remove() delete @lineNodesByLineId[id] delete @lineIdsByScreenRow[@screenRowsByLineId[id]] delete @screenRowsByLineId[id] delete @oldState.lines[id] updateLineNodes: -> {presenter} = @props for id of @oldState.lines unless @newState.lines.hasOwnProperty(id) @removeLineNode(id) newLineIds = null newLinesHTML = null for id, lineState of @newState.lines if @oldState.lines.hasOwnProperty(id) @updateLineNode(id) else newLineIds ?= [] newLinesHTML ?= "" newLineIds.push(id) newLinesHTML += @buildLineHTML(id) @screenRowsByLineId[id] = lineState.screenRow @lineIdsByScreenRow[lineState.screenRow] = id @oldState.lines[id] = _.clone(lineState) return unless newLineIds? WrapperDiv.innerHTML = newLinesHTML newLineNodes = toArray(WrapperDiv.children) node = @getDOMNode() for id, i in newLineIds lineNode = newLineNodes[i] @lineNodesByLineId[id] = lineNode node.appendChild(lineNode) buildLineHTML: (id) -> {presenter} = @props {scrollWidth} = @newState {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id] classes = '' if decorationClasses? for decorationClass in decorationClasses classes += decorationClass + ' ' classes += 'line' lineHTML = "
" if text is "" lineHTML += @buildEmptyLineInnerHTML(id) else lineHTML += @buildLineInnerHTML(id) lineHTML += '' if fold lineHTML += "
" lineHTML buildEmptyLineInnerHTML: (id) -> {indentGuidesVisible} = @newState {indentLevel, tabLength, endOfLineInvisibles} = @newState.lines[id] if indentGuidesVisible and indentLevel > 0 invisibleIndex = 0 lineHTML = '' for i in [0...indentLevel] lineHTML += "" for j in [0...tabLength] if invisible = endOfLineInvisibles?[invisibleIndex++] lineHTML += "#{invisible}" else lineHTML += ' ' lineHTML += "" while invisibleIndex < endOfLineInvisibles?.length lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}" lineHTML else @buildEndOfLineHTML(id) or ' ' buildLineInnerHTML: (id) -> {editor} = @props {indentGuidesVisible} = @newState {tokens, text} = @newState.lines[id] innerHTML = "" scopeStack = [] firstTrailingWhitespacePosition = text.search(/\s*$/) lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 for token in tokens innerHTML += @updateScopeStack(scopeStack, token.scopes) hasIndentGuide = indentGuidesVisible and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and lineIsWhitespaceOnly)) innerHTML += token.getValueAsHtml({hasIndentGuide}) innerHTML += @popScope(scopeStack) while scopeStack.length > 0 innerHTML += @buildEndOfLineHTML(id) innerHTML buildEndOfLineHTML: (id) -> {endOfLineInvisibles} = @newState.lines[id] html = '' if endOfLineInvisibles? for invisible in endOfLineInvisibles html += "#{invisible}" html updateScopeStack: (scopeStack, desiredScopeDescriptor) -> html = "" # Find a common prefix for scope, i in desiredScopeDescriptor break unless scopeStack[i] is desiredScopeDescriptor[i] # Pop scopeDescriptor until we're at the common prefx until scopeStack.length is i html += @popScope(scopeStack) # Push onto common prefix until scopeStack equals desiredScopeDescriptor for j in [i...desiredScopeDescriptor.length] html += @pushScope(scopeStack, desiredScopeDescriptor[j]) html popScope: (scopeStack) -> scopeStack.pop() "" pushScope: (scopeStack, scope) -> scopeStack.push(scope) "" updateLineNode: (id) -> {scrollWidth} = @newState {screenRow, top} = @newState.lines[id] lineNode = @lineNodesByLineId[id] newDecorationClasses = @newState.lines[id].decorationClasses oldDecorationClasses = @oldState.lines[id].decorationClasses if oldDecorationClasses? for decorationClass in oldDecorationClasses unless newDecorationClasses? and decorationClass in newDecorationClasses lineNode.classList.remove(decorationClass) if newDecorationClasses? for decorationClass in newDecorationClasses unless oldDecorationClasses? and decorationClass in oldDecorationClasses lineNode.classList.add(decorationClass) lineNode.style.width = scrollWidth + 'px' lineNode.style.top = top + 'px' lineNode.dataset.screenRow = screenRow @screenRowsByLineId[id] = screenRow @lineIdsByScreenRow[screenRow] = id lineNodeForScreenRow: (screenRow) -> @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] measureLineHeightAndDefaultCharWidth: -> node = @getDOMNode() node.appendChild(DummyLineNode) lineHeightInPixels = DummyLineNode.getBoundingClientRect().height charWidth = DummyLineNode.firstChild.getBoundingClientRect().width node.removeChild(DummyLineNode) {editor, presenter} = @props presenter.setLineHeight(lineHeightInPixels) editor.setLineHeightInPixels(lineHeightInPixels) presenter.setBaseCharacterWidth(charWidth) editor.setDefaultCharWidth(charWidth) remeasureCharacterWidths: -> return unless @props.presenter.hasRequiredMeasurements() @clearScopedCharWidths() @measureCharactersInNewLines() measureCharactersInNewLines: -> {editor} = @props node = @getDOMNode() editor.batchCharacterMeasurement => for id, lineState of @oldState.lines unless @measuredLines.has(id) lineNode = @lineNodesByLineId[id] @measureCharactersInLine(lineState, lineNode) return measureCharactersInLine: (tokenizedLine, lineNode) -> {editor} = @props rangeForMeasurement = null iterator = null charIndex = 0 for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens charWidths = editor.getScopedCharWidths(scopes) valueIndex = 0 while valueIndex < value.length if hasPairedCharacter char = value.substr(valueIndex, 2) charLength = 2 valueIndex += 2 else char = value[valueIndex] charLength = 1 valueIndex++ continue if char is '\0' unless charWidths[char]? unless textNode? rangeForMeasurement ?= document.createRange() iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) textNode = iterator.nextNode() textNodeIndex = 0 nextTextNodeIndex = textNode.textContent.length while nextTextNodeIndex <= charIndex textNode = iterator.nextNode() textNodeIndex = nextTextNodeIndex nextTextNodeIndex = textNodeIndex + textNode.textContent.length i = charIndex - textNodeIndex rangeForMeasurement.setStart(textNode, i) rangeForMeasurement.setEnd(textNode, i + charLength) charWidth = rangeForMeasurement.getBoundingClientRect().width editor.setScopedCharWidth(scopes, char, charWidth) @props.presenter.setScopedCharWidth(scopes, char, charWidth) charIndex += charLength @measuredLines.add(tokenizedLine.id) clearScopedCharWidths: -> @measuredLines.clear() @props.editor.clearScopedCharWidths() @props.presenter.clearScopedCharWidths()