text: /* global ResizeObserver */
  
  const etch = require('etch')
  const {Point, Range} = require('text-buffer')
  const LineTopIndex = require('line-top-index')
  const TextEditor = require('./text-editor')
  const {isPairedCharacter} = require('./text-utils')
  const electron = require('electron')
  const clipboard = electron.clipboard
  const $ = etch.dom
  
  let TextEditorElement
  
  const DEFAULT_ROWS_PER_TILE = 6
  const NORMAL_WIDTH_CHARACTER = 'x'
  const DOUBLE_WIDTH_CHARACTER = '我'
  const HALF_WIDTH_CHARACTER = 'ﾊ'
  const KOREAN_CHARACTER = '세'
  const NBSP_CHARACTER = '\u00a0'
  const ZERO_WIDTH_NBSP_CHARACTER = '\ufeff'
  const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40
  const CURSOR_BLINK_RESUME_DELAY = 300
  const CURSOR_BLINK_PERIOD = 800
  
  function scaleMouseDragAutoscrollDelta (delta) {
    return Math.pow(delta / 3, 3) / 280
  }
  
  module.exports =
  class TextEditorComponent {
    static setScheduler (scheduler) {
      etch.setScheduler(scheduler)
    }
  
    static getScheduler () {
      return etch.getScheduler()
    }
  
    static didUpdateStyles () {
      if (this.attachedComponents) {
        this.attachedComponents.forEach((component) => {
          component.didUpdateStyles()
        })
      }
    }
  
    static didUpdateScrollbarStyles () {
      if (this.attachedComponents) {
        this.attachedComponents.forEach((component) => {
          component.didUpdateScrollbarStyles()
        })
      }
    }
  
    constructor (props) {
      this.props = props
  
      if (!props.model) {
        props.model = new TextEditor({mini: props.mini, readOnly: props.readOnly})
      }
      this.props.model.component = this
  
      if (props.element) {
        this.element = props.element
      } else {
        if (!TextEditorElement) TextEditorElement = require('./text-editor-element')
        this.element = new TextEditorElement()
      }
      this.element.initialize(this)
      this.virtualNode = $('atom-text-editor')
      this.virtualNode.domNode = this.element
      this.refs = {}
  
      this.updateSync = this.updateSync.bind(this)
      this.didBlurHiddenInput = this.didBlurHiddenInput.bind(this)
      this.didFocusHiddenInput = this.didFocusHiddenInput.bind(this)
      this.didPaste = this.didPaste.bind(this)
      this.didTextInput = this.didTextInput.bind(this)
      this.didKeydown = this.didKeydown.bind(this)
      this.didKeyup = this.didKeyup.bind(this)
      this.didKeypress = this.didKeypress.bind(this)
      this.didCompositionStart = this.didCompositionStart.bind(this)
      this.didCompositionUpdate = this.didCompositionUpdate.bind(this)
      this.didCompositionEnd = this.didCompositionEnd.bind(this)
  
      this.updatedSynchronously = this.props.updatedSynchronously
      this.didScrollDummyScrollbar = this.didScrollDummyScrollbar.bind(this)
      this.didMouseDownOnContent = this.didMouseDownOnContent.bind(this)
      this.debouncedResumeCursorBlinking = debounce(
        this.resumeCursorBlinking.bind(this),
        (this.props.cursorBlinkResumeDelay || CURSOR_BLINK_RESUME_DELAY)
      )
      this.lineTopIndex = new LineTopIndex()
      this.lineNodesPool = new NodePool()
      this.updateScheduled = false
      this.suppressUpdates = false
      this.hasInitialMeasurements = false
      this.measurements = {
        lineHeight: 0,
        baseCharacterWidth: 0,
        doubleWidthCharacterWidth: 0,
        halfWidthCharacterWidth: 0,
        koreanCharacterWidth: 0,
        gutterContainerWidth: 0,
        lineNumberGutterWidth: 0,
        clientContainerHeight: 0,
        clientContainerWidth: 0,
        verticalScrollbarWidth: 0,
        horizontalScrollbarHeight: 0,
        longestLineWidth: 0
      }
      this.derivedDimensionsCache = {}
      this.visible = false
      this.cursorsBlinking = false
      this.cursorsBlinkedOff = false
      this.nextUpdateOnlyBlinksCursors = null
      this.linesToMeasure = new Map()
      this.extraRenderedScreenLines = new Map()
      this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure
      this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horizontal pixel positions
      this.blockDecorationsToMeasure = new Set()
      this.blockDecorationsByElement = new WeakMap()
      this.blockDecorationSentinel = document.createElement('div')
      this.blockDecorationSentinel.style.height = '1px'
      this.heightsByBlockDecoration = new WeakMap()
      this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this))
      this.lineComponentsByScreenLineId = new Map()
      this.overlayComponents = new Set()
      this.shouldRenderDummyScrollbars = true
      this.remeasureScrollbars = false
      this.pendingAutoscroll = null
      this.scrollTopPending = false
      this.scrollLeftPending = false
      this.scrollTop = 0
      this.scrollLeft = 0
      this.previousScrollWidth = 0
      this.previousScrollHeight = 0
      this.lastKeydown = null
      this.lastKeydownBeforeKeypress = null
      this.accentedCharacterMenuIsOpen = false
      this.remeasureGutterDimensions = false
      this.guttersToRender = [this.props.model.getLineNumberGutter()]
      this.guttersVisibility = [this.guttersToRender[0].visible]
      this.idsByTileStartRow = new Map()
      this.nextTileId = 0
      this.renderedTileStartRows = []
      this.showLineNumbers = this.props.model.doesShowLineNumbers()
      this.lineNumbersToRender = {
        maxDigits: 2,
        bufferRows: [],
        screenRows: [],
        keys: [],
        softWrappedFlags: [],
        foldableFlags: []
      }
      this.decorationsToRender = {
        lineNumbers: new Map(),
        lines: null,
        highlights: [],
        cursors: [],
        overlays: [],
        customGutter: new Map(),
        blocks: new Map(),
        text: []
      }
      this.decorationsToMeasure = {
        highlights: [],
        cursors: new Map()
      }
      this.textDecorationsByMarker = new Map()
      this.textDecorationBoundaries = []
      this.pendingScrollTopRow = this.props.initialScrollTopRow
      this.pendingScrollLeftColumn = this.props.initialScrollLeftColumn
      this.tabIndex = this.props.element && this.props.element.tabIndex ? this.props.element.tabIndex : -1
  
      this.measuredContent = false
      this.queryGuttersToRender()
      this.queryMaxLineNumberDigits()
      this.observeBlockDecorations()
      this.updateClassList()
      etch.updateSync(this)
    }
  
    update (props) {
      if (props.model !== this.props.model) {
        this.props.model.component = null
        props.model.component = this
      }
      this.props = props
      this.scheduleUpdate()
    }
  
    pixelPositionForScreenPosition ({row, column}) {
      const top = this.pixelPositionAfterBlocksForRow(row)
      let left = column === 0 ? 0 : this.pixelLeftForRowAndColumn(row, column)
      if (left == null) {
        this.requestHorizontalMeasurement(row, column)
        this.updateSync()
        left = this.pixelLeftForRowAndColumn(row, column)
      }
      return {top, left}
    }
  
    scheduleUpdate (nextUpdateOnlyBlinksCursors = false) {
      if (!this.visible) return
      if (this.suppressUpdates) return
  
      this.nextUpdateOnlyBlinksCursors =
        this.nextUpdateOnlyBlinksCursors !== false && nextUpdateOnlyBlinksCursors === true
  
      if (this.updatedSynchronously) {
        this.updateSync()
      } else if (!this.updateScheduled) {
        this.updateScheduled = true
        etch.getScheduler().updateDocument(() => {
          if (this.updateScheduled) this.updateSync(true)
        })
      }
    }
  
    updateSync (useScheduler = false) {
      // Don't proceed if we know we are not visible
      if (!this.visible) {
        this.updateScheduled = false
        return
      }
  
      // Don't proceed if we have to pay for a measurement anyway and detect
      // that we are no longer visible.
      if ((this.remeasureCharacterDimensions || this.remeasureAllBlockDecorations) && !this.isVisible()) {
        if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise()
        this.updateScheduled = false
        return
      }
  
      const onlyBlinkingCursors = this.nextUpdateOnlyBlinksCursors
      this.nextUpdateOnlyBlinksCursors = null
      if (useScheduler && onlyBlinkingCursors) {
        this.refs.cursorsAndInput.updateCursorBlinkSync(this.cursorsBlinkedOff)
        if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise()
        this.updateScheduled = false
        return
      }
  
      if (this.remeasureCharacterDimensions) {
        const originalLineHeight = this.getLineHeight()
        const originalBaseCharacterWidth = this.getBaseCharacterWidth()
        const scrollTopRow = this.getScrollTopRow()
        const scrollLeftColumn = this.getScrollLeftColumn()
  
        this.measureCharacterDimensions()
        this.measureGutterDimensions()
        this.queryLongestLine()
  
        if (this.getLineHeight() !== originalLineHeight) {
          this.setScrollTopRow(scrollTopRow)
        }
        if (this.getBaseCharacterWidth() !== originalBaseCharacterWidth) {
          this.setScrollLeftColumn(scrollLeftColumn)
        }
        this.remeasureCharacterDimensions = false
      }
  
      this.measureBlockDecorations()
  
      this.updateSyncBeforeMeasuringContent()
      if (useScheduler === true) {
        const scheduler = etch.getScheduler()
        scheduler.readDocument(() => {
          const restartFrame = this.measureContentDuringUpdateSync()
          scheduler.updateDocument(() => {
            if (restartFrame) {
              this.updateSync(true)
            } else {
              this.updateSyncAfterMeasuringContent()
            }
          })
        })
      } else {
        const restartFrame = this.measureContentDuringUpdateSync()
        if (restartFrame) {
          this.updateSync(false)
        } else {
          this.updateSyncAfterMeasuringContent()
        }
      }
  
      this.updateScheduled = false
    }
  
    measureBlockDecorations () {
      if (this.remeasureAllBlockDecorations) {
        this.remeasureAllBlockDecorations = false
  
        const decorations = this.props.model.getDecorations()
        for (var i = 0; i < decorations.length; i++) {
          const decoration = decorations[i]
          const marker = decoration.getMarker()
          if (marker.isValid() && decoration.getProperties().type === 'block') {
            this.blockDecorationsToMeasure.add(decoration)
          }
        }
  
        // Update the width of the line tiles to ensure block decorations are
        // measured with the most recent width.
        if (this.blockDecorationsToMeasure.size > 0) {
          this.updateSyncBeforeMeasuringContent()
        }
      }
  
      if (this.blockDecorationsToMeasure.size > 0) {
        const {blockDecorationMeasurementArea} = this.refs
        const sentinelElements = new Set()
  
        blockDecorationMeasurementArea.appendChild(document.createElement('div'))
        this.blockDecorationsToMeasure.forEach((decoration) => {
          const {item} = decoration.getProperties()
          const decorationElement = TextEditor.viewForItem(item)
          if (document.contains(decorationElement)) {
            const parentElement = decorationElement.parentElement
  
            if (!decorationElement.previousSibling) {
              const sentinelElement = this.blockDecorationSentinel.cloneNode()
              parentElement.insertBefore(sentinelElement, decorationElement)
              sentinelElements.add(sentinelElement)
            }
  
            if (!decorationElement.nextSibling) {
              const sentinelElement = this.blockDecorationSentinel.cloneNode()
              parentElement.appendChild(sentinelElement)
              sentinelElements.add(sentinelElement)
            }
  
            this.didMeasureVisibleBlockDecoration = true
          } else {
            blockDecorationMeasurementArea.appendChild(this.blockDecorationSentinel.cloneNode())
            blockDecorationMeasurementArea.appendChild(decorationElement)
            blockDecorationMeasurementArea.appendChild(this.blockDecorationSentinel.cloneNode())
          }
        })
  
        if (this.resizeBlockDecorationMeasurementsArea) {
          this.resizeBlockDecorationMeasurementsArea = false
          this.refs.blockDecorationMeasurementArea.style.width = this.getScrollWidth() + 'px'
        }
  
        this.blockDecorationsToMeasure.forEach((decoration) => {
          const {item} = decoration.getProperties()
          const decorationElement = TextEditor.viewForItem(item)
          const {previousSibling, nextSibling} = decorationElement
          const height = nextSibling.getBoundingClientRect().top - previousSibling.getBoundingClientRect().bottom
          this.heightsByBlockDecoration.set(decoration, height)
          this.lineTopIndex.resizeBlock(decoration, height)
        })
  
        sentinelElements.forEach((sentinelElement) => sentinelElement.remove())
        while (blockDecorationMeasurementArea.firstChild) {
          blockDecorationMeasurementArea.firstChild.remove()
        }
        this.blockDecorationsToMeasure.clear()
      }
    }
  
    updateSyncBeforeMeasuringContent () {
      this.measuredContent = false
      this.derivedDimensionsCache = {}
      this.updateModelSoftWrapColumn()
      if (this.pendingAutoscroll) {
        let {screenRange, options} = this.pendingAutoscroll
        this.autoscrollVertically(screenRange, options)
        this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column)
        this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column)
      }
      this.populateVisibleRowRange(this.getRenderedStartRow())
      this.populateVisibleTiles()
      this.queryScreenLinesToRender()
      this.queryLongestLine()
      this.queryLineNumbersToRender()
      this.queryGuttersToRender()
      this.queryDecorationsToRender()
      this.queryExtraScreenLinesToRender()
      this.shouldRenderDummyScrollbars = !this.remeasureScrollbars
      etch.updateSync(this)
      this.updateClassList()
      this.shouldRenderDummyScrollbars = true
      this.didMeasureVisibleBlockDecoration = false
    }
  
    measureContentDuringUpdateSync () {
      let gutterDimensionsChanged = false
      if (this.remeasureGutterDimensions) {
        gutterDimensionsChanged = this.measureGutterDimensions()
        this.remeasureGutterDimensions = false
      }
      const wasHorizontalScrollbarVisible = (
        this.canScrollHorizontally() &&
        this.getHorizontalScrollbarHeight() > 0
      )
  
      this.measureLongestLineWidth()
      this.measureHorizontalPositions()
      this.updateAbsolutePositionedDecorations()
  
      const isHorizontalScrollbarVisible = (
        this.canScrollHorizontally() &&
        this.getHorizontalScrollbarHeight() > 0
      )
  
      if (this.pendingAutoscroll) {
        this.derivedDimensionsCache = {}
        const {screenRange, options} = this.pendingAutoscroll
        this.autoscrollHorizontally(screenRange, options)
  
        if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) {
          this.autoscrollVertically(screenRange, options)
        }
        this.pendingAutoscroll = null
      }
  
      this.linesToMeasure.clear()
      this.measuredContent = true
  
      return gutterDimensionsChanged || wasHorizontalScrollbarVisible !== isHorizontalScrollbarVisible
    }
  
    updateSyncAfterMeasuringContent () {
      this.derivedDimensionsCache = {}
      etch.updateSync(this)
  
      this.currentFrameLineNumberGutterProps = null
      this.scrollTopPending = false
      this.scrollLeftPending = false
      if (this.remeasureScrollbars) {
        // Flush stored scroll positions to the vertical and the horizontal
        // scrollbars. This is because they have just been destroyed and recreated
        // as a result of their remeasurement, but we could not assign the scroll
        // top while they were initialized because they were not attached to the
        // DOM yet.
        this.refs.verticalScrollbar.flushScrollPosition()
        this.refs.horizontalScrollbar.flushScrollPosition()
  
        this.measureScrollbarDimensions()
        this.remeasureScrollbars = false
        etch.updateSync(this)
      }
  
      this.derivedDimensionsCache = {}
      if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise()
    }
  
    render () {
      const {model} = this.props
      const style = {}
  
      if (!model.getAutoHeight() && !model.getAutoWidth()) {
        style.contain = 'size'
      }
  
      let clientContainerHeight = '100%'
      let clientContainerWidth = '100%'
      if (this.hasInitialMeasurements) {
        if (model.getAutoHeight()) {
          clientContainerHeight =
            this.getContentHeight() +
            this.getHorizontalScrollbarHeight() +
            'px'
        }
        if (model.getAutoWidth()) {
          style.width = 'min-content'
          clientContainerWidth =
            this.getGutterContainerWidth() +
            this.getContentWidth() +
            this.getVerticalScrollbarWidth() +
            'px'
        } else {
          style.width = this.element.style.width
        }
      }
  
      let attributes = {}
      if (model.isMini()) {
        attributes.mini = ''
      }
  
      if (model.isReadOnly()) {
        attributes.readonly = ''
      }
  
      const dataset = {encoding: model.getEncoding()}
      const grammar = model.getGrammar()
      if (grammar && grammar.scopeName) {
        dataset.grammar = grammar.scopeName.replace(/\./g, ' ')
      }
  
      return $('atom-text-editor',
        {
          // See this.updateClassList() for construction of the class name
          style,
          attributes,
          dataset,
          tabIndex: -1,
          on: {mousewheel: this.didMouseWheel}
        },
        $.div(
          {
            ref: 'clientContainer',
            style: {
              position: 'relative',
              contain: 'strict',
              overflow: 'hidden',
              backgroundColor: 'inherit',
              height: clientContainerHeight,
              width: clientContainerWidth
            }
          },
          this.renderGutterContainer(),
          this.renderScrollContainer()
        ),
        this.renderOverlayDecorations()
      )
    }
  
    renderGutterContainer () {
      if (this.props.model.isMini()) {
        return null
      } else {
        return $(GutterContainerComponent, {
          ref: 'gutterContainer',
          key: 'gutterContainer',
          rootComponent: this,
          hasInitialMeasurements: this.hasInitialMeasurements,
          measuredContent: this.measuredContent,
          scrollTop: this.getScrollTop(),
          scrollHeight: this.getScrollHeight(),
          lineNumberGutterWidth: this.getLineNumberGutterWidth(),
          lineHeight: this.getLineHeight(),
          renderedStartRow: this.getRenderedStartRow(),
          renderedEndRow: this.getRenderedEndRow(),
          rowsPerTile: this.getRowsPerTile(),
          guttersToRender: this.guttersToRender,
          decorationsToRender: this.decorationsToRender,
          isLineNumberGutterVisible: this.props.model.isLineNumberGutterVisible(),
          showLineNumbers: this.showLineNumbers,
          lineNumbersToRender: this.lineNumbersToRender,
          didMeasureVisibleBlockDecoration: this.didMeasureVisibleBlockDecoration
        })
      }
    }
  
    renderScrollContainer () {
      const style = {
        position: 'absolute',
        contain: 'strict',
        overflow: 'hidden',
        top: 0,
        bottom: 0,
        backgroundColor: 'inherit'
      }
  
      if (this.hasInitialMeasurements) {
        style.left = this.getGutterContainerWidth() + 'px'
        style.width = this.getScrollContainerWidth() + 'px'
      }
  
      return $.div(
        {
          ref: 'scrollContainer',
          key: 'scrollContainer',
          className: 'scroll-view',
          style
        },
        this.renderContent(),
        this.renderDummyScrollbars()
      )
    }
  
    renderContent () {
      let style = {
        contain: 'strict',
        overflow: 'hidden',
        backgroundColor: 'inherit'
      }
      if (this.hasInitialMeasurements) {
        style.width = ceilToPhysicalPixelBoundary(this.getScrollWidth()) + 'px'
        style.height = ceilToPhysicalPixelBoundary(this.getScrollHeight()) + 'px'
        style.willChange = 'transform'
        style.transform = `translate(${-roundToPhysicalPixelBoundary(this.getScrollLeft())}px, ${-roundToPhysicalPixelBoundary(this.getScrollTop())}px)`
      }
  
      return $.div(
        {
          ref: 'content',
          on: {mousedown: this.didMouseDownOnContent},
          style
        },
        this.renderLineTiles(),
        this.renderBlockDecorationMeasurementArea(),
        this.renderCharacterMeasurementLine()
      )
    }
  
    renderHighlightDecorations () {
      return $(HighlightsComponent, {
        hasInitialMeasurements: this.hasInitialMeasurements,
        highlightDecorations: this.decorationsToRender.highlights.slice(),
        width: this.getScrollWidth(),
        height: this.getScrollHeight(),
        lineHeight: this.getLineHeight()
      })
    }
  
    renderLineTiles () {
      const style = {
        position: 'absolute',
        contain: 'strict',
        overflow: 'hidden'
      }
  
      const children = []
      children.push(this.renderHighlightDecorations())
  
      if (this.hasInitialMeasurements) {
        const {lineComponentsByScreenLineId} = this
  
        const startRow = this.getRenderedStartRow()
        const endRow = this.getRenderedEndRow()
        const rowsPerTile = this.getRowsPerTile()
        const tileWidth = this.getScrollWidth()
  
        for (let i = 0; i < this.renderedTileStartRows.length; i++) {
          const tileStartRow = this.renderedTileStartRows[i]
          const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile)
          const tileHeight = this.pixelPositionBeforeBlocksForRow(tileEndRow) - this.pixelPositionBeforeBlocksForRow(tileStartRow)
  
          children.push($(LinesTileComponent, {
            key: this.idsByTileStartRow.get(tileStartRow),
            measuredContent: this.measuredContent,
            height: tileHeight,
            width: tileWidth,
            top: this.pixelPositionBeforeBlocksForRow(tileStartRow),
            lineHeight: this.getLineHeight(),
            renderedStartRow: startRow,
            tileStartRow,
            tileEndRow,
            screenLines: this.renderedScreenLines.slice(tileStartRow - startRow, tileEndRow - startRow),
            lineDecorations: this.decorationsToRender.lines.slice(tileStartRow - startRow, tileEndRow - startRow),
            textDecorations: this.decorationsToRender.text.slice(tileStartRow - startRow, tileEndRow - startRow),
            blockDecorations: this.decorationsToRender.blocks.get(tileStartRow),
            displayLayer: this.props.model.displayLayer,
            nodePool: this.lineNodesPool,
            lineComponentsByScreenLineId
          }))
        }
  
        this.extraRenderedScreenLines.forEach((screenLine, screenRow) => {
          if (screenRow < startRow || screenRow >= endRow) {
            children.push($(LineComponent, {
              key: 'extra-' + screenLine.id,
              offScreen: true,
              screenLine,
              screenRow,
              displayLayer: this.props.model.displayLayer,
              nodePool: this.lineNodesPool,
              lineComponentsByScreenLineId
            }))
          }
        })
  
        style.width = this.getScrollWidth() + 'px'
        style.height = this.getScrollHeight() + 'px'
      }
  
      children.push(this.renderPlaceholderText())
      children.push(this.renderCursorsAndInput())
  
      return $.div(
        {key: 'lineTiles', ref: 'lineTiles', className: 'lines', style},
        children
      )
    }
  
    renderCursorsAndInput () {
      return $(CursorsAndInputComponent, {
        ref: 'cursorsAndInput',
        key: 'cursorsAndInput',
        didBlurHiddenInput: this.didBlurHiddenInput,
        didFocusHiddenInput: this.didFocusHiddenInput,
        didTextInput: this.didTextInput,
        didPaste: this.didPaste,
        didKeydown: this.didKeydown,
        didKeyup: this.didKeyup,
        didKeypress: this.didKeypress,
        didCompositionStart: this.didCompositionStart,
        didCompositionUpdate: this.didCompositionUpdate,
        didCompositionEnd: this.didCompositionEnd,
        measuredContent: this.measuredContent,
        lineHeight: this.getLineHeight(),
        scrollHeight: this.getScrollHeight(),
        scrollWidth: this.getScrollWidth(),
        decorationsToRender: this.decorationsToRender,
        cursorsBlinkedOff: this.cursorsBlinkedOff,
        hiddenInputPosition: this.hiddenInputPosition,
        tabIndex: this.tabIndex
      })
    }
  
    renderPlaceholderText () {
      const {model} = this.props
      if (model.isEmpty()) {
        const placeholderText = model.getPlaceholderText()
        if (placeholderText != null) {
          return $.div({className: 'placeholder-text'}, placeholderText)
        }
      }
      return null
    }
  
    renderCharacterMeasurementLine () {
      return $.div(
        {
          key: 'characterMeasurementLine',
          ref: 'characterMeasurementLine',
          className: 'line dummy',
          style: {position: 'absolute', visibility: 'hidden'}
        },
        $.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER),
        $.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER),
        $.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER),
        $.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER)
      )
    }
  
    renderBlockDecorationMeasurementArea () {
      return $.div({
        ref: 'blockDecorationMeasurementArea',
        key: 'blockDecorationMeasurementArea',
        style: {
          contain: 'strict',
          position: 'absolute',
          visibility: 'hidden',
          width: this.getScrollWidth() + 'px'
        }
      })
    }
  
    renderDummyScrollbars () {
      if (this.shouldRenderDummyScrollbars && !this.props.model.isMini()) {
        let scrollHeight, scrollTop, horizontalScrollbarHeight
        let scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible
        let canScrollHorizontally, canScrollVertically
  
        if (this.hasInitialMeasurements) {
          scrollHeight = this.getScrollHeight()
          scrollWidth = this.getScrollWidth()
          scrollTop = this.getScrollTop()
          scrollLeft = this.getScrollLeft()
          canScrollHorizontally = this.canScrollHorizontally()
          canScrollVertically = this.canScrollVertically()
          horizontalScrollbarHeight = this.getHorizontalScrollbarHeight()
          verticalScrollbarWidth = this.getVerticalScrollbarWidth()
          forceScrollbarVisible = this.remeasureScrollbars
        } else {
          forceScrollbarVisible = true
        }
  
        return [
          $(DummyScrollbarComponent, {
            ref: 'verticalScrollbar',
            orientation: 'vertical',
            didScroll: this.didScrollDummyScrollbar,
            didMouseDown: this.didMouseDownOnContent,
            canScroll: canScrollVertically,
            scrollHeight,
            scrollTop,
            horizontalScrollbarHeight,
            forceScrollbarVisible
          }),
          $(DummyScrollbarComponent, {
            ref: 'horizontalScrollbar',
            orientation: 'horizontal',
            didScroll: this.didScrollDummyScrollbar,
            didMouseDown: this.didMouseDownOnContent,
            canScroll: canScrollHorizontally,
            scrollWidth,
            scrollLeft,
            verticalScrollbarWidth,
            forceScrollbarVisible
          }),
  
          // Force a "corner" to render where the two scrollbars meet at the lower right
          $.div(
            {
              ref: 'scrollbarCorner',
              className: 'scrollbar-corner',
              style: {
                position: 'absolute',
                height: '20px',
                width: '20px',
                bottom: 0,
                right: 0,
                overflow: 'scroll'
              }
            }
          )
        ]
      } else {
        return null
      }
    }
  
    renderOverlayDecorations () {
      return this.decorationsToRender.overlays.map((overlayProps) =>
        $(OverlayComponent, Object.assign(
          {
            key: overlayProps.element,
            overlayComponents: this.overlayComponents,
            didResize: (overlayComponent) => {
              this.updateOverlayToRender(overlayProps)
              overlayComponent.update(overlayProps)
            }
          },
          overlayProps
        ))
      )
    }
  
    // Imperatively manipulate the class list of the root element to avoid
    // clearing classes assigned by package authors.
    updateClassList () {
      const {model} = this.props
  
      const oldClassList = this.classList
      const newClassList = ['editor']
      if (this.focused) newClassList.push('is-focused')
      if (model.isMini()) newClassList.push('mini')
      for (var i = 0; i < model.selections.length; i++) {
        if (!model.selections[i].isEmpty()) {
          newClassList.push('has-selection')
          break
        }
      }
  
      if (oldClassList) {
        for (let i = 0; i < oldClassList.length; i++) {
          const className = oldClassList[i]
          if (!newClassList.includes(className)) {
            this.element.classList.remove(className)
          }
        }
      }
  
      for (let i = 0; i < newClassList.length; i++) {
        this.element.classList.add(newClassList[i])
      }
  
      this.classList = newClassList
    }
  
    queryScreenLinesToRender () {
      const {model} = this.props
  
      this.renderedScreenLines = model.displayLayer.getScreenLines(
        this.getRenderedStartRow(),
        this.getRenderedEndRow()
      )
    }
  
    queryLongestLine () {
      const {model} = this.props
  
      const longestLineRow = model.getApproximateLongestScreenRow()
      const longestLine = model.screenLineForScreenRow(longestLineRow)
      if (longestLine !== this.previousLongestLine || this.remeasureCharacterDimensions) {
        this.requestLineToMeasure(longestLineRow, longestLine)
        this.longestLineToMeasure = longestLine
        this.previousLongestLine = longestLine
      }
    }
  
    queryExtraScreenLinesToRender () {
      this.extraRenderedScreenLines.clear()
      this.linesToMeasure.forEach((screenLine, row) => {
        if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) {
          this.extraRenderedScreenLines.set(row, screenLine)
        }
      })
    }
  
    queryLineNumbersToRender () {
      const {model} = this.props
      if (!model.anyLineNumberGutterVisible()) return
      if (this.showLineNumbers !== model.doesShowLineNumbers()) {
        this.remeasureGutterDimensions = true
        this.showLineNumbers = model.doesShowLineNumbers()
      }
  
      this.queryMaxLineNumberDigits()
  
      const startRow = this.getRenderedStartRow()
      const endRow = this.getRenderedEndRow()
      const renderedRowCount = this.getRenderedRowCount()
  
      const bufferRows = model.bufferRowsForScreenRows(startRow, endRow)
      const screenRows = new Array(renderedRowCount)
      const keys = new Array(renderedRowCount)
      const foldableFlags = new Array(renderedRowCount)
      const softWrappedFlags = new Array(renderedRowCount)
  
      let previousBufferRow = (startRow > 0) ? model.bufferRowForScreenRow(startRow - 1) : -1
      let softWrapCount = 0
      for (let row = startRow; row < endRow; row++) {
        const i = row - startRow
        const bufferRow = bufferRows[i]
        if (bufferRow === previousBufferRow) {
          softWrapCount++
          softWrappedFlags[i] = true
          keys[i] = bufferRow + '-' + softWrapCount
        } else {
          softWrapCount = 0
          softWrappedFlags[i] = false
          keys[i] = bufferRow
        }
  
        const nextBufferRow = bufferRows[i + 1]
        if (bufferRow !== nextBufferRow) {
          foldableFlags[i] = model.isFoldableAtBufferRow(bufferRow)
        } else {
          foldableFlags[i] = false
        }
  
        screenRows[i] = row
        previousBufferRow = bufferRow
      }
  
      // Delete extra buffer row at the end because it's not currently on screen.
      bufferRows.pop()
  
      this.lineNumbersToRender.bufferRows = bufferRows
      this.lineNumbersToRender.screenRows = screenRows
      this.lineNumbersToRender.keys = keys
      this.lineNumbersToRender.foldableFlags = foldableFlags
      this.lineNumbersToRender.softWrappedFlags = softWrappedFlags
    }
  
    queryMaxLineNumberDigits () {
      const {model} = this.props
      if (model.anyLineNumberGutterVisible()) {
        const maxDigits = Math.max(2, model.getLineCount().toString().length)
        if (maxDigits !== this.lineNumbersToRender.maxDigits) {
          this.remeasureGutterDimensions = true
          this.lineNumbersToRender.maxDigits = maxDigits
        }
      }
    }
  
    renderedScreenLineForRow (row) {
      return (
        this.renderedScreenLines[row - this.getRenderedStartRow()] ||
        this.extraRenderedScreenLines.get(row)
      )
    }
  
    queryGuttersToRender () {
      const oldGuttersToRender = this.guttersToRender
      const oldGuttersVisibility = this.guttersVisibility
      this.guttersToRender = this.props.model.getGutters()
      this.guttersVisibility = this.guttersToRender.map(g => g.visible)
  
      if (!oldGuttersToRender || oldGuttersToRender.length !== this.guttersToRender.length) {
        this.remeasureGutterDimensions = true
      } else {
        for (let i = 0, length = this.guttersToRender.length; i < length; i++) {
          if (this.guttersToRender[i] !== oldGuttersToRender[i] || this.guttersVisibility[i] !== oldGuttersVisibility[i]) {
            this.remeasureGutterDimensions = true
            break
          }
        }
      }
    }
  
    queryDecorationsToRender () {
      this.decorationsToRender.lineNumbers.clear()
      this.decorationsToRender.lines = []
      this.decorationsToRender.overlays.length = 0
      this.decorationsToRender.customGutter.clear()
      this.decorationsToRender.blocks = new Map()
      this.decorationsToRender.text = []
      this.decorationsToMeasure.highlights.length = 0
      this.decorationsToMeasure.cursors.clear()
      this.textDecorationsByMarker.clear()
      this.textDecorationBoundaries.length = 0
  
      const decorationsByMarker =
        this.props.model.decorationManager.decorationPropertiesByMarkerForScreenRowRange(
          this.getRenderedStartRow(),
          this.getRenderedEndRow()
        )
  
      decorationsByMarker.forEach((decorations, marker) => {
        const screenRange = marker.getScreenRange()
        const reversed = marker.isReversed()
        for (let i = 0; i < decorations.length; i++) {
          const decoration = decorations[i]
          this.addDecorationToRender(decoration.type, decoration, marker, screenRange, reversed)
        }
      })
  
      this.populateTextDecorationsToRender()
    }
  
    addDecorationToRender (type, decoration, marker, screenRange, reversed) {
      if (Array.isArray(type)) {
        for (let i = 0, length = type.length; i < length; i++) {
          this.addDecorationToRender(type[i], decoration, marker, screenRange, reversed)
        }
      } else {
        switch (type) {
          case 'line':
          case 'line-number':
            this.addLineDecorationToRender(type, decoration, screenRange, reversed)
            break
          case 'highlight':
            this.addHighlightDecorationToMeasure(decoration, screenRange, marker.id)
            break
          case 'cursor':
            this.addCursorDecorationToMeasure(decoration, marker, screenRange, reversed)
            break
          case 'overlay':
            this.addOverlayDecorationToRender(decoration, marker)
            break
          case 'gutter':
            this.addCustomGutterDecorationToRender(decoration, screenRange)
            break
          case 'block':
            this.addBlockDecorationToRender(decoration, screenRange, reversed)
            break
          case 'text':
            this.addTextDecorationToRender(decoration, screenRange, marker)
            break
        }
      }
    }
  
    addLineDecorationToRender (type, decoration, screenRange, reversed) {
      let decorationsToRender
      if (type === 'line') {
        decorationsToRender = this.decorationsToRender.lines
      } else {
        const gutterName = decoration.gutterName || 'line-number'
        decorationsToRender = this.decorationsToRender.lineNumbers.get(gutterName)
        if (!decorationsToRender) {
          decorationsToRender = []
          this.decorationsToRender.lineNumbers.set(gutterName, decorationsToRender)
        }
      }
  
      let omitLastRow = false
      if (screenRange.isEmpty()) {
        if (decoration.onlyNonEmpty) return
      } else {
        if (decoration.onlyEmpty) return
        if (decoration.omitEmptyLastRow !== false) {
          omitLastRow = screenRange.end.column === 0
        }
      }
  
      const renderedStartRow = this.getRenderedStartRow()
      let rangeStartRow = screenRange.start.row
      let rangeEndRow = screenRange.end.row
  
      if (decoration.onlyHead) {
        if (reversed) {
          rangeEndRow = rangeStartRow
        } else {
          rangeStartRow = rangeEndRow
        }
      }
  
      rangeStartRow = Math.max(rangeStartRow, this.getRenderedStartRow())
      rangeEndRow = Math.min(rangeEndRow, this.getRenderedEndRow() - 1)
  
      for (let row = rangeStartRow; row <= rangeEndRow; row++) {
        if (omitLastRow && row === screenRange.end.row) break
        const currentClassName = decorationsToRender[row - renderedStartRow]
        const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class
        decorationsToRender[row - renderedStartRow] = newClassName
      }
    }
  
    addHighlightDecorationToMeasure (decoration, screenRange, key) {
      screenRange = constrainRangeToRows(screenRange, this.getRenderedStartRow(), this.getRenderedEndRow())
      if (screenRange.isEmpty()) return
  
      const {class: className, flashRequested, flashClass, flashDuration} = decoration
      decoration.flashRequested = false
      this.decorationsToMeasure.highlights.push({
        screenRange,
        key,
        className,
        flashRequested,
        flashClass,
        flashDuration
      })
      this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column)
      this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column)
    }
  
    addCursorDecorationToMeasure (decoration, marker, screenRange, reversed) {
      const {model} = this.props
      if (!model.getShowCursorOnSelection() && !screenRange.isEmpty()) return
  
      let decorationToMeasure = this.decorationsToMeasure.cursors.get(marker)
      if (!decorationToMeasure) {
        const isLastCursor = model.getLastCursor().getMarker() === marker
        const screenPosition = reversed ? screenRange.start : screenRange.end
        const {row, column} = screenPosition
  
        if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) return
  
        this.requestHorizontalMeasurement(row, column)
        let columnWidth = 0
        if (model.lineLengthForScreenRow(row) > column) {
          columnWidth = 1
          this.requestHorizontalMeasurement(row, column + 1)
        }
        decorationToMeasure = {screenPosition, columnWidth, isLastCursor}
        this.decorationsToMeasure.cursors.set(marker, decorationToMeasure)
      }
  
      if (decoration.class) {
        if (decorationToMeasure.className) {
          decorationToMeasure.className += ' ' + decoration.class
        } else {
          decorationToMeasure.className = decoration.class
        }
      }
  
      if (decoration.style) {
        if (decorationToMeasure.style) {
          Object.assign(decorationToMeasure.style, decoration.style)
        } else {
          decorationToMeasure.style = Object.assign({}, decoration.style)
        }
      }
    }
  
    addOverlayDecorationToRender (decoration, marker) {
      const {class: className, item, position, avoidOverflow} = decoration
      const element = TextEditor.viewForItem(item)
      const screenPosition = (position === 'tail')
        ? marker.getTailScreenPosition()
        : marker.getHeadScreenPosition()
  
      this.requestHorizontalMeasurement(screenPosition.row, screenPosition.column)
      this.decorationsToRender.overlays.push({className, element, avoidOverflow, screenPosition})
    }
  
    addCustomGutterDecorationToRender (decoration, screenRange) {
      let decorations = this.decorationsToRender.customGutter.get(decoration.gutterName)
      if (!decorations) {
        decorations = []
        this.decorationsToRender.customGutter.set(decoration.gutterName, decorations)
      }
      const top = this.pixelPositionAfterBlocksForRow(screenRange.start.row)
      const height = this.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1) - top
  
      decorations.push({
        className: 'decoration' + (decoration.class ? ' ' + decoration.class : ''),
        element: TextEditor.viewForItem(decoration.item),
        top,
        height
      })
    }
  
    addBlockDecorationToRender (decoration, screenRange, reversed) {
      const {row} = reversed ? screenRange.start : screenRange.end
      if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) return
  
      const tileStartRow = this.tileStartRowForRow(row)
      const screenLine = this.renderedScreenLines[row - this.getRenderedStartRow()]
  
      let decorationsByScreenLine = this.decorationsToRender.blocks.get(tileStartRow)
      if (!decorationsByScreenLine) {
        decorationsByScreenLine = new Map()
        this.decorationsToRender.blocks.set(tileStartRow, decorationsByScreenLine)
      }
  
      let decorations = decorationsByScreenLine.get(screenLine.id)
      if (!decorations) {
        decorations = []
        decorationsByScreenLine.set(screenLine.id, decorations)
      }
      decorations.push(decoration)
  
      // Order block decorations by increasing values of their "order" property. Break ties with "id", which mirrors
      // their creation sequence.
      decorations.sort((a, b) => a.order !== b.order ? a.order - b.order : a.id - b.id)
    }
  
    addTextDecorationToRender (decoration, screenRange, marker) {
      if (screenRange.isEmpty()) return
  
      let decorationsForMarker = this.textDecorationsByMarker.get(marker)
      if (!decorationsForMarker) {
        decorationsForMarker = []
        this.textDecorationsByMarker.set(marker, decorationsForMarker)
        this.textDecorationBoundaries.push({position: screenRange.start, starting: [marker]})
        this.textDecorationBoundaries.push({position: screenRange.end, ending: [marker]})
      }
      decorationsForMarker.push(decoration)
    }
  
    populateTextDecorationsToRender () {
      // Sort all boundaries in ascending order of position
      this.textDecorationBoundaries.sort((a, b) => a.position.compare(b.position))
  
      // Combine adjacent boundaries with the same position
      for (let i = 0; i < this.textDecorationBoundaries.length;) {
        const boundary = this.textDecorationBoundaries[i]
        const nextBoundary = this.textDecorationBoundaries[i + 1]
        if (nextBoundary && nextBoundary.position.isEqual(boundary.position)) {
          if (nextBoundary.starting) {
            if (boundary.starting) {
              boundary.starting.push(...nextBoundary.starting)
            } else {
              boundary.starting = nextBoundary.starting
            }
          }
  
          if (nextBoundary.ending) {
            if (boundary.ending) {
              boundary.ending.push(...nextBoundary.ending)
            } else {
              boundary.ending = nextBoundary.ending
            }
          }
  
          this.textDecorationBoundaries.splice(i + 1, 1)
        } else {
          i++
        }
      }
  
      const renderedStartRow = this.getRenderedStartRow()
      const renderedEndRow = this.getRenderedEndRow()
      const containingMarkers = []
  
      // Iterate over boundaries to build up text decorations.
      for (let i = 0; i < this.textDecorationBoundaries.length; i++) {
        const boundary = this.textDecorationBoundaries[i]
  
        // If multiple markers start here, sort them by order of nesting (markers ending later come first)
        if (boundary.starting && boundary.starting.length > 1) {
          boundary.starting.sort((a, b) => a.compare(b))
        }
  
        // If multiple markers start here, sort them by order of nesting (markers starting earlier come first)
        if (boundary.ending && boundary.ending.length > 1) {
          boundary.ending.sort((a, b) => b.compare(a))
        }
  
        // Remove markers ending here from containing markers array
        if (boundary.ending) {
          for (let j = boundary.ending.length - 1; j >= 0; j--) {
            containingMarkers.splice(containingMarkers.lastIndexOf(boundary.ending[j]), 1)
          }
        }
        // Add markers starting here to containing markers array
        if (boundary.starting) containingMarkers.push(...boundary.starting)
  
        // Determine desired className and style based on containing markers
        let className, style
        for (let j = 0; j < containingMarkers.length; j++) {
          const marker = containingMarkers[j]
          const decorations = this.textDecorationsByMarker.get(marker)
          for (let k = 0; k < decorations.length; k++) {
            const decoration = decorations[k]
            if (decoration.class) {
              if (className) {
                className += ' ' + decoration.class
              } else {
                className = decoration.class
              }
            }
            if (decoration.style) {
              if (style) {
                Object.assign(style, decoration.style)
              } else {
                style = Object.assign({}, decoration.style)
              }
            }
          }
        }
  
        // Add decoration start with className/style for current position's column,
        // and also for the start of every row up until the next decoration boundary
        if (boundary.position.row >= renderedStartRow) {
          this.addTextDecorationStart(boundary.position.row, boundary.position.column, className, style)
        }
        const nextBoundary = this.textDecorationBoundaries[i + 1]
        if (nextBoundary) {
          let row = Math.max(boundary.position.row + 1, renderedStartRow)
          const endRow = Math.min(nextBoundary.position.row, renderedEndRow)
          for (; row < endRow; row++) {
            this.addTextDecorationStart(row, 0, className, style)
          }
  
          if (row === nextBoundary.position.row && nextBoundary.position.column !== 0) {
            this.addTextDecorationStart(row, 0, className, style)
          }
        }
      }
    }
  
    addTextDecorationStart (row, column, className, style) {
      const renderedStartRow = this.getRenderedStartRow()
      let decorationStarts = this.decorationsToRender.text[row - renderedStartRow]
      if (!decorationStarts) {
        decorationStarts = []
        this.decorationsToRender.text[row - renderedStartRow] = decorationStarts
      }
      decorationStarts.push({column, className, style})
    }
  
    updateAbsolutePositionedDecorations () {
      this.updateHighlightsToRender()
      this.updateCursorsToRender()
      this.updateOverlaysToRender()
    }
  
    updateHighlightsToRender () {
      this.decorationsToRender.highlights.length = 0
      for (let i = 0; i < this.decorationsToMeasure.highlights.length; i++) {
        const highlight = this.decorationsToMeasure.highlights[i]
        const {start, end} = highlight.screenRange
        highlight.startPixelTop = this.pixelPositionAfterBlocksForRow(start.row)
        highlight.startPixelLeft = this.pixelLeftForRowAndColumn(start.row, start.column)
        highlight.endPixelTop = this.pixelPositionAfterBlocksForRow(end.row) + this.getLineHeight()
        highlight.endPixelLeft = this.pixelLeftForRowAndColumn(end.row, end.column)
        this.decorationsToRender.highlights.push(highlight)
      }
    }
  
    updateCursorsToRender () {
      this.decorationsToRender.cursors.length = 0
  
      this.decorationsToMeasure.cursors.forEach((cursor) => {
        const {screenPosition, className, style} = cursor
        const {row, column} = screenPosition
  
        const pixelTop = this.pixelPositionAfterBlocksForRow(row)
        const pixelLeft = this.pixelLeftForRowAndColumn(row, column)
        let pixelWidth
        if (cursor.columnWidth === 0) {
          pixelWidth = this.getBaseCharacterWidth()
        } else {
          pixelWidth = this.pixelLeftForRowAndColumn(row, column + 1) - pixelLeft
        }
  
        const cursorPosition = {pixelTop, pixelLeft, pixelWidth, className, style}
        this.decorationsToRender.cursors.push(cursorPosition)
        if (cursor.isLastCursor) this.hiddenInputPosition = cursorPosition
      })
    }
  
    updateOverlayToRender (decoration) {
      const windowInnerHeight = this.getWindowInnerHeight()
      const windowInnerWidth = this.getWindowInnerWidth()
      const contentClientRect = this.refs.content.getBoundingClientRect()
  
      const {element, screenPosition, avoidOverflow} = decoration
      const {row, column} = screenPosition
      let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight()
      let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
      const clientRect = element.getBoundingClientRect()
  
      if (avoidOverflow !== false) {
        const computedStyle = window.getComputedStyle(element)
        const elementTop = wrapperTop + parseInt(computedStyle.marginTop)
        const elementBottom = elementTop + clientRect.height
        const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom)
        const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft)
        const elementRight = elementLeft + clientRect.width
  
        if (elementBottom > windowInnerHeight && flippedElementTop >= 0) {
          wrapperTop -= (elementTop - flippedElementTop)
        }
        if (elementLeft < 0) {
          wrapperLeft -= elementLeft
        } else if (elementRight > windowInnerWidth) {
          wrapperLeft -= (elementRight - windowInnerWidth)
        }
      }
  
      decoration.pixelTop = Math.round(wrapperTop)
      decoration.pixelLeft = Math.round(wrapperLeft)
    }
  
    updateOverlaysToRender () {
      const overlayCount = this.decorationsToRender.overlays.length
      if (overlayCount === 0) return null
  
      for (let i = 0; i < overlayCount; i++) {
        const decoration = this.decorationsToRender.overlays[i]
        this.updateOverlayToRender(decoration)
      }
    }
  
    didAttach () {
      if (!this.attached) {
        this.attached = true
        this.intersectionObserver = new IntersectionObserver((entries) => {
          const {intersectionRect} = entries[entries.length - 1]
          if (intersectionRect.width > 0 || intersectionRect.height > 0) {
            this.didShow()
          } else {
            this.didHide()
          }
        })
        this.intersectionObserver.observe(this.element)
  
        this.resizeObserver = new ResizeObserver(this.didResize.bind(this))
        this.resizeObserver.observe(this.element)
  
        if (this.refs.gutterContainer) {
          this.gutterContainerResizeObserver = new ResizeObserver(this.didResizeGutterContainer.bind(this))
          this.gutterContainerResizeObserver.observe(this.refs.gutterContainer.element)
        }
  
        this.overlayComponents.forEach((component) => component.didAttach())
  
        if (this.isVisible()) {
          this.didShow()
  
          if (this.refs.verticalScrollbar) this.refs.verticalScrollbar.flushScrollPosition()
          if (this.refs.horizontalScrollbar) this.refs.horizontalScrollbar.flushScrollPosition()
        } else {
          this.didHide()
        }
        if (!this.constructor.attachedComponents) {
          this.constructor.attachedComponents = new Set()
        }
        this.constructor.attachedComponents.add(this)
      }
    }
  
    didDetach () {
      if (this.attached) {
        this.intersectionObserver.disconnect()
        this.resizeObserver.disconnect()
        if (this.gutterContainerResizeObserver) this.gutterContainerResizeObserver.disconnect()
        this.overlayComponents.forEach((component) => component.didDetach())
  
        this.didHide()
        this.attached = false
        this.constructor.attachedComponents.delete(this)
      }
    }
  
    didShow () {
      if (!this.visible && this.isVisible()) {
        if (!this.hasInitialMeasurements) this.measureDimensions()
        this.visible = true
        this.props.model.setVisible(true)
        this.resizeBlockDecorationMeasurementsArea = true
        this.updateSync()
        this.flushPendingLogicalScrollPosition()
      }
    }
  
    didHide () {
      if (this.visible) {
        this.visible = false
        this.props.model.setVisible(false)
      }
    }
  
    // Called by TextEditorElement so that focus events can be handled before
    // the element is attached to the DOM.
    didFocus () {
      // This element can be focused from a parent custom element's
      // attachedCallback before *its* attachedCallback is fired. This protects
      // against that case.
      if (!this.attached) this.didAttach()
  
      // The element can be focused before the intersection observer detects that
      // it has been shown for the first time. If this element is being focused,
      // it is necessarily visible, so we call `didShow` to ensure the hidden
      // input is rendered before we try to shift focus to it.
      if (!this.visible) this.didShow()
  
      if (!this.focused) {
        this.focused = true
        this.startCursorBlinking()
        this.scheduleUpdate()
      }
  
      this.getHiddenInput().focus()
    }
  
    // Called by TextEditorElement so that this function is always the first
    // listener to be fired, even if other listeners are bound before creating
    // the component.
    didBlur (event) {
      if (event.relatedTarget === this.getHiddenInput()) {
        event.stopImmediatePropagation()
      }
    }
  
    didBlurHiddenInput (event) {
      if (this.element !== event.relatedTarget && !this.element.contains(event.relatedTarget)) {
        this.focused = false
        this.stopCursorBlinking()
        this.scheduleUpdate()
        this.element.dispatchEvent(new FocusEvent(event.type, event))
      }
    }
  
    didFocusHiddenInput () {
      // Focusing the hidden input when it is off-screen causes the browser to
      // scroll it into view. Since we use synthetic scrolling this behavior
      // causes all the lines to disappear so we counteract it by always setting
      // the scroll position to 0.
      this.refs.scrollContainer.scrollTop = 0
      this.refs.scrollContainer.scrollLeft = 0
      if (!this.focused) {
        this.focused = true
        this.startCursorBlinking()
        this.scheduleUpdate()
      }
    }
  
    didMouseWheel (event) {
      const scrollSensitivity = this.props.model.getScrollSensitivity() / 100
  
      let {wheelDeltaX, wheelDeltaY} = event
  
      if (Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)) {
        wheelDeltaX = wheelDeltaX * scrollSensitivity
        wheelDeltaY = 0
      } else {
        wheelDeltaX = 0
        wheelDeltaY = wheelDeltaY * scrollSensitivity
      }
  
      if (this.getPlatform() !== 'darwin' && event.shiftKey) {
        let temp = wheelDeltaX
        wheelDeltaX = wheelDeltaY
        wheelDeltaY = temp
      }
  
      const scrollLeftChanged = wheelDeltaX !== 0 && this.setScrollLeft(this.getScrollLeft() - wheelDeltaX)
      const scrollTopChanged = wheelDeltaY !== 0 && this.setScrollTop(this.getScrollTop() - wheelDeltaY)
  
      if (scrollLeftChanged || scrollTopChanged) this.updateSync()
    }
  
    didResize () {
      // Prevent the component from measuring the client container dimensions when
      // getting spurious resize events.
      if (this.isVisible()) {
        const clientContainerWidthChanged = this.measureClientContainerWidth()
        const clientContainerHeightChanged = this.measureClientContainerHeight()
        if (clientContainerWidthChanged || clientContainerHeightChanged) {
          if (clientContainerWidthChanged) {
            this.remeasureAllBlockDecorations = true
          }
  
          this.resizeObserver.disconnect()
          this.scheduleUpdate()
          process.nextTick(() => { this.resizeObserver.observe(this.element) })
        }
      }
    }
  
    didResizeGutterContainer () {
      // Prevent the component from measuring the gutter dimensions when getting
      // spurious resize events.
      if (this.isVisible() && this.measureGutterDimensions()) {
        this.gutterContainerResizeObserver.disconnect()
        this.scheduleUpdate()
        process.nextTick(() => { this.gutterContainerResizeObserver.observe(this.refs.gutterContainer.element) })
      }
    }
  
    didScrollDummyScrollbar () {
      let scrollTopChanged = false
      let scrollLeftChanged = false
      if (!this.scrollTopPending) {
        scrollTopChanged = this.setScrollTop(this.refs.verticalScrollbar.element.scrollTop)
      }
      if (!this.scrollLeftPending) {
        scrollLeftChanged = this.setScrollLeft(this.refs.horizontalScrollbar.element.scrollLeft)
      }
      if (scrollTopChanged || scrollLeftChanged) this.updateSync()
    }
  
    didUpdateStyles () {
      this.remeasureCharacterDimensions = true
      this.horizontalPixelPositionsByScreenLineId.clear()
      this.scheduleUpdate()
    }
  
    didUpdateScrollbarStyles () {
      if (!this.props.model.isMini()) {
        this.remeasureScrollbars = true
        this.scheduleUpdate()
      }
    }
  
    didPaste (event) {
      // On Linux, Chromium translates a middle-button mouse click into a
      // mousedown event *and* a paste event. Since Atom supports the middle mouse
      // click as a way of closing a tab, we only want the mousedown event, not
      // the paste event. And since we don't use the `paste` event for any
      // behavior in Atom, we can no-op the event to eliminate this issue.
      // See https://github.com/atom/atom/pull/15183#issue-248432413.
      if (this.getPlatform() === 'linux') event.preventDefault()
    }
  
    didTextInput (event) {
      if (this.compositionCheckpoint) {
        this.props.model.revertToCheckpoint(this.compositionCheckpoint)
        this.compositionCheckpoint = null
      }
  
      if (this.isInputEnabled()) {
        event.stopPropagation()
  
        // WARNING: If we call preventDefault on the input of a space
        // character, then the browser interprets the spacebar keypress as a
        // page-down command, causing spaces to scroll elements containing
        // editors. This means typing space will actually change the contents
        // of the hidden input, which will cause the browser to autoscroll the
        // scroll container to reveal the input if it is off screen (See
        // https://github.com/atom/atom/issues/16046). To correct for this
        // situation, we automatically reset the scroll position to 0,0 after
        // typing a space. None of this can really be tested.
        if (event.data === ' ') {
          window.setImmediate(() => {
            this.refs.scrollContainer.scrollTop = 0
            this.refs.scrollContainer.scrollLeft = 0
          })
        } else {
          event.preventDefault()
        }
  
        // If the input event is fired while the accented character menu is open it
        // means that the user has chosen one of the accented alternatives. Thus, we
        // will replace the original non accented character with the selected
        // alternative.
        if (this.accentedCharacterMenuIsOpen) {
          this.props.model.selectLeft()
        }
  
        this.props.model.insertText(event.data, {groupUndo: true})
      }
    }
  
    // We need to get clever to detect when the accented character menu is
    // opened on macOS. Usually, every keydown event that could cause input is
    // followed by a corresponding keypress. However, pressing and holding
    // long enough to open the accented character menu causes additional keydown
    // events to fire that aren't followed by their own keypress and textInput
    // events.
    //
    // Therefore, we assume the accented character menu has been deployed if,
    // before observing any keyup event, we observe events in the following
    // sequence:
    //
    // keydown(code: X), keypress, keydown(code: X)
    //
    // The code X must be the same in the keydown events that bracket the
    // keypress, meaning we're *holding* the _same_ key we intially pressed.
    // Got that?
    didKeydown (event) {
      // Stop dragging when user interacts with the keyboard. This prevents
      // unwanted selections in the case edits are performed while selecting text
      // at the same time. Modifier keys are exempt to preserve the ability to
      // add selections, shift-scroll horizontally while selecting.
      if (this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta' && event.key !== 'Shift') {
        this.stopDragging()
      }
  
      if (this.lastKeydownBeforeKeypress != null) {
        if (this.lastKeydownBeforeKeypress.code === event.code) {
          this.accentedCharacterMenuIsOpen = true
        }
  
        this.lastKeydownBeforeKeypress = null
      }
  
      this.lastKeydown = event
    }
  
    didKeypress (event) {
      this.lastKeydownBeforeKeypress = this.lastKeydown
  
      // This cancels the accented character behavior if we type a key normally
      // with the menu open.
      this.accentedCharacterMenuIsOpen = false
    }
  
    didKeyup (event) {
      if (this.lastKeydownBeforeKeypress && this.lastKeydownBeforeKeypress.code === event.code) {
        this.lastKeydownBeforeKeypress = null
      }
    }
  
    // The IME composition events work like this:
    //
    // User types 's', chromium pops up the completion helper
    //   1. compositionstart fired
    //   2. compositionupdate fired; event.data == 's'
    // User hits arrow keys to move around in completion helper
    //   3. compositionupdate fired; event.data == 's' for each arry key press
    // User escape to cancel OR User chooses a completion
    //   4. compositionend fired
    //   5. textInput fired; event.data == the completion string
    didCompositionStart () {
      // Workaround for Chromium not preventing composition events when
      // preventDefault is called on the keydown event that precipitated them.
      if (this.lastKeydown && this.lastKeydown.defaultPrevented) {
        this.getHiddenInput().disabled = true
        process.nextTick(() => {
          // Disabling the hidden input makes it lose focus as well, so we have to
          // re-enable and re-focus it.
          this.getHiddenInput().disabled = false
          this.getHiddenInput().focus()
        })
        return
      }
  
      this.compositionCheckpoint = this.props.model.createCheckpoint()
      if (this.accentedCharacterMenuIsOpen) {
        this.props.model.selectLeft()
      }
    }
  
    didCompositionUpdate (event) {
      this.props.model.insertText(event.data, {select: true})
    }
  
    didCompositionEnd (event) {
      event.target.value = ''
    }
  
    didMouseDownOnContent (event) {
      const {model} = this.props
      const {target, button, detail, ctrlKey, shiftKey, metaKey} = event
      const platform = this.getPlatform()
  
      // Ignore clicks on block decorations.
      if (target) {
        let element = target
        while (element && element !== this.element) {
          if (this.blockDecorationsByElement.has(element)) {
            return
          }
  
          element = element.parentElement
        }
      }
  
      const screenPosition = this.screenPositionForMouseEvent(event)
  
      if (button === 1) {
        model.setCursorScreenPosition(screenPosition, {autoscroll: false})
  
        // On Linux, pasting happens on middle click. A textInput event with the
        // contents of the selection clipboard will be dispatched by the browser
        // automatically on mouseup.
        if (platform === 'linux' && this.isInputEnabled()) model.insertText(clipboard.readText('selection'))
        return
      }
  
      if (button !== 0) return
  
      // Ctrl-click brings up the context menu on macOS
      if (platform === 'darwin' && ctrlKey) return
  
      if (target && target.matches('.fold-marker')) {
        const bufferPosition = model.bufferPositionForScreenPosition(screenPosition)
        model.destroyFoldsContainingBufferPositions([bufferPosition], false)
        return
      }
  
      const addOrRemoveSelection = metaKey || (ctrlKey && platform !== 'darwin')
  
      switch (detail) {
        case 1:
          if (addOrRemoveSelection) {
            const existingSelection = model.getSelectionAtScreenPosition(screenPosition)
            if (existingSelection) {
              if (model.hasMultipleCursors()) existingSelection.destroy()
            } else {
              model.addCursorAtScreenPosition(screenPosition, {autoscroll: false})
            }
          } else {
            if (shiftKey) {
              model.selectToScreenPosition(screenPosition, {autoscroll: false})
            } else {
              model.setCursorScreenPosition(screenPosition, {autoscroll: false})
            }
          }
          break
        case 2:
          if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition, {autoscroll: false})
          model.getLastSelection().selectWord({autoscroll: false})
          break
        case 3:
          if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition, {autoscroll: false})
          model.getLastSelection().selectLine(null, {autoscroll: false})
          break
      }
  
      this.handleMouseDragUntilMouseUp({
        didDrag: (event) => {
          this.autoscrollOnMouseDrag(event)
          const screenPosition = this.screenPositionForMouseEvent(event)
          model.selectToScreenPosition(screenPosition, {suppressSelectionMerge: true, autoscroll: false})
          this.updateSync()
        },
        didStopDragging: () => {
          model.finalizeSelections()
          model.mergeIntersectingSelections()
          this.updateSync()
        }
      })
    }
  
    didMouseDownOnLineNumberGutter (event) {
      const {model} = this.props
      const {target, button, ctrlKey, shiftKey, metaKey} = event
  
      // Only handle mousedown events for left mouse button
      if (button !== 0) return
  
      const clickedScreenRow = this.screenPositionForMouseEvent(event).row
      const startBufferRow = model.bufferPositionForScreenPosition([clickedScreenRow, 0]).row
  
      if (target && (target.matches('.foldable .icon-right') || target.matches('.folded .icon-right'))) {
        model.toggleFoldAtBufferRow(startBufferRow)
        return
      }
  
      const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin')
      const endBufferRow = model.bufferPositionForScreenPosition([clickedScreenRow, Infinity]).row
      const clickedLineBufferRange = Range(Point(startBufferRow, 0), Point(endBufferRow + 1, 0))
  
      let initialBufferRange
      if (shiftKey) {
        const lastSelection = model.getLastSelection()
        initialBufferRange = lastSelection.getBufferRange()
        lastSelection.setBufferRange(initialBufferRange.union(clickedLineBufferRange), {
          reversed: clickedScreenRow < lastSelection.getScreenRange().start.row,
          autoscroll: false,
          preserveFolds: true,
          suppressSelectionMerge: true
        })
      } else {
        initialBufferRange = clickedLineBufferRange
        if (addOrRemoveSelection) {
          model.addSelectionForBufferRange(clickedLineBufferRange, {autoscroll: false, preserveFolds: true})
        } else {
          model.setSelectedBufferRange(clickedLineBufferRange, {autoscroll: false, preserveFolds: true})
        }
      }
  
      const initialScreenRange = model.screenRangeForBufferRange(initialBufferRange)
      this.handleMouseDragUntilMouseUp({
        didDrag: (event) => {
          this.autoscrollOnMouseDrag(event, true)
          const dragRow = this.screenPositionForMouseEvent(event).row
          const draggedLineScreenRange = Range(Point(dragRow, 0), Point(dragRow + 1, 0))
          model.getLastSelection().setScreenRange(draggedLineScreenRange.union(initialScreenRange), {
            reversed: dragRow < initialScreenRange.start.row,
            autoscroll: false,
            preserveFolds: true
          })
          this.updateSync()
        },
        didStopDragging: () => {
          model.mergeIntersectingSelections()
          this.updateSync()
        }
      })
    }
  
    handleMouseDragUntilMouseUp ({didDrag, didStopDragging}) {
      let dragging = false
      let lastMousemoveEvent
  
      const animationFrameLoop = () => {
        window.requestAnimationFrame(() => {
          if (dragging && this.visible) {
            didDrag(lastMousemoveEvent)
            animationFrameLoop()
          }
        })
      }
  
      function didMouseMove (event) {
        lastMousemoveEvent = event
        if (!dragging) {
          dragging = true
          animationFrameLoop()
        }
      }
  
      function didMouseUp () {
        this.stopDragging = null
        window.removeEventListener('mousemove', didMouseMove)
        window.removeEventListener('mouseup', didMouseUp, {capture: true})
        if (dragging) {
          dragging = false
          didStopDragging()
        }
      }
  
      window.addEventListener('mousemove', didMouseMove)
      window.addEventListener('mouseup', didMouseUp, {capture: true})
      this.stopDragging = didMouseUp
    }
  
    autoscrollOnMouseDrag ({clientX, clientY}, verticalOnly = false) {
      var {top, bottom, left, right} = this.refs.scrollContainer.getBoundingClientRect() // Using var to avoid deopt on += assignments below
      top += MOUSE_DRAG_AUTOSCROLL_MARGIN
      bottom -= MOUSE_DRAG_AUTOSCROLL_MARGIN
      left += MOUSE_DRAG_AUTOSCROLL_MARGIN
      right -= MOUSE_DRAG_AUTOSCROLL_MARGIN
  
      let yDelta, yDirection
      if (clientY < top) {
        yDelta = top - clientY
        yDirection = -1
      } else if (clientY > bottom) {
        yDelta = clientY - bottom
        yDirection = 1
      }
  
      let xDelta, xDirection
      if (clientX < left) {
        xDelta = left - clientX
        xDirection = -1
      } else if (clientX > right) {
        xDelta = clientX - right
        xDirection = 1
      }
  
      let scrolled = false
      if (yDelta != null) {
        const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection
        scrolled = this.setScrollTop(this.getScrollTop() + scaledDelta)
      }
  
      if (!verticalOnly && xDelta != null) {
        const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection
        scrolled = this.setScrollLeft(this.getScrollLeft() + scaledDelta)
      }
  
      if (scrolled) this.updateSync()
    }
  
    screenPositionForMouseEvent (event) {
      return this.screenPositionForPixelPosition(this.pixelPositionForMouseEvent(event))
    }
  
    pixelPositionForMouseEvent ({clientX, clientY}) {
      const scrollContainerRect = this.refs.scrollContainer.getBoundingClientRect()
      clientX = Math.min(scrollContainerRect.right, Math.max(scrollContainerRect.left, clientX))
      clientY = Math.min(scrollContainerRect.bottom, Math.max(scrollContainerRect.top, clientY))
      const linesRect = this.refs.lineTiles.getBoundingClientRect()
      return {
        top: clientY - linesRect.top,
        left: clientX - linesRect.left
      }
    }
  
    didUpdateSelections () {
      this.pauseCursorBlinking()
      this.scheduleUpdate()
    }
  
    pauseCursorBlinking () {
      this.stopCursorBlinking()
      this.debouncedResumeCursorBlinking()
    }
  
    resumeCursorBlinking () {
      this.cursorsBlinkedOff = true
      this.startCursorBlinking()
    }
  
    stopCursorBlinking () {
      if (this.cursorsBlinking) {
        this.cursorsBlinkedOff = false
        this.cursorsBlinking = false
        window.clearInterval(this.cursorBlinkIntervalHandle)
        this.cursorBlinkIntervalHandle = null
        this.scheduleUpdate()
      }
    }
  
    startCursorBlinking () {
      if (!this.cursorsBlinking) {
        this.cursorBlinkIntervalHandle = window.setInterval(() => {
          this.cursorsBlinkedOff = !this.cursorsBlinkedOff
          this.scheduleUpdate(true)
        }, (this.props.cursorBlinkPeriod || CURSOR_BLINK_PERIOD) / 2)
        this.cursorsBlinking = true
        this.scheduleUpdate(true)
      }
    }
  
    didRequestAutoscroll (autoscroll) {
      this.pendingAutoscroll = autoscroll
      this.scheduleUpdate()
    }
  
    flushPendingLogicalScrollPosition () {
      let changedScrollTop = false
      if (this.pendingScrollTopRow > 0) {
        changedScrollTop = this.setScrollTopRow(this.pendingScrollTopRow, false)
        this.pendingScrollTopRow = null
      }
  
      let changedScrollLeft = false
      if (this.pendingScrollLeftColumn > 0) {
        changedScrollLeft = this.setScrollLeftColumn(this.pendingScrollLeftColumn, false)
        this.pendingScrollLeftColumn = null
      }
  
      if (changedScrollTop || changedScrollLeft) {
        this.updateSync()
      }
    }
  
    autoscrollVertically (screenRange, options) {
      const screenRangeTop = this.pixelPositionAfterBlocksForRow(screenRange.start.row)
      const screenRangeBottom = this.pixelPositionAfterBlocksForRow(screenRange.end.row) + this.getLineHeight()
      const verticalScrollMargin = this.getVerticalAutoscrollMargin()
  
      let desiredScrollTop, desiredScrollBottom
      if (options && options.center) {
        const desiredScrollCenter = (screenRangeTop + screenRangeBottom) / 2
        if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) {
          desiredScrollTop = desiredScrollCenter - this.getScrollContainerClientHeight() / 2
          desiredScrollBottom = desiredScrollCenter + this.getScrollContainerClientHeight() / 2
        }
      } else {
        desiredScrollTop = screenRangeTop - verticalScrollMargin
        desiredScrollBottom = screenRangeBottom + verticalScrollMargin
      }
  
      if (!options || options.reversed !== false) {
        if (desiredScrollBottom > this.getScrollBottom()) {
          this.setScrollBottom(desiredScrollBottom)
        }
        if (desiredScrollTop < this.getScrollTop()) {
          this.setScrollTop(desiredScrollTop)
        }
      } else {
        if (desiredScrollTop < this.getScrollTop()) {
          this.setScrollTop(desiredScrollTop)
        }
        if (desiredScrollBottom > this.getScrollBottom()) {
          this.setScrollBottom(desiredScrollBottom)
        }
      }
  
      return false
    }
  
    autoscrollHorizontally (screenRange, options) {
      const horizontalScrollMargin = this.getHorizontalAutoscrollMargin()
  
      const gutterContainerWidth = this.getGutterContainerWidth()
      let left = this.pixelLeftForRowAndColumn(screenRange.start.row, screenRange.start.column) + gutterContainerWidth
      let right = this.pixelLeftForRowAndColumn(screenRange.end.row, screenRange.end.column) + gutterContainerWidth
      const desiredScrollLeft = Math.max(0, left - horizontalScrollMargin - gutterContainerWidth)
      const desiredScrollRight = Math.min(this.getScrollWidth(), right + horizontalScrollMargin)
  
      if (!options || options.reversed !== false) {
        if (desiredScrollRight > this.getScrollRight()) {
          this.setScrollRight(desiredScrollRight)
        }
        if (desiredScrollLeft < this.getScrollLeft()) {
          this.setScrollLeft(desiredScrollLeft)
        }
      } else {
        if (desiredScrollLeft < this.getScrollLeft()) {
          this.setScrollLeft(desiredScrollLeft)
        }
        if (desiredScrollRight > this.getScrollRight()) {
          this.setScrollRight(desiredScrollRight)
        }
      }
    }
  
    getVerticalAutoscrollMargin () {
      const maxMarginInLines = Math.floor(
        (this.getScrollContainerClientHeight() / this.getLineHeight() - 1) / 2
      )
      const marginInLines = Math.min(
        this.props.model.verticalScrollMargin,
        maxMarginInLines
      )
      return marginInLines * this.getLineHeight()
    }
  
    getHorizontalAutoscrollMargin () {
      const maxMarginInBaseCharacters = Math.floor(
        (this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() - 1) / 2
      )
      const marginInBaseCharacters = Math.min(
        this.props.model.horizontalScrollMargin,
        maxMarginInBaseCharacters
      )
      return marginInBaseCharacters * this.getBaseCharacterWidth()
    }
  
    // This method is called at the beginning of a frame render to relay any
    // potential changes in the editor's width into the model before proceeding.
    updateModelSoftWrapColumn () {
      const {model} = this.props
      const newEditorWidthInChars = this.getScrollContainerClientWidthInBaseCharacters()
      if (newEditorWidthInChars !== model.getEditorWidthInChars()) {
        this.suppressUpdates = true
  
        const renderedStartRow = this.getRenderedStartRow()
        this.props.model.setEditorWidthInChars(newEditorWidthInChars)
  
        // Relaying a change in to the editor's client width may cause the
        // vertical scrollbar to appear or disappear, which causes the editor's
        // client width to change *again*. Make sure the display layer is fully
        // populated for the visible area before recalculating the editor's
        // width in characters. Then update the display layer *again* just in
        // case a change in scrollbar visibility causes lines to wrap
        // differently. We capture the renderedStartRow before resetting the
        // display layer because once it has been reset, we can't compute the
        // rendered start row accurately. 😥
        this.populateVisibleRowRange(renderedStartRow)
        this.props.model.setEditorWidthInChars(this.getScrollContainerClientWidthInBaseCharacters())
        this.derivedDimensionsCache = {}
  
        this.suppressUpdates = false
      }
    }
  
    // This method exists because it existed in the previous implementation and some
    // package tests relied on it
    measureDimensions () {
      this.measureCharacterDimensions()
      this.measureGutterDimensions()
      this.measureClientContainerHeight()
      this.measureClientContainerWidth()
      this.measureScrollbarDimensions()
      this.hasInitialMeasurements = true
    }
  
    measureCharacterDimensions () {
      this.measurements.lineHeight = Math.max(1, this.refs.characterMeasurementLine.getBoundingClientRect().height)
      this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width
      this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width
      this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width
      this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().width
  
      this.props.model.setLineHeightInPixels(this.measurements.lineHeight)
      this.props.model.setDefaultCharWidth(
        this.measurements.baseCharacterWidth,
        this.measurements.doubleWidthCharacterWidth,
        this.measurements.halfWidthCharacterWidth,
        this.measurements.koreanCharacterWidth
      )
      this.lineTopIndex.setDefaultLineHeight(this.measurements.lineHeight)
    }
  
    measureGutterDimensions () {
      let dimensionsChanged = false
  
      if (this.refs.gutterContainer) {
        const gutterContainerWidth = this.refs.gutterContainer.element.offsetWidth
        if (gutterContainerWidth !== this.measurements.gutterContainerWidth) {
          dimensionsChanged = true
          this.measurements.gutterContainerWidth = gutterContainerWidth
        }
      } else {
        this.measurements.gutterContainerWidth = 0
      }
  
      if (this.refs.gutterContainer && this.refs.gutterContainer.refs.lineNumberGutter) {
        const lineNumberGutterWidth = this.refs.gutterContainer.refs.lineNumberGutter.element.offsetWidth
        if (lineNumberGutterWidth !== this.measurements.lineNumberGutterWidth) {
          dimensionsChanged = true
          this.measurements.lineNumberGutterWidth = lineNumberGutterWidth
        }
      } else {
        this.measurements.lineNumberGutterWidth = 0
      }
  
      return dimensionsChanged
    }
  
    measureClientContainerHeight () {
      const clientContainerHeight = this.refs.clientContainer.offsetHeight
      if (clientContainerHeight !== this.measurements.clientContainerHeight) {
        this.measurements.clientContainerHeight = clientContainerHeight
        return true
      } else {
        return false
      }
    }
  
    measureClientContainerWidth () {
      const clientContainerWidth = this.refs.clientContainer.offsetWidth
      if (clientContainerWidth !== this.measurements.clientContainerWidth) {
        this.measurements.clientContainerWidth = clientContainerWidth
        return true
      } else {
        return false
      }
    }
  
    measureScrollbarDimensions () {
      if (this.props.model.isMini()) {
        this.measurements.verticalScrollbarWidth = 0
        this.measurements.horizontalScrollbarHeight = 0
      } else {
        this.measurements.verticalScrollbarWidth = this.refs.verticalScrollbar.getRealScrollbarWidth()
        this.measurements.horizontalScrollbarHeight = this.refs.horizontalScrollbar.getRealScrollbarHeight()
      }
    }
  
    measureLongestLineWidth () {
      if (this.longestLineToMeasure) {
        const lineComponent = this.lineComponentsByScreenLineId.get(this.longestLineToMeasure.id)
        this.measurements.longestLineWidth = lineComponent.element.firstChild.offsetWidth
        this.longestLineToMeasure = null
      }
    }
  
    requestLineToMeasure (row, screenLine) {
      this.linesToMeasure.set(row, screenLine)
    }
  
    requestHorizontalMeasurement (row, column) {
      if (column === 0) return
  
      const screenLine = this.props.model.screenLineForScreenRow(row)
      if (screenLine) {
        this.requestLineToMeasure(row, screenLine)
  
        let columns = this.horizontalPositionsToMeasure.get(row)
        if (columns == null) {
          columns = []
          this.horizontalPositionsToMeasure.set(row, columns)
        }
        columns.push(column)
      }
    }
  
    measureHorizontalPositions () {
      this.horizontalPositionsToMeasure.forEach((columnsToMeasure, row) => {
        columnsToMeasure.sort((a, b) => a - b)
  
        const screenLine = this.renderedScreenLineForRow(row)
        const lineComponent = this.lineComponentsByScreenLineId.get(screenLine.id)
  
        if (!lineComponent) {
          const error = new Error('Requested measurement of a line component that is not currently rendered')
          error.metadata = {
            row,
            columnsToMeasure,
            renderedScreenLineIds: this.renderedScreenLines.map((line) => line.id),
            extraRenderedScreenLineIds: Array.from(this.extraRenderedScreenLines.keys()),
            lineComponentScreenLineIds: Array.from(this.lineComponentsByScreenLineId.keys()),
            renderedStartRow: this.getRenderedStartRow(),
            renderedEndRow: this.getRenderedEndRow(),
            requestedScreenLineId: screenLine.id
          }
          throw error
        }
  
        const lineNode = lineComponent.element
        const textNodes = lineComponent.textNodes
        let positionsForLine = this.horizontalPixelPositionsByScreenLineId.get(screenLine.id)
        if (positionsForLine == null) {
          positionsForLine = new Map()
          this.horizontalPixelPositionsByScreenLineId.set(screenLine.id, positionsForLine)
        }
  
        this.measureHorizontalPositionsOnLine(lineNode, textNodes, columnsToMeasure, positionsForLine)
      })
      this.horizontalPositionsToMeasure.clear()
    }
  
    measureHorizontalPositionsOnLine (lineNode, textNodes, columnsToMeasure, positions) {
      let lineNodeClientLeft = -1
      let textNodeStartColumn = 0
      let textNodesIndex = 0
      let lastTextNodeRight = null
  
      columnLoop: // eslint-disable-line no-labels
      for (let columnsIndex = 0; columnsIndex < columnsToMeasure.length; columnsIndex++) {
        const nextColumnToMeasure = columnsToMeasure[columnsIndex]
        while (textNodesIndex < textNodes.length) {
          if (nextColumnToMeasure === 0) {
            positions.set(0, 0)
            continue columnLoop // eslint-disable-line no-labels
          }
  
          if (positions.has(nextColumnToMeasure)) continue columnLoop // eslint-disable-line no-labels
          const textNode = textNodes[textNodesIndex]
          const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length
  
          if (nextColumnToMeasure < textNodeEndColumn) {
            let clientPixelPosition
            if (nextColumnToMeasure === textNodeStartColumn) {
              clientPixelPosition = clientRectForRange(textNode, 0, 1).left
            } else {
              clientPixelPosition = clientRectForRange(textNode, 0, nextColumnToMeasure - textNodeStartColumn).right
            }
  
            if (lineNodeClientLeft === -1) {
              lineNodeClientLeft = lineNode.getBoundingClientRect().left
            }
  
            positions.set(nextColumnToMeasure, Math.round(clientPixelPosition - lineNodeClientLeft))
            continue columnLoop // eslint-disable-line no-labels
          } else {
            textNodesIndex++
            textNodeStartColumn = textNodeEndColumn
          }
        }
  
        if (lastTextNodeRight == null) {
          const lastTextNode = textNodes[textNodes.length - 1]
          lastTextNodeRight = clientRectForRange(lastTextNode, 0, lastTextNode.textContent.length).right
        }
  
        if (lineNodeClientLeft === -1) {
          lineNodeClientLeft = lineNode.getBoundingClientRect().left
        }
  
        positions.set(nextColumnToMeasure, Math.round(lastTextNodeRight - lineNodeClientLeft))
      }
    }
  
    rowForPixelPosition (pixelPosition) {
      return Math.max(0, this.lineTopIndex.rowForPixelPosition(pixelPosition))
    }
  
    heightForBlockDecorationsBeforeRow (row) {
      return this.pixelPositionAfterBlocksForRow(row) - this.pixelPositionBeforeBlocksForRow(row)
    }
  
    heightForBlockDecorationsAfterRow (row) {
      const currentRowBottom = this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight()
      const nextRowTop = this.pixelPositionBeforeBlocksForRow(row + 1)
      return nextRowTop - currentRowBottom
    }
  
    pixelPositionBeforeBlocksForRow (row) {
      return this.lineTopIndex.pixelPositionBeforeBlocksForRow(row)
    }
  
    pixelPositionAfterBlocksForRow (row) {
      return this.lineTopIndex.pixelPositionAfterBlocksForRow(row)
    }
  
    pixelLeftForRowAndColumn (row, column) {
      if (column === 0) return 0
      const screenLine = this.renderedScreenLineForRow(row)
      if (screenLine) {
        const horizontalPositionsByColumn = this.horizontalPixelPositionsByScreenLineId.get(screenLine.id)
        if (horizontalPositionsByColumn) {
          return horizontalPositionsByColumn.get(column)
        }
      }
    }
  
    screenPositionForPixelPosition ({top, left}) {
      const {model} = this.props
  
      const row = Math.min(
        this.rowForPixelPosition(top),
        model.getApproximateScreenLineCount() - 1
      )
  
      let screenLine = this.renderedScreenLineForRow(row)
      if (!screenLine) {
        this.requestLineToMeasure(row, model.screenLineForScreenRow(row))
        this.updateSyncBeforeMeasuringContent()
        this.measureContentDuringUpdateSync()
        screenLine = this.renderedScreenLineForRow(row)
      }
  
      const linesClientLeft = this.refs.lineTiles.getBoundingClientRect().left
      const targetClientLeft = linesClientLeft + Math.max(0, left)
      const {textNodes} = this.lineComponentsByScreenLineId.get(screenLine.id)
  
      let containingTextNodeIndex
      {
        let low = 0
        let high = textNodes.length - 1
        while (low <= high) {
          const mid = low + ((high - low) >> 1)
          const textNode = textNodes[mid]
          const textNodeRect = clientRectForRange(textNode, 0, textNode.length)
  
          if (targetClientLeft < textNodeRect.left) {
            high = mid - 1
            containingTextNodeIndex = Math.max(0, mid - 1)
          } else if (targetClientLeft > textNodeRect.right) {
            low = mid + 1
            containingTextNodeIndex = Math.min(textNodes.length - 1, mid + 1)
          } else {
            containingTextNodeIndex = mid
            break
          }
        }
      }
      const containingTextNode = textNodes[containingTextNodeIndex]
      let characterIndex = 0
      {
        let low = 0
        let high = containingTextNode.length - 1
        while (low <= high) {
          const charIndex = low + ((high - low) >> 1)
          const nextCharIndex = isPairedCharacter(containingTextNode.textContent, charIndex)
            ? charIndex + 2
            : charIndex + 1
  
          const rangeRect = clientRectForRange(containingTextNode, charIndex, nextCharIndex)
          if (targetClientLeft < rangeRect.left) {
            high = charIndex - 1
            characterIndex = Math.max(0, charIndex - 1)
          } else if (targetClientLeft > rangeRect.right) {
            low = nextCharIndex
            characterIndex = Math.min(containingTextNode.textContent.length, nextCharIndex)
          } else {
            if (targetClientLeft <= ((rangeRect.left + rangeRect.right) / 2)) {
              characterIndex = charIndex
            } else {
              characterIndex = nextCharIndex
            }
            break
          }
        }
      }
  
      let textNodeStartColumn = 0
      for (let i = 0; i < containingTextNodeIndex; i++) {
        textNodeStartColumn = textNodeStartColumn + textNodes[i].length
      }
      const column = textNodeStartColumn + characterIndex
  
      return Point(row, column)
    }
  
    didResetDisplayLayer () {
      this.spliceLineTopIndex(0, Infinity, Infinity)
      this.scheduleUpdate()
    }
  
    didChangeDisplayLayer (changes) {
      for (let i = 0; i < changes.length; i++) {
        const {oldRange, newRange} = changes[i]
        this.spliceLineTopIndex(
          newRange.start.row,
          oldRange.end.row - oldRange.start.row,
          newRange.end.row - newRange.start.row
        )
      }
  
      this.scheduleUpdate()
    }
  
    didChangeSelectionRange () {
      const {model} = this.props
  
      if (this.getPlatform() === 'linux') {
        if (this.selectionClipboardImmediateId) {
          clearImmediate(this.selectionClipboardImmediateId)
        }
  
        this.selectionClipboardImmediateId = setImmediate(() => {
          this.selectionClipboardImmediateId = null
  
          if (model.isDestroyed()) return
  
          const selectedText = model.getSelectedText()
          if (selectedText) {
            // This uses ipcRenderer.send instead of clipboard.writeText because
            // clipboard.writeText is a sync ipcRenderer call on Linux and that
            // will slow down selections.
            electron.ipcRenderer.send('write-text-to-selection-clipboard', selectedText)
          }
        })
      }
    }
  
    observeBlockDecorations () {
      const {model} = this.props
      const decorations = model.getDecorations({type: 'block'})
      for (let i = 0; i < decorations.length; i++) {
        this.addBlockDecoration(decorations[i])
      }
    }
  
    addBlockDecoration (decoration, subscribeToChanges = true) {
      const marker = decoration.getMarker()
      const {item, position} = decoration.getProperties()
      const element = TextEditor.viewForItem(item)
  
      if (marker.isValid()) {
        const row = marker.getHeadScreenPosition().row
        this.lineTopIndex.insertBlock(decoration, row, 0, position === 'after')
        this.blockDecorationsToMeasure.add(decoration)
        this.blockDecorationsByElement.set(element, decoration)
        this.blockDecorationResizeObserver.observe(element)
  
        this.scheduleUpdate()
      }
  
      if (subscribeToChanges) {
        let wasValid = marker.isValid()
  
        const didUpdateDisposable = marker.bufferMarker.onDidChange(({textChanged}) => {
          const isValid = marker.isValid()
          if (wasValid && !isValid) {
            wasValid = false
            this.blockDecorationsToMeasure.delete(decoration)
            this.heightsByBlockDecoration.delete(decoration)
            this.blockDecorationsByElement.delete(element)
            this.blockDecorationResizeObserver.unobserve(element)
            this.lineTopIndex.removeBlock(decoration)
            this.scheduleUpdate()
          } else if (!wasValid && isValid) {
            wasValid = true
            this.addBlockDecoration(decoration, false)
          } else if (isValid && !textChanged) {
            this.lineTopIndex.moveBlock(decoration, marker.getHeadScreenPosition().row)
            this.scheduleUpdate()
          }
        })
  
        const didDestroyDisposable = decoration.onDidDestroy(() => {
          didUpdateDisposable.dispose()
          didDestroyDisposable.dispose()
  
          if (wasValid) {
            wasValid = false
            this.blockDecorationsToMeasure.delete(decoration)
            this.heightsByBlockDecoration.delete(decoration)
            this.blockDecorationsByElement.delete(element)
            this.blockDecorationResizeObserver.unobserve(element)
            this.lineTopIndex.removeBlock(decoration)
            this.scheduleUpdate()
          }
        })
      }
    }
  
    didResizeBlockDecorations (entries) {
      if (!this.visible) return
  
      for (let i = 0; i < entries.length; i++) {
        const {target, contentRect} = entries[i]
        const decoration = this.blockDecorationsByElement.get(target)
        const previousHeight = this.heightsByBlockDecoration.get(decoration)
        if (this.element.contains(target) && contentRect.height !== previousHeight) {
          this.invalidateBlockDecorationDimensions(decoration)
        }
      }
    }
  
    invalidateBlockDecorationDimensions (decoration) {
      this.blockDecorationsToMeasure.add(decoration)
      this.scheduleUpdate()
    }
  
    spliceLineTopIndex (startRow, oldExtent, newExtent) {
      const invalidatedBlockDecorations = this.lineTopIndex.splice(startRow, oldExtent, newExtent)
      invalidatedBlockDecorations.forEach((decoration) => {
        const newPosition = decoration.getMarker().getHeadScreenPosition()
        this.lineTopIndex.moveBlock(decoration, newPosition.row)
      })
    }
  
    isVisible () {
      return this.element.offsetWidth > 0 || this.element.offsetHeight > 0
    }
  
    getWindowInnerHeight () {
      return window.innerHeight
    }
  
    getWindowInnerWidth () {
      return window.innerWidth
    }
  
    getLineHeight () {
      return this.measurements.lineHeight
    }
  
    getBaseCharacterWidth () {
      return this.measurements.baseCharacterWidth
    }
  
    getLongestLineWidth () {
      return this.measurements.longestLineWidth
    }
  
    getClientContainerHeight () {
      return this.measurements.clientContainerHeight
    }
  
    getClientContainerWidth () {
      return this.measurements.clientContainerWidth
    }
  
    getScrollContainerWidth () {
      if (this.props.model.getAutoWidth()) {
        return this.getScrollWidth()
      } else {
        return this.getClientContainerWidth() - this.getGutterContainerWidth()
      }
    }
  
    getScrollContainerHeight () {
      if (this.props.model.getAutoHeight()) {
        return this.getScrollHeight() + this.getHorizontalScrollbarHeight()
      } else {
        return this.getClientContainerHeight()
      }
    }
  
    getScrollContainerClientWidth () {
      return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()
    }
  
    getScrollContainerClientHeight () {
      return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()
    }
  
    canScrollVertically () {
      const {model} = this.props
      if (model.isMini()) return false
      if (model.getAutoHeight()) return false
      return this.getContentHeight() > this.getScrollContainerClientHeight()
    }
  
    canScrollHorizontally () {
      const {model} = this.props
      if (model.isMini()) return false
      if (model.getAutoWidth()) return false
      if (model.isSoftWrapped()) return false
      return this.getContentWidth() > this.getScrollContainerClientWidth()
    }
  
    getScrollHeight () {
      if (this.props.model.getScrollPastEnd()) {
        return this.getContentHeight() + Math.max(
          3 * this.getLineHeight(),
          this.getScrollContainerClientHeight() - (3 * this.getLineHeight())
        )
      } else if (this.props.model.getAutoHeight()) {
        return this.getContentHeight()
      } else {
        return Math.max(this.getContentHeight(), this.getScrollContainerClientHeight())
      }
    }
  
    getScrollWidth () {
      const {model} = this.props
  
      if (model.isSoftWrapped()) {
        return this.getScrollContainerClientWidth()
      } else if (model.getAutoWidth()) {
        return this.getContentWidth()
      } else {
        return Math.max(this.getContentWidth(), this.getScrollContainerClientWidth())
      }
    }
  
    getContentHeight () {
      return this.pixelPositionAfterBlocksForRow(this.props.model.getApproximateScreenLineCount())
    }
  
    getContentWidth () {
      return Math.ceil(this.getLongestLineWidth() + this.getBaseCharacterWidth())
    }
  
    getScrollContainerClientWidthInBaseCharacters () {
      return Math.floor(this.getScrollContainerClientWidth() / this.getBaseCharacterWidth())
    }
  
    getGutterContainerWidth () {
      return this.measurements.gutterContainerWidth
    }
  
    getLineNumberGutterWidth () {
      return this.measurements.lineNumberGutterWidth
    }
  
    getVerticalScrollbarWidth () {
      return this.measurements.verticalScrollbarWidth
    }
  
    getHorizontalScrollbarHeight () {
      return this.measurements.horizontalScrollbarHeight
    }
  
    getRowsPerTile () {
      return this.props.rowsPerTile || DEFAULT_ROWS_PER_TILE
    }
  
    tileStartRowForRow (row) {
      return row - (row % this.getRowsPerTile())
    }
  
    getRenderedStartRow () {
      if (this.derivedDimensionsCache.renderedStartRow == null) {
        this.derivedDimensionsCache.renderedStartRow = this.tileStartRowForRow(this.getFirstVisibleRow())
      }
  
      return this.derivedDimensionsCache.renderedStartRow
    }
  
    getRenderedEndRow () {
      if (this.derivedDimensionsCache.renderedEndRow == null) {
        this.derivedDimensionsCache.renderedEndRow = Math.min(
          this.props.model.getApproximateScreenLineCount(),
          this.getRenderedStartRow() + this.getVisibleTileCount() * this.getRowsPerTile()
        )
      }
  
      return this.derivedDimensionsCache.renderedEndRow
    }
  
    getRenderedRowCount () {
      if (this.derivedDimensionsCache.renderedRowCount == null) {
        this.derivedDimensionsCache.renderedRowCount = Math.max(0, this.getRenderedEndRow() - this.getRenderedStartRow())
      }
  
      return this.derivedDimensionsCache.renderedRowCount
    }
  
    getRenderedTileCount () {
      if (this.derivedDimensionsCache.renderedTileCount == null) {
        this.derivedDimensionsCache.renderedTileCount = Math.ceil(this.getRenderedRowCount() / this.getRowsPerTile())
      }
  
      return this.derivedDimensionsCache.renderedTileCount
    }
  
    getFirstVisibleRow () {
      if (this.derivedDimensionsCache.firstVisibleRow == null) {
        this.derivedDimensionsCache.firstVisibleRow = this.rowForPixelPosition(this.getScrollTop())
      }
  
      return this.derivedDimensionsCache.firstVisibleRow
    }
  
    getLastVisibleRow () {
      if (this.derivedDimensionsCache.lastVisibleRow == null) {
        this.derivedDimensionsCache.lastVisibleRow = Math.min(
          this.props.model.getApproximateScreenLineCount() - 1,
          this.rowForPixelPosition(this.getScrollBottom())
        )
      }
  
      return this.derivedDimensionsCache.lastVisibleRow
    }
  
    // We may render more tiles than needed if some contain block decorations,
    // but keeping this calculation simple ensures the number of tiles remains
    // fixed for a given editor height, which eliminates situations where a
    // tile is repeatedly added and removed during scrolling in certain
    // combinations of editor height and line height.
    getVisibleTileCount () {
      if (this.derivedDimensionsCache.visibleTileCount == null) {
        const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() / this.getRowsPerTile()
        this.derivedDimensionsCache.visibleTileCount = Math.ceil(editorHeightInTiles) + 1
      }
      return this.derivedDimensionsCache.visibleTileCount
    }
  
    getFirstVisibleColumn () {
      return Math.floor(this.getScrollLeft() / this.getBaseCharacterWidth())
    }
  
    getScrollTop () {
      this.scrollTop = Math.min(this.getMaxScrollTop(), this.scrollTop)
      return this.scrollTop
    }
  
    setScrollTop (scrollTop) {
      if (Number.isNaN(scrollTop) || scrollTop == null) return false
  
      scrollTop = roundToPhysicalPixelBoundary(Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop)))
      if (scrollTop !== this.scrollTop) {
        this.derivedDimensionsCache = {}
        this.scrollTopPending = true
        this.scrollTop = scrollTop
        this.element.emitter.emit('did-change-scroll-top', scrollTop)
        return true
      } else {
        return false
      }
    }
  
    getMaxScrollTop () {
      return Math.round(Math.max(0, this.getScrollHeight() - this.getScrollContainerClientHeight()))
    }
  
    getScrollBottom () {
      return this.getScrollTop() + this.getScrollContainerClientHeight()
    }
  
    setScrollBottom (scrollBottom) {
      return this.setScrollTop(scrollBottom - this.getScrollContainerClientHeight())
    }
  
    getScrollLeft () {
      return this.scrollLeft
    }
  
    setScrollLeft (scrollLeft) {
      if (Number.isNaN(scrollLeft) || scrollLeft == null) return false
  
      scrollLeft = roundToPhysicalPixelBoundary(Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft)))
      if (scrollLeft !== this.scrollLeft) {
        this.scrollLeftPending = true
        this.scrollLeft = scrollLeft
        this.element.emitter.emit('did-change-scroll-left', scrollLeft)
        return true
      } else {
        return false
      }
    }
  
    getMaxScrollLeft () {
      return Math.round(Math.max(0, this.getScrollWidth() - this.getScrollContainerClientWidth()))
    }
  
    getScrollRight () {
      return this.getScrollLeft() + this.getScrollContainerClientWidth()
    }
  
    setScrollRight (scrollRight) {
      return this.setScrollLeft(scrollRight - this.getScrollContainerClientWidth())
    }
  
    setScrollTopRow (scrollTopRow, scheduleUpdate = true) {
      if (this.hasInitialMeasurements) {
        const didScroll = this.setScrollTop(this.pixelPositionBeforeBlocksForRow(scrollTopRow))
        if (didScroll && scheduleUpdate) {
          this.scheduleUpdate()
        }
        return didScroll
      } else {
        this.pendingScrollTopRow = scrollTopRow
        return false
      }
    }
  
    getScrollTopRow () {
      if (this.hasInitialMeasurements) {
        return this.rowForPixelPosition(this.getScrollTop())
      } else {
        return this.pendingScrollTopRow || 0
      }
    }
  
    setScrollLeftColumn (scrollLeftColumn, scheduleUpdate = true) {
      if (this.hasInitialMeasurements && this.getLongestLineWidth() != null) {
        const didScroll = this.setScrollLeft(scrollLeftColumn * this.getBaseCharacterWidth())
        if (didScroll && scheduleUpdate) {
          this.scheduleUpdate()
        }
        return didScroll
      } else {
        this.pendingScrollLeftColumn = scrollLeftColumn
        return false
      }
    }
  
    getScrollLeftColumn () {
      if (this.hasInitialMeasurements && this.getLongestLineWidth() != null) {
        return Math.round(this.getScrollLeft() / this.getBaseCharacterWidth())
      } else {
        return this.pendingScrollLeftColumn || 0
      }
    }
  
    // Ensure the spatial index is populated with rows that are currently visible
    populateVisibleRowRange (renderedStartRow) {
      const {model} = this.props
      const previousScreenLineCount = model.getApproximateScreenLineCount()
  
      const renderedEndRow = renderedStartRow + (this.getVisibleTileCount() * this.getRowsPerTile())
      this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, renderedEndRow)
  
      // If the approximate screen line count changes, previously-cached derived
      // dimensions could now be out of date.
      if (model.getApproximateScreenLineCount() !== previousScreenLineCount) {
        this.derivedDimensionsCache = {}
      }
    }
  
    populateVisibleTiles () {
      const startRow = this.getRenderedStartRow()
      const endRow = this.getRenderedEndRow()
      const freeTileIds = []
      for (let i = 0; i < this.renderedTileStartRows.length; i++) {
        const tileStartRow = this.renderedTileStartRows[i]
        if (tileStartRow < startRow || tileStartRow >= endRow) {
          const tileId = this.idsByTileStartRow.get(tileStartRow)
          freeTileIds.push(tileId)
          this.idsByTileStartRow.delete(tileStartRow)
        }
      }
  
      const rowsPerTile = this.getRowsPerTile()
      this.renderedTileStartRows.length = this.getRenderedTileCount()
      for (let tileStartRow = startRow, i = 0; tileStartRow < endRow; tileStartRow = tileStartRow + rowsPerTile, i++) {
        this.renderedTileStartRows[i] = tileStartRow
        if (!this.idsByTileStartRow.has(tileStartRow)) {
          if (freeTileIds.length > 0) {
            this.idsByTileStartRow.set(tileStartRow, freeTileIds.shift())
          } else {
            this.idsByTileStartRow.set(tileStartRow, this.nextTileId++)
          }
        }
      }
  
      this.renderedTileStartRows.sort((a, b) => this.idsByTileStartRow.get(a) - this.idsByTileStartRow.get(b))
    }
  
    getNextUpdatePromise () {
      if (!this.nextUpdatePromise) {
        this.nextUpdatePromise = new Promise((resolve) => {
          this.resolveNextUpdatePromise = () => {
            this.nextUpdatePromise = null
            this.resolveNextUpdatePromise = null
            resolve()
          }
        })
      }
      return this.nextUpdatePromise
    }
  
    setInputEnabled (inputEnabled) {
      this.props.model.update({keyboardInputEnabled: inputEnabled})
    }
  
    isInputEnabled () {
      return !this.props.model.isReadOnly() && this.props.model.isKeyboardInputEnabled()
    }
  
    getHiddenInput () {
      return this.refs.cursorsAndInput.refs.hiddenInput
    }
  
    getPlatform () {
      return this.props.platform || process.platform
    }
  
    getChromeVersion () {
      return this.props.chromeVersion || parseInt(process.versions.chrome)
    }
  }
  
  class DummyScrollbarComponent {
    constructor (props) {
      this.props = props
      etch.initialize(this)
    }
  
    update (newProps) {
      const oldProps = this.props
      this.props = newProps
      etch.updateSync(this)
  
      const shouldFlushScrollPosition = (
        newProps.scrollTop !== oldProps.scrollTop ||
        newProps.scrollLeft !== oldProps.scrollLeft
      )
      if (shouldFlushScrollPosition) this.flushScrollPosition()
    }
  
    flushScrollPosition () {
      if (this.props.orientation === 'horizontal') {
        this.element.scrollLeft = this.props.scrollLeft
      } else {
        this.element.scrollTop = this.props.scrollTop
      }
    }
  
    render () {
      const {
        orientation, scrollWidth, scrollHeight,
        verticalScrollbarWidth, horizontalScrollbarHeight,
        canScroll, forceScrollbarVisible, didScroll
      } = this.props
  
      const outerStyle = {
        position: 'absolute',
        contain: 'content',
        zIndex: 1,
        willChange: 'transform'
      }
      if (!canScroll) outerStyle.visibility = 'hidden'
  
      const innerStyle = {}
      if (orientation === 'horizontal') {
        let right = (verticalScrollbarWidth || 0)
        outerStyle.bottom = 0
        outerStyle.left = 0
        outerStyle.right = right + 'px'
        outerStyle.height = '15px'
        outerStyle.overflowY = 'hidden'
        outerStyle.overflowX = forceScrollbarVisible ? 'scroll' : 'auto'
        outerStyle.cursor = 'default'
        innerStyle.height = '15px'
        innerStyle.width = (scrollWidth || 0) + 'px'
      } else {
        let bottom = (horizontalScrollbarHeight || 0)
        outerStyle.right = 0
        outerStyle.top = 0
        outerStyle.bottom = bottom + 'px'
        outerStyle.width = '15px'
        outerStyle.overflowX = 'hidden'
        outerStyle.overflowY = forceScrollbarVisible ? 'scroll' : 'auto'
        outerStyle.cursor = 'default'
        innerStyle.width = '15px'
        innerStyle.height = (scrollHeight || 0) + 'px'
      }
  
      return $.div(
        {
          className: `${orientation}-scrollbar`,
          style: outerStyle,
          on: {
            scroll: didScroll,
            mousedown: this.didMouseDown
          }
        },
        $.div({style: innerStyle})
      )
    }
  
    didMouseDown (event) {
      let {bottom, right} = this.element.getBoundingClientRect()
      const clickedOnScrollbar = (this.props.orientation === 'horizontal')
        ? event.clientY >= (bottom - this.getRealScrollbarHeight())
        : event.clientX >= (right - this.getRealScrollbarWidth())
      if (!clickedOnScrollbar) this.props.didMouseDown(event)
    }
  
    getRealScrollbarWidth () {
      return this.element.offsetWidth - this.element.clientWidth
    }
  
    getRealScrollbarHeight () {
      return this.element.offsetHeight - this.element.clientHeight
    }
  }
  
  class GutterContainerComponent {
    constructor (props) {
      this.props = props
      etch.initialize(this)
    }
  
    update (props) {
      if (this.shouldUpdate(props)) {
        this.props = props
        etch.updateSync(this)
      }
    }
  
    shouldUpdate (props) {
      return (
        !props.measuredContent ||
        props.lineNumberGutterWidth !== this.props.lineNumberGutterWidth
      )
    }
  
    render () {
      const {hasInitialMeasurements, scrollTop, scrollHeight, guttersToRender, decorationsToRender} = this.props
  
      const innerStyle = {
        willChange: 'transform',
        display: 'flex'
      }
  
      if (hasInitialMeasurements) {
        innerStyle.transform = `translateY(${-roundToPhysicalPixelBoundary(scrollTop)}px)`
      }
  
      return $.div(
        {
          ref: 'gutterContainer',
          key: 'gutterContainer',
          className: 'gutter-container',
          style: {
            position: 'relative',
            zIndex: 1,
            backgroundColor: 'inherit'
          }
        },
        $.div({style: innerStyle},
          guttersToRender.map((gutter) => {
            if (gutter.type === 'line-number') {
              return this.renderLineNumberGutter(gutter)
            } else {
              return $(CustomGutterComponent, {
                key: gutter,
                element: gutter.getElement(),
                name: gutter.name,
                visible: gutter.isVisible(),
                height: scrollHeight,
                decorations: decorationsToRender.customGutter.get(gutter.name)
              })
            }
          })
        )
      )
    }
  
    renderLineNumberGutter (gutter) {
      const {
        rootComponent, showLineNumbers, hasInitialMeasurements, lineNumbersToRender,
        renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration,
        scrollHeight, lineNumberGutterWidth, lineHeight
      } = this.props
  
      if (!gutter.isVisible()) {
        return null
      }
  
      const oneTrueLineNumberGutter = gutter.name === 'line-number'
      const ref = oneTrueLineNumberGutter ? 'lineNumberGutter' : undefined
      const width = oneTrueLineNumberGutter ? lineNumberGutterWidth : undefined
  
      if (hasInitialMeasurements) {
        const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender
        return $(LineNumberGutterComponent, {
          ref,
          element: gutter.getElement(),
          name: gutter.name,
          className: gutter.className,
          labelFn: gutter.labelFn,
          onMouseDown: gutter.onMouseDown,
          onMouseMove: gutter.onMouseMove,
          rootComponent: rootComponent,
          startRow: renderedStartRow,
          endRow: renderedEndRow,
          rowsPerTile: rowsPerTile,
          maxDigits: maxDigits,
          keys: keys,
          bufferRows: bufferRows,
          screenRows: screenRows,
          softWrappedFlags: softWrappedFlags,
          foldableFlags: foldableFlags,
          decorations: decorationsToRender.lineNumbers.get(gutter.name) || [],
          blockDecorations: decorationsToRender.blocks,
          didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration,
          height: scrollHeight,
          width,
          lineHeight: lineHeight,
          showLineNumbers
        })
      } else {
        return $(LineNumberGutterComponent, {
          ref,
          element: gutter.getElement(),
          name: gutter.name,
          className: gutter.className,
          onMouseDown: gutter.onMouseDown,
          onMouseMove: gutter.onMouseMove,
          maxDigits: lineNumbersToRender.maxDigits,
          showLineNumbers
        })
      }
    }
  }
  
  class LineNumberGutterComponent {
    constructor (props) {
      this.props = props
      this.element = this.props.element
      this.virtualNode = $.div(null)
      this.virtualNode.domNode = this.element
      this.nodePool = new NodePool()
      etch.updateSync(this)
    }
  
    update (newProps) {
      if (this.shouldUpdate(newProps)) {
        this.props = newProps
        etch.updateSync(this)
      }
    }
  
    render () {
      const {
        rootComponent, showLineNumbers, height, width, startRow, endRow, rowsPerTile,
        maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations,
        className
      } = this.props
  
      let children = null
  
      if (bufferRows) {
        children = new Array(rootComponent.renderedTileStartRows.length)
        for (let i = 0; i < rootComponent.renderedTileStartRows.length; i++) {
          const tileStartRow = rootComponent.renderedTileStartRows[i]
          const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile)
          const tileChildren = new Array(tileEndRow - tileStartRow)
          for (let row = tileStartRow; row < tileEndRow; row++) {
            const indexInTile = row - tileStartRow
            const j = row - startRow
            const key = keys[j]
            const softWrapped = softWrappedFlags[j]
            const foldable = foldableFlags[j]
            const bufferRow = bufferRows[j]
            const screenRow = screenRows[j]
  
            let className = 'line-number'
            if (foldable) className = className + ' foldable'
  
            const decorationsForRow = decorations[row - startRow]
            if (decorationsForRow) className = className + ' ' + decorationsForRow
  
            let number = null
            if (showLineNumbers) {
              if (this.props.labelFn == null) {
                number = softWrapped ? '•' : bufferRow + 1
                number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
              } else {
                number = this.props.labelFn({bufferRow, screenRow, foldable, softWrapped, maxDigits})
              }
            }
  
            // We need to adjust the line number position to account for block
            // decorations preceding the current row and following the preceding
            // row. Note that we ignore the latter when the line number starts at
            // the beginning of the tile, because the tile will already be
            // positioned to take into account block decorations added after the
            // last row of the previous tile.
            let marginTop = rootComponent.heightForBlockDecorationsBeforeRow(row)
            if (indexInTile > 0) marginTop += rootComponent.heightForBlockDecorationsAfterRow(row - 1)
  
            tileChildren[row - tileStartRow] = $(LineNumberComponent, {
              key,
              className,
              width,
              bufferRow,
              screenRow,
              number,
              marginTop,
              nodePool: this.nodePool
            })
          }
  
          const tileTop = rootComponent.pixelPositionBeforeBlocksForRow(tileStartRow)
          const tileBottom = rootComponent.pixelPositionBeforeBlocksForRow(tileEndRow)
          const tileHeight = tileBottom - tileTop
          const tileWidth = width != null && width > 0 ? width + 'px' : ''
  
          children[i] = $.div({
            key: rootComponent.idsByTileStartRow.get(tileStartRow),
            style: {
              contain: 'layout style',
              position: 'absolute',
              top: 0,
              height: tileHeight + 'px',
              width: tileWidth,
              transform: `translateY(${tileTop}px)`
            }
          }, ...tileChildren)
        }
      }
  
      let rootClassName = 'gutter line-numbers'
      if (className) {
        rootClassName += ' ' + className
      }
  
      return $.div(
        {
          className: rootClassName,
          attributes: {'gutter-name': this.props.name},
          style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'},
          on: {
            mousedown: this.didMouseDown,
            mousemove: this.didMouseMove
          }
        },
        $.div({key: 'placeholder', className: 'line-number dummy', style: {visibility: 'hidden'}},
          showLineNumbers ? '0'.repeat(maxDigits) : null,
          $.div({className: 'icon-right'})
        ),
        children
      )
    }
  
    shouldUpdate (newProps) {
      const oldProps = this.props
  
      if (oldProps.showLineNumbers !== newProps.showLineNumbers) return true
      if (oldProps.height !== newProps.height) return true
      if (oldProps.width !== newProps.width) return true
      if (oldProps.lineHeight !== newProps.lineHeight) return true
      if (oldProps.startRow !== newProps.startRow) return true
      if (oldProps.endRow !== newProps.endRow) return true
      if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true
      if (oldProps.maxDigits !== newProps.maxDigits) return true
      if (oldProps.labelFn !== newProps.labelFn) return true
      if (oldProps.className !== newProps.className) return true
      if (newProps.didMeasureVisibleBlockDecoration) return true
      if (!arraysEqual(oldProps.keys, newProps.keys)) return true
      if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true
      if (!arraysEqual(oldProps.foldableFlags, newProps.foldableFlags)) return true
      if (!arraysEqual(oldProps.decorations, newProps.decorations)) return true
  
      let oldTileStartRow = oldProps.startRow
      let newTileStartRow = newProps.startRow
      while (oldTileStartRow < oldProps.endRow || newTileStartRow < newProps.endRow) {
        let oldTileBlockDecorations = oldProps.blockDecorations.get(oldTileStartRow)
        let newTileBlockDecorations = newProps.blockDecorations.get(newTileStartRow)
  
        if (oldTileBlockDecorations && newTileBlockDecorations) {
          if (oldTileBlockDecorations.size !== newTileBlockDecorations.size) return true
  
          let blockDecorationsChanged = false
  
          oldTileBlockDecorations.forEach((oldDecorations, screenLineId) => {
            if (!blockDecorationsChanged) {
              const newDecorations = newTileBlockDecorations.get(screenLineId)
              blockDecorationsChanged = (newDecorations == null || !arraysEqual(oldDecorations, newDecorations))
            }
          })
          if (blockDecorationsChanged) return true
  
          newTileBlockDecorations.forEach((newDecorations, screenLineId) => {
            if (!blockDecorationsChanged) {
              const oldDecorations = oldTileBlockDecorations.get(screenLineId)
              blockDecorationsChanged = (oldDecorations == null)
            }
          })
          if (blockDecorationsChanged) return true
        } else if (oldTileBlockDecorations) {
          return true
        } else if (newTileBlockDecorations) {
          return true
        }
  
        oldTileStartRow += oldProps.rowsPerTile
        newTileStartRow += newProps.rowsPerTile
      }
  
      return false
    }
  
    didMouseDown (event) {
      if (this.props.onMouseDown == null) {
        this.props.rootComponent.didMouseDownOnLineNumberGutter(event)
      } else {
        const {bufferRow, screenRow} = event.target.dataset
        this.props.onMouseDown({
          bufferRow: parseInt(bufferRow, 10),
          screenRow: parseInt(screenRow, 10),
          domEvent: event
        })
      }
    }
  
    didMouseMove (event) {
      if (this.props.onMouseMove != null) {
        const {bufferRow, screenRow} = event.target.dataset
        this.props.onMouseMove({
          bufferRow: parseInt(bufferRow, 10),
          screenRow: parseInt(screenRow, 10),
          domEvent: event
        })
      }
    }
  }
  
  class LineNumberComponent {
    constructor (props) {
      const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props
      this.props = props
      const style = {}
      if (width != null && width > 0) style.width = width + 'px'
      if (marginTop != null && marginTop > 0) style.marginTop = marginTop + 'px'
      this.element = nodePool.getElement('DIV', className, style)
      this.element.dataset.bufferRow = bufferRow
      this.element.dataset.screenRow = screenRow
      if (number) this.element.appendChild(nodePool.getTextNode(number))
      this.element.appendChild(nodePool.getElement('DIV', 'icon-right', null))
    }
  
    destroy () {
      this.element.remove()
      this.props.nodePool.release(this.element)
    }
  
    update (props) {
      const {nodePool, className, width, marginTop, bufferRow, screenRow, number} = props
  
      if (this.props.bufferRow !== bufferRow) this.element.dataset.bufferRow = bufferRow
      if (this.props.screenRow !== screenRow) this.element.dataset.screenRow = screenRow
      if (this.props.className !== className) this.element.className = className
      if (this.props.width !== width) {
        if (width != null && width > 0) {
          this.element.style.width = width + 'px'
        } else {
          this.element.style.width = ''
        }
      }
      if (this.props.marginTop !== marginTop) {
        if (marginTop != null && marginTop > 0) {
          this.element.style.marginTop = marginTop + 'px'
        } else {
          this.element.style.marginTop = ''
        }
      }
  
      if (this.props.number !== number) {
        if (this.props.number != null) {
          const numberNode = this.element.firstChild
          numberNode.remove()
          nodePool.release(numberNode)
        }
  
        if (number != null) {
          this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild)
        }
      }
  
      this.props = props
    }
  }
  
  class CustomGutterComponent {
    constructor (props) {
      this.props = props
      this.element = this.props.element
      this.virtualNode = $.div(null)
      this.virtualNode.domNode = this.element
      etch.updateSync(this)
    }
  
    update (props) {
      this.props = props
      etch.updateSync(this)
    }
  
    destroy () {
      etch.destroy(this)
    }
  
    render () {
      let className = 'gutter'
      if (this.props.className) {
        className += ' ' + this.props.className
      }
      return $.div(
        {
          className,
          attributes: {'gutter-name': this.props.name},
          style: {
            display: this.props.visible ? '' : 'none'
          }
        },
        $.div(
          {
            className: 'custom-decorations',
            style: {height: this.props.height + 'px'}
          },
          this.renderDecorations()
        )
      )
    }
  
    renderDecorations () {
      if (!this.props.decorations) return null
  
      return this.props.decorations.map(({className, element, top, height}) => {
        return $(CustomGutterDecorationComponent, {
          className,
          element,
          top,
          height
        })
      })
    }
  }
  
  class CustomGutterDecorationComponent {
    constructor (props) {
      this.props = props
      this.element = document.createElement('div')
      const {top, height, className, element} = this.props
  
      this.element.style.position = 'absolute'
      this.element.style.top = top + 'px'
      this.element.style.height = height + 'px'
      if (className != null) this.element.className = className
      if (element != null) {
        this.element.appendChild(element)
        element.style.height = height + 'px'
      }
    }
  
    update (newProps) {
      const oldProps = this.props
      this.props = newProps
  
      if (newProps.top !== oldProps.top) this.element.style.top = newProps.top + 'px'
      if (newProps.height !== oldProps.height) {
        this.element.style.height = newProps.height + 'px'
        if (newProps.element) newProps.element.style.height = newProps.height + 'px'
      }
      if (newProps.className !== oldProps.className) this.element.className = newProps.className || ''
      if (newProps.element !== oldProps.element) {
        if (this.element.firstChild) this.element.firstChild.remove()
        if (newProps.element != null) {
          this.element.appendChild(newProps.element)
          newProps.element.style.height = newProps.height + 'px'
        }
      }
    }
  }
  
  class CursorsAndInputComponent {
    constructor (props) {
      this.props = props
      etch.initialize(this)
    }
  
    update (props) {
      if (props.measuredContent) {
        this.props = props
        etch.updateSync(this)
      }
    }
  
    updateCursorBlinkSync (cursorsBlinkedOff) {
      this.props.cursorsBlinkedOff = cursorsBlinkedOff
      const className = this.getCursorsClassName()
      this.refs.cursors.className = className
      this.virtualNode.props.className = className
    }
  
    render () {
      const {lineHeight, decorationsToRender, scrollHeight, scrollWidth} = this.props
  
      const className = this.getCursorsClassName()
      const cursorHeight = lineHeight + 'px'
  
      const children = [this.renderHiddenInput()]
      for (let i = 0; i < decorationsToRender.cursors.length; i++) {
        const {pixelLeft, pixelTop, pixelWidth, className: extraCursorClassName, style: extraCursorStyle} = decorationsToRender.cursors[i]
        let cursorClassName = 'cursor'
        if (extraCursorClassName) cursorClassName += ' ' + extraCursorClassName
  
        const cursorStyle = {
          height: cursorHeight,
          width: Math.min(pixelWidth, scrollWidth - pixelLeft) + 'px',
          transform: `translate(${pixelLeft}px, ${pixelTop}px)`
        }
        if (extraCursorStyle) Object.assign(cursorStyle, extraCursorStyle)
  
        children.push($.div({
          className: cursorClassName,
          style: cursorStyle
        }))
      }
  
      return $.div({
        key: 'cursors',
        ref: 'cursors',
        className,
        style: {
          position: 'absolute',
          contain: 'strict',
          zIndex: 1,
          width: scrollWidth + 'px',
          height: scrollHeight + 'px',
          pointerEvents: 'none',
          userSelect: 'none'
        }
      }, children)
    }
  
    getCursorsClassName () {
      return this.props.cursorsBlinkedOff ? 'cursors blink-off' : 'cursors'
    }
  
    renderHiddenInput () {
      const {
        lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput,
        didPaste, didTextInput, didKeydown, didKeyup, didKeypress,
        didCompositionStart, didCompositionUpdate, didCompositionEnd, tabIndex
      } = this.props
  
      let top, left
      if (hiddenInputPosition) {
        top = hiddenInputPosition.pixelTop
        left = hiddenInputPosition.pixelLeft
      } else {
        top = 0
        left = 0
      }
  
      return $.input({
        ref: 'hiddenInput',
        key: 'hiddenInput',
        className: 'hidden-input',
        on: {
          blur: didBlurHiddenInput,
          focus: didFocusHiddenInput,
          paste: didPaste,
          textInput: didTextInput,
          keydown: didKeydown,
          keyup: didKeyup,
          keypress: didKeypress,
          compositionstart: didCompositionStart,
          compositionupdate: didCompositionUpdate,
          compositionend: didCompositionEnd
        },
        tabIndex: tabIndex,
        style: {
          position: 'absolute',
          width: '1px',
          height: lineHeight + 'px',
          top: top + 'px',
          left: left + 'px',
          opacity: 0,
          padding: 0,
          border: 0
        }
      })
    }
  }
  
  class LinesTileComponent {
    constructor (props) {
      this.props = props
      etch.initialize(this)
      this.createLines()
      this.updateBlockDecorations({}, props)
    }
  
    update (newProps) {
      if (this.shouldUpdate(newProps)) {
        const oldProps = this.props
        this.props = newProps
        etch.updateSync(this)
        if (!newProps.measuredContent) {
          this.updateLines(oldProps, newProps)
          this.updateBlockDecorations(oldProps, newProps)
        }
      }
    }
  
    destroy () {
      for (let i = 0; i < this.lineComponents.length; i++) {
        this.lineComponents[i].destroy()
      }
      this.lineComponents.length = 0
  
      return etch.destroy(this)
    }
  
    render () {
      const {height, width, top} = this.props
  
      return $.div(
        {
          style: {
            contain: 'layout style',
            position: 'absolute',
            height: height + 'px',
            width: width + 'px',
            transform: `translateY(${top}px)`
          }
        }
        // Lines and block decorations will be manually inserted here for efficiency
      )
    }
  
    createLines () {
      const {
        tileStartRow, screenLines, lineDecorations, textDecorations,
        nodePool, displayLayer, lineComponentsByScreenLineId
      } = this.props
  
      this.lineComponents = []
      for (let i = 0, length = screenLines.length; i < length; i++) {
        const component = new LineComponent({
          screenLine: screenLines[i],
          screenRow: tileStartRow + i,
          lineDecoration: lineDecorations[i],
          textDecorations: textDecorations[i],
          displayLayer,
          nodePool,
          lineComponentsByScreenLineId
        })
        this.element.appendChild(component.element)
        this.lineComponents.push(component)
      }
    }
  
    updateLines (oldProps, newProps) {
      var {
        screenLines, tileStartRow, lineDecorations, textDecorations,
        nodePool, displayLayer, lineComponentsByScreenLineId
      } = newProps
  
      var oldScreenLines = oldProps.screenLines
      var newScreenLines = screenLines
      var oldScreenLinesEndIndex = oldScreenLines.length
      var newScreenLinesEndIndex = newScreenLines.length
      var oldScreenLineIndex = 0
      var newScreenLineIndex = 0
      var lineComponentIndex = 0
  
      while (oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex) {
        var oldScreenLine = oldScreenLines[oldScreenLineIndex]
        var newScreenLine = newScreenLines[newScreenLineIndex]
  
        if (oldScreenLineIndex >= oldScreenLinesEndIndex) {
          var newScreenLineComponent = new LineComponent({
            screenLine: newScreenLine,
            screenRow: tileStartRow + newScreenLineIndex,
            lineDecoration: lineDecorations[newScreenLineIndex],
            textDecorations: textDecorations[newScreenLineIndex],
            displayLayer,
            nodePool,
            lineComponentsByScreenLineId
          })
          this.element.appendChild(newScreenLineComponent.element)
          this.lineComponents.push(newScreenLineComponent)
  
          newScreenLineIndex++
          lineComponentIndex++
        } else if (newScreenLineIndex >= newScreenLinesEndIndex) {
          this.lineComponents[lineComponentIndex].destroy()
          this.lineComponents.splice(lineComponentIndex, 1)
  
          oldScreenLineIndex++
        } else if (oldScreenLine === newScreenLine) {
          var lineComponent = this.lineComponents[lineComponentIndex]
          lineComponent.update({
            screenRow: tileStartRow + newScreenLineIndex,
            lineDecoration: lineDecorations[newScreenLineIndex],
            textDecorations: textDecorations[newScreenLineIndex]
          })
  
          oldScreenLineIndex++
          newScreenLineIndex++
          lineComponentIndex++
        } else {
          var oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf(oldScreenLine)
          var newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf(newScreenLine)
          if (newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex) {
            var newScreenLineComponents = []
            while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) {
              var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare
                screenLine: newScreenLines[newScreenLineIndex],
                screenRow: tileStartRow + newScreenLineIndex,
                lineDecoration: lineDecorations[newScreenLineIndex],
                textDecorations: textDecorations[newScreenLineIndex],
                displayLayer,
                nodePool,
                lineComponentsByScreenLineId
              })
              this.element.insertBefore(newScreenLineComponent.element, this.getFirstElementForScreenLine(oldProps, oldScreenLine))
              newScreenLineComponents.push(newScreenLineComponent)
  
              newScreenLineIndex++
            }
  
            this.lineComponents.splice(lineComponentIndex, 0, ...newScreenLineComponents)
            lineComponentIndex = lineComponentIndex + newScreenLineComponents.length
          } else if (oldScreenLineIndex < newScreenLineIndexInOldScreenLines && newScreenLineIndexInOldScreenLines < oldScreenLinesEndIndex) {
            while (oldScreenLineIndex < newScreenLineIndexInOldScreenLines) {
              this.lineComponents[lineComponentIndex].destroy()
              this.lineComponents.splice(lineComponentIndex, 1)
  
              oldScreenLineIndex++
            }
          } else {
            var oldScreenLineComponent = this.lineComponents[lineComponentIndex]
            var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare
              screenLine: newScreenLines[newScreenLineIndex],
              screenRow: tileStartRow + newScreenLineIndex,
              lineDecoration: lineDecorations[newScreenLineIndex],
              textDecorations: textDecorations[newScreenLineIndex],
              displayLayer,
              nodePool,
              lineComponentsByScreenLineId
            })
            this.element.insertBefore(newScreenLineComponent.element, oldScreenLineComponent.element)
            oldScreenLineComponent.destroy()
            this.lineComponents[lineComponentIndex] = newScreenLineComponent
  
            oldScreenLineIndex++
            newScreenLineIndex++
            lineComponentIndex++
          }
        }
      }
    }
  
    getFirstElementForScreenLine (oldProps, screenLine) {
      var blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null
      if (blockDecorations) {
        var blockDecorationElementsBeforeOldScreenLine = []
        for (let i = 0; i < blockDecorations.length; i++) {
          var decoration = blockDecorations[i]
          if (decoration.position !== 'after') {
            blockDecorationElementsBeforeOldScreenLine.push(
              TextEditor.viewForItem(decoration.item)
            )
          }
        }
  
        for (let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++) {
          var blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i]
          if (!blockDecorationElementsBeforeOldScreenLine.includes(blockDecorationElement.previousSibling)) {
            return blockDecorationElement
          }
        }
      }
  
      return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element
    }
  
    updateBlockDecorations (oldProps, newProps) {
      var {blockDecorations, lineComponentsByScreenLineId} = newProps
  
      if (oldProps.blockDecorations) {
        oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => {
          var newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null
          for (var i = 0; i < oldDecorations.length; i++) {
            var oldDecoration = oldDecorations[i]
            if (newDecorations && newDecorations.includes(oldDecoration)) continue
  
            var element = TextEditor.viewForItem(oldDecoration.item)
            if (element.parentElement !== this.element) continue
  
            element.remove()
          }
        })
      }
  
      if (blockDecorations) {
        blockDecorations.forEach((newDecorations, screenLineId) => {
          const oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null
          const lineNode = lineComponentsByScreenLineId.get(screenLineId).element
          let lastAfter = lineNode
  
          for (let i = 0; i < newDecorations.length; i++) {
            const newDecoration = newDecorations[i]
            const element = TextEditor.viewForItem(newDecoration.item)
  
            if (oldDecorations && oldDecorations.includes(newDecoration)) {
              if (newDecoration.position === 'after') {
                lastAfter = element
              }
              continue
            }
  
            if (newDecoration.position === 'after') {
              this.element.insertBefore(element, lastAfter.nextSibling)
              lastAfter = element
            } else {
              this.element.insertBefore(element, lineNode)
            }
          }
        })
      }
    }
  
    shouldUpdate (newProps) {
      const oldProps = this.props
      if (oldProps.top !== newProps.top) return true
      if (oldProps.height !== newProps.height) return true
      if (oldProps.width !== newProps.width) return true
      if (oldProps.lineHeight !== newProps.lineHeight) return true
      if (oldProps.tileStartRow !== newProps.tileStartRow) return true
      if (oldProps.tileEndRow !== newProps.tileEndRow) return true
      if (!arraysEqual(oldProps.screenLines, newProps.screenLines)) return true
      if (!arraysEqual(oldProps.lineDecorations, newProps.lineDecorations)) return true
  
      if (oldProps.blockDecorations && newProps.blockDecorations) {
        if (oldProps.blockDecorations.size !== newProps.blockDecorations.size) return true
  
        let blockDecorationsChanged = false
  
        oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => {
          if (!blockDecorationsChanged) {
            const newDecorations = newProps.blockDecorations.get(screenLineId)
            blockDecorationsChanged = (newDecorations == null || !arraysEqual(oldDecorations, newDecorations))
          }
        })
        if (blockDecorationsChanged) return true
  
        newProps.blockDecorations.forEach((newDecorations, screenLineId) => {
          if (!blockDecorationsChanged) {
            const oldDecorations = oldProps.blockDecorations.get(screenLineId)
            blockDecorationsChanged = (oldDecorations == null)
          }
        })
        if (blockDecorationsChanged) return true
      } else if (oldProps.blockDecorations) {
        return true
      } else if (newProps.blockDecorations) {
        return true
      }
  
      if (oldProps.textDecorations.length !== newProps.textDecorations.length) return true
      for (let i = 0; i < oldProps.textDecorations.length; i++) {
        if (!textDecorationsEqual(oldProps.textDecorations[i], newProps.textDecorations[i])) return true
      }
  
      return false
    }
  }
  
  class LineComponent {
    constructor (props) {
      const {nodePool, screenRow, screenLine, lineComponentsByScreenLineId, offScreen} = props
      this.props = props
      this.element = nodePool.getElement('DIV', this.buildClassName(), null)
      this.element.dataset.screenRow = screenRow
      this.textNodes = []
  
      if (offScreen) {
        this.element.style.position = 'absolute'
        this.element.style.visibility = 'hidden'
        this.element.dataset.offScreen = true
      }
  
      this.appendContents()
      lineComponentsByScreenLineId.set(screenLine.id, this)
    }
  
    update (newProps) {
      if (this.props.lineDecoration !== newProps.lineDecoration) {
        this.props.lineDecoration = newProps.lineDecoration
        this.element.className = this.buildClassName()
      }
  
      if (this.props.screenRow !== newProps.screenRow) {
        this.props.screenRow = newProps.screenRow
        this.element.dataset.screenRow = newProps.screenRow
      }
  
      if (!textDecorationsEqual(this.props.textDecorations, newProps.textDecorations)) {
        this.props.textDecorations = newProps.textDecorations
        this.element.firstChild.remove()
        this.appendContents()
      }
    }
  
    destroy () {
      const {nodePool, lineComponentsByScreenLineId, screenLine} = this.props
  
      if (lineComponentsByScreenLineId.get(screenLine.id) === this) {
        lineComponentsByScreenLineId.delete(screenLine.id)
      }
  
      this.element.remove()
      nodePool.release(this.element)
    }
  
    appendContents () {
      const {displayLayer, nodePool, screenLine, textDecorations} = this.props
  
      this.textNodes.length = 0
  
      const {lineText, tags} = screenLine
      let openScopeNode = nodePool.getElement('SPAN', null, null)
      this.element.appendChild(openScopeNode)
  
      let decorationIndex = 0
      let column = 0
      let activeClassName = null
      let activeStyle = null
      let nextDecoration = textDecorations ? textDecorations[decorationIndex] : null
      if (nextDecoration && nextDecoration.column === 0) {
        column = nextDecoration.column
        activeClassName = nextDecoration.className
        activeStyle = nextDecoration.style
        nextDecoration = textDecorations[++decorationIndex]
      }
  
      for (let i = 0; i < tags.length; i++) {
        const tag = tags[i]
        if (tag !== 0) {
          if (displayLayer.isCloseTag(tag)) {
            openScopeNode = openScopeNode.parentElement
          } else if (displayLayer.isOpenTag(tag)) {
            const newScopeNode = nodePool.getElement('SPAN', displayLayer.classNameForTag(tag), null)
            openScopeNode.appendChild(newScopeNode)
            openScopeNode = newScopeNode
          } else {
            const nextTokenColumn = column + tag
            while (nextDecoration && nextDecoration.column <= nextTokenColumn) {
              const text = lineText.substring(column, nextDecoration.column)
              this.appendTextNode(openScopeNode, text, activeClassName, activeStyle)
              column = nextDecoration.column
              activeClassName = nextDecoration.className
              activeStyle = nextDecoration.style
              nextDecoration = textDecorations[++decorationIndex]
            }
  
            if (column < nextTokenColumn) {
              const text = lineText.substring(column, nextTokenColumn)
              this.appendTextNode(openScopeNode, text, activeClassName, activeStyle)
              column = nextTokenColumn
            }
          }
        }
      }
  
      if (column === 0) {
        const textNode = nodePool.getTextNode(' ')
        this.element.appendChild(textNode)
        this.textNodes.push(textNode)
      }
  
      if (lineText.endsWith(displayLayer.foldCharacter)) {
        // Insert a zero-width non-breaking whitespace, so that LinesYardstick can
        // take the fold-marker::after pseudo-element into account during
        // measurements when such marker is the last character on the line.
        const textNode = nodePool.getTextNode(ZERO_WIDTH_NBSP_CHARACTER)
        this.element.appendChild(textNode)
        this.textNodes.push(textNode)
      }
    }
  
    appendTextNode (openScopeNode, text, activeClassName, activeStyle) {
      const {nodePool} = this.props
  
      if (activeClassName || activeStyle) {
        const decorationNode = nodePool.getElement('SPAN', activeClassName, activeStyle)
        openScopeNode.appendChild(decorationNode)
        openScopeNode = decorationNode
      }
  
      const textNode = nodePool.getTextNode(text)
      openScopeNode.appendChild(textNode)
      this.textNodes.push(textNode)
    }
  
    buildClassName () {
      const {lineDecoration} = this.props
      let className = 'line'
      if (lineDecoration != null) className = className + ' ' + lineDecoration
      return className
    }
  }
  
  class HighlightsComponent {
    constructor (props) {
      this.props = {}
      this.element = document.createElement('div')
      this.element.className = 'highlights'
      this.element.style.contain = 'strict'
      this.element.style.position = 'absolute'
      this.element.style.overflow = 'hidden'
      this.element.style.userSelect = 'none'
      this.highlightComponentsByKey = new Map()
      this.update(props)
    }
  
    destroy () {
      this.highlightComponentsByKey.forEach((highlightComponent) => {
        highlightComponent.destroy()
      })
      this.highlightComponentsByKey.clear()
    }
  
    update (newProps) {
      if (this.shouldUpdate(newProps)) {
        this.props = newProps
        const {height, width, lineHeight, highlightDecorations} = this.props
  
        this.element.style.height = height + 'px'
        this.element.style.width = width + 'px'
  
        const visibleHighlightDecorations = new Set()
        if (highlightDecorations) {
          for (let i = 0; i < highlightDecorations.length; i++) {
            const highlightDecoration = highlightDecorations[i]
            const highlightProps = Object.assign({lineHeight}, highlightDecorations[i])
  
            let highlightComponent = this.highlightComponentsByKey.get(highlightDecoration.key)
            if (highlightComponent) {
              highlightComponent.update(highlightProps)
            } else {
              highlightComponent = new HighlightComponent(highlightProps)
              this.element.appendChild(highlightComponent.element)
              this.highlightComponentsByKey.set(highlightDecoration.key, highlightComponent)
            }
  
            highlightDecorations[i].flashRequested = false
            visibleHighlightDecorations.add(highlightDecoration.key)
          }
        }
  
        this.highlightComponentsByKey.forEach((highlightComponent, key) => {
          if (!visibleHighlightDecorations.has(key)) {
            highlightComponent.destroy()
            this.highlightComponentsByKey.delete(key)
          }
        })
      }
    }
  
    shouldUpdate (newProps) {
      const oldProps = this.props
  
      if (!newProps.hasInitialMeasurements) return false
  
      if (oldProps.width !== newProps.width) return true
      if (oldProps.height !== newProps.height) return true
      if (oldProps.lineHeight !== newProps.lineHeight) return true
      if (!oldProps.highlightDecorations && newProps.highlightDecorations) return true
      if (oldProps.highlightDecorations && !newProps.highlightDecorations) return true
      if (oldProps.highlightDecorations && newProps.highlightDecorations) {
        if (oldProps.highlightDecorations.length !== newProps.highlightDecorations.length) return true
  
        for (let i = 0, length = oldProps.highlightDecorations.length; i < length; i++) {
          const oldHighlight = oldProps.highlightDecorations[i]
          const newHighlight = newProps.highlightDecorations[i]
          if (oldHighlight.className !== newHighlight.className) return true
          if (newHighlight.flashRequested) return true
          if (oldHighlight.startPixelTop !== newHighlight.startPixelTop) return true
          if (oldHighlight.startPixelLeft !== newHighlight.startPixelLeft) return true
          if (oldHighlight.endPixelTop !== newHighlight.endPixelTop) return true
          if (oldHighlight.endPixelLeft !== newHighlight.endPixelLeft) return true
          if (!oldHighlight.screenRange.isEqual(newHighlight.screenRange)) return true
        }
      }
    }
  }
  
  class HighlightComponent {
    constructor (props) {
      this.props = props
      etch.initialize(this)
      if (this.props.flashRequested) this.performFlash()
    }
  
    destroy () {
      if (this.timeoutsByClassName) {
        this.timeoutsByClassName.forEach((timeout) => {
          window.clearTimeout(timeout)
        })
        this.timeoutsByClassName.clear()
      }
  
      return etch.destroy(this)
    }
  
    update (newProps) {
      this.props = newProps
      etch.updateSync(this)
      if (newProps.flashRequested) this.performFlash()
    }
  
    performFlash () {
      const {flashClass, flashDuration} = this.props
      if (!this.timeoutsByClassName) this.timeoutsByClassName = new Map()
  
      // If a flash of this class is already in progress, clear it early and
      // flash again on the next frame to ensure CSS transitions apply to the
      // second flash.
      if (this.timeoutsByClassName.has(flashClass)) {
        window.clearTimeout(this.timeoutsByClassName.get(flashClass))
        this.timeoutsByClassName.delete(flashClass)
        this.element.classList.remove(flashClass)
        requestAnimationFrame(() => this.performFlash())
      } else {
        this.element.classList.add(flashClass)
        this.timeoutsByClassName.set(flashClass, window.setTimeout(() => {
          this.element.classList.remove(flashClass)
        }, flashDuration))
      }
    }
  
    render () {
      const {
        className, screenRange, lineHeight,
        startPixelTop, startPixelLeft, endPixelTop, endPixelLeft
      } = this.props
      const regionClassName = 'region ' + className
  
      let children
      if (screenRange.start.row === screenRange.end.row) {
        children = $.div({
          className: regionClassName,
          style: {
            position: 'absolute',
            boxSizing: 'border-box',
            top: startPixelTop + 'px',
            left: startPixelLeft + 'px',
            width: endPixelLeft - startPixelLeft + 'px',
            height: lineHeight + 'px'
          }
        })
      } else {
        children = []
        children.push($.div({
          className: regionClassName,
          style: {
            position: 'absolute',
            boxSizing: 'border-box',
            top: startPixelTop + 'px',
            left: startPixelLeft + 'px',
            right: 0,
            height: lineHeight + 'px'
          }
        }))
  
        if (screenRange.end.row - screenRange.start.row > 1) {
          children.push($.div({
            className: regionClassName,
            style: {
              position: 'absolute',
              boxSizing: 'border-box',
              top: startPixelTop + lineHeight + 'px',
              left: 0,
              right: 0,
              height: endPixelTop - startPixelTop - (lineHeight * 2) + 'px'
            }
          }))
        }
  
        if (endPixelLeft > 0) {
          children.push($.div({
            className: regionClassName,
            style: {
              position: 'absolute',
              boxSizing: 'border-box',
              top: endPixelTop - lineHeight + 'px',
              left: 0,
              width: endPixelLeft + 'px',
              height: lineHeight + 'px'
            }
          }))
        }
      }
  
      return $.div({className: 'highlight ' + className}, children)
    }
  }
  
  class OverlayComponent {
    constructor (props) {
      this.props = props
      this.element = document.createElement('atom-overlay')
      if (this.props.className != null) this.element.classList.add(this.props.className)
      this.element.appendChild(this.props.element)
      this.element.style.position = 'fixed'
      this.element.style.zIndex = 4
      this.element.style.top = (this.props.pixelTop || 0) + 'px'
      this.element.style.left = (this.props.pixelLeft || 0) + 'px'
      this.currentContentRect = null
  
      // Synchronous DOM updates in response to resize events might trigger a
      // "loop limit exceeded" error. We disconnect the observer before
      // potentially mutating the DOM, and then reconnect it on the next tick.
      // Note: ResizeObserver calls its callback when .observe is called
      this.resizeObserver = new ResizeObserver((entries) => {
        const {contentRect} = entries[0]
  
        if (
          this.currentContentRect &&
          (this.currentContentRect.width !== contentRect.width ||
            this.currentContentRect.height !== contentRect.height)
        ) {
          this.resizeObserver.disconnect()
          this.props.didResize(this)
          process.nextTick(() => { this.resizeObserver.observe(this.props.element) })
        }
  
        this.currentContentRect = contentRect
      })
      this.didAttach()
      this.props.overlayComponents.add(this)
    }
  
    destroy () {
      this.props.overlayComponents.delete(this)
      this.didDetach()
    }
  
    getNextUpdatePromise () {
      if (!this.nextUpdatePromise) {
        this.nextUpdatePromise = new Promise((resolve) => {
          this.resolveNextUpdatePromise = () => {
            this.nextUpdatePromise = null
            this.resolveNextUpdatePromise = null
            resolve()
          }
        })
      }
      return this.nextUpdatePromise
    }
  
    update (newProps) {
      const oldProps = this.props
      this.props = Object.assign({}, oldProps, newProps)
      if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px'
      if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px'
      if (newProps.className !== oldProps.className) {
        if (oldProps.className != null) this.element.classList.remove(oldProps.className)
        if (newProps.className != null) this.element.classList.add(newProps.className)
      }
  
      if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise()
    }
  
    didAttach () {
      this.resizeObserver.observe(this.props.element)
    }
  
    didDetach () {
      this.resizeObserver.disconnect()
    }
  }
  
  let rangeForMeasurement
  function clientRectForRange (textNode, startIndex, endIndex) {
    if (!rangeForMeasurement) rangeForMeasurement = document.createRange()
    rangeForMeasurement.setStart(textNode, startIndex)
    rangeForMeasurement.setEnd(textNode, endIndex)
    return rangeForMeasurement.getBoundingClientRect()
  }
  
  function textDecorationsEqual (oldDecorations, newDecorations) {
    if (!oldDecorations && newDecorations) return false
    if (oldDecorations && !newDecorations) return false
    if (oldDecorations && newDecorations) {
      if (oldDecorations.length !== newDecorations.length) return false
      for (let j = 0; j < oldDecorations.length; j++) {
        if (oldDecorations[j].column !== newDecorations[j].column) return false
        if (oldDecorations[j].className !== newDecorations[j].className) return false
        if (!objectsEqual(oldDecorations[j].style, newDecorations[j].style)) return false
      }
    }
    return true
  }
  
  function arraysEqual (a, b) {
    if (a.length !== b.length) return false
    for (let i = 0, length = a.length; i < length; i++) {
      if (a[i] !== b[i]) return false
    }
    return true
  }
  
  function objectsEqual (a, b) {
    if (!a && b) return false
    if (a && !b) return false
    if (a && b) {
      for (const key in a) {
        if (a[key] !== b[key]) return false
      }
      for (const key in b) {
        if (a[key] !== b[key]) return false
      }
    }
    return true
  }
  
  function constrainRangeToRows (range, startRow, endRow) {
    if (range.start.row < startRow || range.end.row >= endRow) {
      range = range.copy()
      if (range.start.row < startRow) {
        range.start.row = startRow
        range.start.column = 0
      }
      if (range.end.row >= endRow) {
        range.end.row = endRow
        range.end.column = 0
      }
    }
    return range
  }
  
  function debounce (fn, wait) {
    let timestamp, timeout
  
    function later () {
      const last = Date.now() - timestamp
      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last)
      } else {
        timeout = null
        fn()
      }
    }
  
    return function () {
      timestamp = Date.now()
      if (!timeout) timeout = setTimeout(later, wait)
    }
  }
  
  class NodePool {
    constructor () {
      this.elementsByType = {}
      this.textNodes = []
    }
  
    getElement (type, className, style) {
      var element
      var elementsByDepth = this.elementsByType[type]
      if (elementsByDepth) {
        while (elementsByDepth.length > 0) {
          var elements = elementsByDepth[elementsByDepth.length - 1]
          if (elements && elements.length > 0) {
            element = elements.pop()
            if (elements.length === 0) elementsByDepth.pop()
            break
          } else {
            elementsByDepth.pop()
          }
        }
      }
  
      if (element) {
        element.className = className || ''
        element.styleMap.forEach((value, key) => {
          if (!style || style[key] == null) element.style[key] = ''
        })
        if (style) Object.assign(element.style, style)
        for (const key in element.dataset) delete element.dataset[key]
        while (element.firstChild) element.firstChild.remove()
        return element
      } else {
        var newElement = document.createElement(type)
        if (className) newElement.className = className
        if (style) Object.assign(newElement.style, style)
        return newElement
      }
    }
  
    getTextNode (text) {
      if (this.textNodes.length > 0) {
        var node = this.textNodes.pop()
        node.textContent = text
        return node
      } else {
        return document.createTextNode(text)
      }
    }
  
    release (node, depth = 0) {
      var {nodeName} = node
      if (nodeName === '#text') {
        this.textNodes.push(node)
      } else {
        var elementsByDepth = this.elementsByType[nodeName]
        if (!elementsByDepth) {
          elementsByDepth = []
          this.elementsByType[nodeName] = elementsByDepth
        }
  
        var elements = elementsByDepth[depth]
        if (!elements) {
          elements = []
          elementsByDepth[depth] = elements
        }
  
        elements.push(node)
        for (var i = 0; i < node.childNodes.length; i++) {
          this.release(node.childNodes[i], depth + 1)
        }
      }
    }
  }
  
  function roundToPhysicalPixelBoundary (virtualPixelPosition) {
    const virtualPixelsPerPhysicalPixel = (1 / window.devicePixelRatio)
    return Math.round(virtualPixelPosition / virtualPixelsPerPhysicalPixel) * virtualPixelsPerPhysicalPixel
  }
  
  function ceilToPhysicalPixelBoundary (virtualPixelPosition) {
    const virtualPixelsPerPhysicalPixel = (1 / window.devicePixelRatio)
    return Math.ceil(virtualPixelPosition / virtualPixelsPerPhysicalPixel) * virtualPixelsPerPhysicalPixel
  }
  