_ = require 'underscore-plus' {toArray} = require 'underscore-plus' {$$} = require 'space-pen' CursorsComponent = require './cursors-component' HighlightsComponent = require './highlights-component' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} WrapperDiv = document.createElement('div') cloneObject = (object) -> clone = {} clone[key] = value for key, value of object clone module.exports = class LinesComponent placeholderTextDiv: null constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) -> @measuredLines = new Set @lineNodesByLineId = {} @screenRowsByLineId = {} @lineIdsByScreenRow = {} @renderedDecorationsByLineId = {} @domNode = document.createElement('div') @domNode.classList.add('lines') @cursorsComponent = new CursorsComponent(@presenter) @domNode.appendChild(@cursorsComponent.getDomNode()) @highlightsComponent = new HighlightsComponent(@presenter) @domNode.appendChild(@highlightsComponent.getDomNode()) if @useShadowDOM insertionPoint = document.createElement('content') insertionPoint.setAttribute('select', '.overlayer') @domNode.appendChild(insertionPoint) getDomNode: -> @domNode updateSync: (state) -> @newState = state.content @oldState ?= {lines: {}} if @newState.scrollHeight isnt @oldState.scrollHeight @domNode.style.height = @newState.scrollHeight + 'px' @oldState.scrollHeight = @newState.scrollHeight if @newState.scrollTop isnt @oldState.scrollTop or @newState.scrollLeft isnt @oldState.scrollLeft @domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, #{-@newState.scrollTop}px, 0px)" @oldState.scrollTop = @newState.scrollTop @oldState.scrollLeft = @newState.scrollLeft if @newState.backgroundColor isnt @oldState.backgroundColor @domNode.style.backgroundColor = @newState.backgroundColor @oldState.backgroundColor = @newState.backgroundColor if @newState.placeholderText isnt @oldState.placeholderText @placeholderTextDiv?.remove() if @newState.placeholderText? @placeholderTextDiv = document.createElement('div') @placeholderTextDiv.classList.add('placeholder-text') @placeholderTextDiv.textContent = @newState.placeholderText @domNode.appendChild(@placeholderTextDiv) @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible @updateLineNodes() if @newState.scrollWidth isnt @oldState.scrollWidth @domNode.style.width = @newState.scrollWidth + 'px' @oldState.scrollWidth = @newState.scrollWidth @cursorsComponent.updateSync(state) @highlightsComponent.updateSync(state) @oldState.indentGuidesVisible = @newState.indentGuidesVisible @oldState.scrollWidth = @newState.scrollWidth removeLineNodes: -> @removeLineNode(id) for id of @oldState.lines return removeLineNode: (id) -> @lineNodesByLineId[id].remove() delete @lineNodesByLineId[id] delete @lineIdsByScreenRow[@screenRowsByLineId[id]] delete @screenRowsByLineId[id] delete @oldState.lines[id] updateLineNodes: -> 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] = cloneObject(lineState) return unless newLineIds? WrapperDiv.innerHTML = newLinesHTML newLineNodes = _.toArray(WrapperDiv.children) for id, i in newLineIds lineNode = newLineNodes[i] @lineNodesByLineId[id] = lineNode @domNode.appendChild(lineNode) return buildLineHTML: (id) -> {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) -> {indentGuidesVisible} = @newState {tokens, text, isOnlyWhitespace} = @newState.lines[id] innerHTML = "" scopeStack = [] for token in tokens innerHTML += @updateScopeStack(scopeStack, token.scopes) hasIndentGuide = indentGuidesVisible and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and isOnlyWhitespace)) 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) -> oldLineState = @oldState.lines[id] newLineState = @newState.lines[id] lineNode = @lineNodesByLineId[id] if @newState.scrollWidth isnt @oldState.scrollWidth lineNode.style.width = @newState.scrollWidth + 'px' newDecorationClasses = newLineState.decorationClasses oldDecorationClasses = oldLineState.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) oldLineState.decorationClasses = newLineState.decorationClasses if newLineState.top isnt oldLineState.top lineNode.style.top = newLineState.top + 'px' oldLineState.top = newLineState.cop if newLineState.screenRow isnt oldLineState.screenRow lineNode.dataset.screenRow = newLineState.screenRow oldLineState.screenRow = newLineState.screenRow @lineIdsByScreenRow[newLineState.screenRow] = id lineNodeForScreenRow: (screenRow) -> @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] measureLineHeightAndDefaultCharWidth: -> @domNode.appendChild(DummyLineNode) lineHeightInPixels = DummyLineNode.getBoundingClientRect().height charWidth = DummyLineNode.firstChild.getBoundingClientRect().width @domNode.removeChild(DummyLineNode) @presenter.setLineHeight(lineHeightInPixels) @presenter.setBaseCharacterWidth(charWidth) remeasureCharacterWidths: -> return unless @presenter.baseCharacterWidth @clearScopedCharWidths() @measureCharactersInNewLines() measureCharactersInNewLines: -> @presenter.batchCharacterMeasurement => for id, lineState of @oldState.lines unless @measuredLines.has(id) lineNode = @lineNodesByLineId[id] @measureCharactersInLine(id, lineState, lineNode) return measureCharactersInLine: (lineId, tokenizedLine, lineNode) -> rangeForMeasurement = null iterator = null charIndex = 0 for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens charWidths = @presenter.getScopedCharacterWidths(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 @presenter.setScopedCharacterWidth(scopes, char, charWidth) charIndex += charLength @measuredLines.add(lineId) clearScopedCharWidths: -> @measuredLines.clear() @presenter.clearScopedCharacterWidths()