OpenC3/cosmos

View on GitHub
openc3-cosmos-init/plugins/packages/openc3-cosmos-ace-diff/src/index.js

Summary

Maintainability
F
6 days
Test Coverage
/* eslint-disable no-console */

/* Using local update by @dmsnell:
https://github.com/dmsnell/diff-match-patch/blob/issues/69-broken-surrogate-pairs/javascript/diff_match_patch_uncompressed.js
*/
import diff_match_patch from './diff-match-patch.js'
import merge from './helpers/merge.js'
import throttle from './helpers/throttle.js'
import debounce from './helpers/debounce.js'
import normalizeContent from './helpers/normalizeContent.js'

import getCurve from './visuals/getCurve.js'
import getMode from './visuals/getMode.js'
import getTheme from './visuals/getTheme.js'
import getLine from './visuals/getLine.js'
import getEditorHeight from './visuals/getEditorHeight.js'
import createArrow from './visuals/createArrow.js'

import ensureElement from './dom/ensureElement.js'
import query from './dom/query.js'
import C from './constants.js'

// Range module placeholder
let Range

function getRangeModule(ace) {
  if (ace.Range) {
    return ace.Range
  }

  const requireFunc = ace.acequire || ace.require
  if (requireFunc) {
    return requireFunc('ace/range')
  }

  return false
}

// our constructor
export default function AceDiff(options = {}) {
  // Ensure instance is a constructor with `new`
  if (!(this instanceof AceDiff)) {
    return new AceDiff(options)
  }

  // Current instance we pass around to other functions
  const acediff = this
  const getDefaultAce = () => (window ? window.ace : undefined)

  acediff.options = merge(
    {
      ace: getDefaultAce(),
      mode: null,
      theme: null,
      element: null,
      diffGranularity: C.DIFF_GRANULARITY_BROAD,
      lockScrolling: false, // not implemented yet
      showDiffs: true,
      showConnectors: true,
      maxDiffs: 5000,
      left: {
        id: null,
        content: null,
        mode: null,
        theme: null,
        editable: true,
        copyLinkEnabled: true,
      },
      right: {
        id: null,
        content: null,
        mode: null,
        theme: null,
        editable: true,
        copyLinkEnabled: true,
      },
      classes: {
        gutterID: 'acediff__gutter',
        diff: 'acediff__diffLine',
        connector: 'acediff__connector',
        newCodeConnectorLink: 'acediff__newCodeConnector',
        newCodeConnectorLinkContent: '→',
        deletedCodeConnectorLink: 'acediff__deletedCodeConnector',
        deletedCodeConnectorLinkContent: '←',
        copyRightContainer: 'acediff__copy--right',
        copyLeftContainer: 'acediff__copy--left',
      },
      connectorYOffset: 0,
    },
    options,
  )

  const { ace } = acediff.options

  if (!ace) {
    const errMessage =
      'No ace editor found nor supplied - `options.ace` or `window.ace` is missing'
    console.error(errMessage)
    return new Error(errMessage)
  }

  Range = getRangeModule(ace)
  if (!Range) {
    const errMessage =
      'Could not require Range module for Ace. Depends on your bundling strategy, but it usually comes with Ace itself. See https://ace.c9.io/api/range.html, open an issue on GitHub ace-diff/ace-diff'
    console.error(errMessage)
    return new Error(errMessage)
  }

  if (acediff.options.element === null) {
    const errMessage =
      'You need to specify an element for Ace-diff - `options.element` is missing'
    console.error(errMessage)
    return new Error(errMessage)
  }

  if (acediff.options.element instanceof HTMLElement) {
    acediff.el = acediff.options.element
  } else {
    acediff.el = document.body.querySelector(acediff.options.element)
  }

  if (!acediff.el) {
    const errMessage = `Can't find the specified element ${acediff.options.element}`
    console.error(errMessage)
    return new Error(errMessage)
  }

  acediff.options.left.id = ensureElement(acediff.el, 'acediff__left')
  acediff.options.classes.gutterID = ensureElement(
    acediff.el,
    'acediff__gutter',
  )
  acediff.options.right.id = ensureElement(acediff.el, 'acediff__right')

  acediff.el.innerHTML = `<div class="acediff__wrap">${acediff.el.innerHTML}</div>`

  // instantiate the editors in an internal data structure
  // that will store a little info about the diffs and
  // editor content
  acediff.editors = {
    left: {
      ace: ace.edit(acediff.options.left.id),
      markers: [],
      lineLengths: [],
    },
    right: {
      ace: ace.edit(acediff.options.right.id),
      markers: [],
      lineLengths: [],
    },
    editorHeight: null,
  }

  // set up the editors
  acediff.editors.left.ace.getSession().setMode(getMode(acediff, C.EDITOR_LEFT))
  acediff.editors.right.ace
    .getSession()
    .setMode(getMode(acediff, C.EDITOR_RIGHT))
  acediff.editors.left.ace.setReadOnly(!acediff.options.left.editable)
  acediff.editors.right.ace.setReadOnly(!acediff.options.right.editable)
  acediff.editors.left.ace.setTheme(getTheme(acediff, C.EDITOR_LEFT))
  acediff.editors.right.ace.setTheme(getTheme(acediff, C.EDITOR_RIGHT))

  acediff.editors.left.ace.setValue(
    normalizeContent(acediff.options.left.content),
    -1,
  )
  acediff.editors.right.ace.setValue(
    normalizeContent(acediff.options.right.content),
    -1,
  )

  // store the visible height of the editors (assumed the same)
  acediff.editors.editorHeight = getEditorHeight(acediff)

  // The lineHeight is set to 0 initially and we need to wait for another tick to get it
  // Thus moving the diff() with it
  setTimeout(() => {
    // assumption: both editors have same line heights
    acediff.lineHeight = acediff.editors.left.ace.renderer.lineHeight

    addEventHandlers(acediff)
    createCopyContainers(acediff)
    createGutter(acediff)
    acediff.diff()
  }, 1)
}

// our public API
AceDiff.prototype = {
  // allows on-the-fly changes to the AceDiff instance settings
  setOptions(options) {
    merge(this.options, options)
    this.diff()
  },

  getNumDiffs() {
    return this.diffs.length
  },

  // exposes the Ace editors in case the dev needs it
  getEditors() {
    return {
      left: this.editors.left.ace,
      right: this.editors.right.ace,
    }
  },

  // our main diffing function. I actually don't think this needs to exposed: it's called automatically,
  // but just to be safe, it's included
  diff() {
    const dmp = new diff_match_patch()
    const val1 = this.editors.left.ace.getSession().getValue()
    const val2 = this.editors.right.ace.getSession().getValue()
    // Main diff method that calculates the diffs
    const diff = dmp.diff_main(val2, val1)
    // console.log(JSON.stringify(diff)) // Debug the diffs

    // diff_cleanupSemantic can change the diffs by adjusting them
    // left or right to align things so check the diffs after when debugging
    dmp.diff_cleanupSemantic(diff)
    // console.log(JSON.stringify(diff)) // Debug the diffs

    this.editors.left.lineLengths = getLineLengths(this.editors.left)
    this.editors.right.lineLengths = getLineLengths(this.editors.right)

    // parse the raw diff into something a little more palatable
    const diffs = []
    const offset = {
      left: 0,
      right: 0,
    }

    diff.forEach((chunk) => {
      const chunkType = chunk[0]
      let text = chunk[1]

      // oddly, occasionally the algorithm returns a diff with no changes made
      if (text.length === 0) {
        return
      }
      if (chunkType === C.DIFF_EQUAL) {
        offset.left += text.length
        offset.right += text.length
      } else if (chunkType === C.DIFF_DELETE) {
        diffs.push(
          computeDiff(this, C.DIFF_DELETE, offset.left, offset.right, text),
        )
        offset.right += text.length
      } else if (chunkType === C.DIFF_INSERT) {
        diffs.push(
          computeDiff(this, C.DIFF_INSERT, offset.left, offset.right, text),
        )
        offset.left += text.length
      }
    }, this)

    // simplify our computed diffs; this groups together multiple diffs on subsequent lines
    this.diffs = simplifyDiffs(this, diffs)

    // if we're dealing with too many diffs, fail silently
    if (this.diffs.length > this.options.maxDiffs) {
      return
    }

    clearDiffs(this)
    decorate(this)
  },

  destroy() {
    // destroy the two editors
    const leftValue = this.editors.left.ace.getValue()
    this.editors.left.ace.destroy()
    let oldDiv = this.editors.left.ace.container
    let newDiv = oldDiv.cloneNode(false)
    newDiv.textContent = leftValue
    oldDiv.parentNode.replaceChild(newDiv, oldDiv)

    const rightValue = this.editors.right.ace.getValue()
    this.editors.right.ace.destroy()
    oldDiv = this.editors.right.ace.container
    newDiv = oldDiv.cloneNode(false)
    newDiv.textContent = rightValue
    oldDiv.parentNode.replaceChild(newDiv, oldDiv)

    document.getElementById(this.options.classes.gutterID).innerHTML = ''
    removeEventHandlers()
  },
}

let removeEventHandlers = () => {}

function addEventHandlers(acediff) {
  acediff.editors.left.ace.getSession().on(
    'changeScrollTop',
    throttle(() => {
      updateGap(acediff)
    }, 16),
  )
  acediff.editors.right.ace.getSession().on(
    'changeScrollTop',
    throttle(() => {
      updateGap(acediff)
    }, 16),
  )

  const diff = acediff.diff.bind(acediff)
  acediff.editors.left.ace.on('change', diff)
  acediff.editors.right.ace.on('change', diff)

  if (acediff.options.left.copyLinkEnabled) {
    query(
      `#${acediff.options.classes.gutterID}`,
      'click',
      `.${acediff.options.classes.newCodeConnectorLink}`,
      (e) => {
        copy(acediff, e, C.LTR)
      },
    )
  }
  if (acediff.options.right.copyLinkEnabled) {
    query(
      `#${acediff.options.classes.gutterID}`,
      'click',
      `.${acediff.options.classes.deletedCodeConnectorLink}`,
      (e) => {
        copy(acediff, e, C.RTL)
      },
    )
  }

  const onResize = debounce(() => {
    acediff.editors.availableHeight = document.getElementById(
      acediff.options.left.id,
    ).offsetHeight

    // TODO this should re-init gutter
    acediff.diff()
  }, 250)

  window.addEventListener('resize', onResize)
  removeEventHandlers = () => {
    window.removeEventListener('resize', onResize)
  }
}

function copy(acediff, e, dir) {
  const diffIndex = parseInt(e.target.getAttribute('data-diff-index'), 10)
  const diff = acediff.diffs[diffIndex]
  let sourceEditor
  let targetEditor

  let startLine
  let endLine
  let targetStartLine
  let targetEndLine
  if (dir === C.LTR) {
    sourceEditor = acediff.editors.left
    targetEditor = acediff.editors.right
    startLine = diff.leftStartLine
    endLine = diff.leftEndLine
    targetStartLine = diff.rightStartLine
    targetEndLine = diff.rightEndLine
  } else {
    sourceEditor = acediff.editors.right
    targetEditor = acediff.editors.left
    startLine = diff.rightStartLine
    endLine = diff.rightEndLine
    targetStartLine = diff.leftStartLine
    targetEndLine = diff.leftEndLine
  }

  let contentToInsert = ''
  for (let i = startLine; i < endLine; i += 1) {
    contentToInsert += getLine(sourceEditor, i)
    // JMT: If the first line is blank we add an extra newline
    // because when it is inserted the first one is effectively eaten
    // by the replace and merging of the lines
    // However if the targetStartLine is the very top then we don't
    if (i === startLine && contentToInsert === '' && targetStartLine != 0) {
      contentToInsert += '\n'
    }
    // Only add a trailing newline if we're not on the final line
    if (i < sourceEditor.ace.getSession().getLength() - 1) {
      contentToInsert += '\n'
    }
  }

  // keep track of the scroll height
  const h = targetEditor.ace.getSession().getScrollTop()

  targetEditor.ace
    .getSession()
    .replace(new Range(targetStartLine, 0, targetEndLine, 0), contentToInsert)
  targetEditor.ace.getSession().setScrollTop(parseInt(h, 10))

  acediff.diff()
}

function getLineLengths(editor) {
  const lines = editor.ace.getSession().doc.getAllLines()
  const lineLengths = []
  lines.forEach((line) => {
    lineLengths.push(line.length + 1) // +1 for the newline char
  })
  return lineLengths
}

// shows a diff in one of the two editors.
function showDiff(acediff, editor, startLine, endLine, className) {
  const editorInstance = acediff.editors[editor]

  if (endLine < startLine) {
    // can this occur? Just in case.
    endLine = startLine
  }

  const classNames = `${className} ${
    endLine > startLine ? 'lines' : 'targetOnly'
  }`

  if (endLine > startLine) {
    endLine -= 1 /* because endLine is usually + 1 */
  }

  // to get Ace to highlight the full row we just set the start and end chars to 0 and 1
  editorInstance.markers.push(
    editorInstance.ace.session.addMarker(
      new Range(startLine, 0, endLine, 1),
      classNames,
      'fullLine',
    ),
  )
}

// called onscroll. Updates the gap to ensure the connectors are all lining up
function updateGap(acediff) {
  clearDiffs(acediff)
  decorate(acediff)

  // reposition the copy containers containing all the arrows
  positionCopyContainers(acediff)
}

function clearDiffs(acediff) {
  acediff.editors.left.markers.forEach((marker) => {
    acediff.editors.left.ace.getSession().removeMarker(marker)
  }, acediff)
  acediff.editors.right.markers.forEach((marker) => {
    acediff.editors.right.ace.getSession().removeMarker(marker)
  }, acediff)
}

function addConnector(
  acediff,
  leftStartLine,
  leftEndLine,
  rightStartLine,
  rightEndLine,
) {
  const leftScrollTop = acediff.editors.left.ace.getSession().getScrollTop()
  const rightScrollTop = acediff.editors.right.ace.getSession().getScrollTop()

  // All connectors, regardless of ltr or rtl
  // have the same point system, even if p1 === p3 or p2 === p4
  //  p1   p2
  //
  //  p3   p4

  acediff.connectorYOffset = 1

  const p1_x = -1
  const p1_y = leftStartLine * acediff.lineHeight - leftScrollTop + 0.5
  const p2_x = acediff.gutterWidth + 1
  const p2_y = rightStartLine * acediff.lineHeight - rightScrollTop + 0.5
  const p3_x = -1
  const p3_y =
    leftEndLine * acediff.lineHeight -
    leftScrollTop +
    acediff.connectorYOffset +
    0.5
  const p4_x = acediff.gutterWidth + 1
  const p4_y =
    rightEndLine * acediff.lineHeight -
    rightScrollTop +
    acediff.connectorYOffset +
    0.5
  const curve1 = getCurve(p1_x, p1_y, p2_x, p2_y)
  const curve2 = getCurve(p4_x, p4_y, p3_x, p3_y)

  const verticalLine1 = `L${p2_x},${p2_y} ${p4_x},${p4_y}`
  const verticalLine2 = `L${p3_x},${p3_y} ${p1_x},${p1_y}`
  const d = `${curve1} ${verticalLine1} ${curve2} ${verticalLine2}`

  const el = document.createElementNS(C.SVG_NS, 'path')
  el.setAttribute('d', d)
  el.setAttribute('class', acediff.options.classes.connector)
  acediff.gutterSVG.appendChild(el)
}

function addCopyArrows(acediff, info, diffIndex) {
  if (
    info.leftEndLine > info.leftStartLine &&
    acediff.options.left.copyLinkEnabled
  ) {
    const arrow = createArrow({
      className: acediff.options.classes.newCodeConnectorLink,
      topOffset: info.leftStartLine * acediff.lineHeight,
      tooltip: 'Copy to right',
      diffIndex,
      arrowContent: acediff.options.classes.newCodeConnectorLinkContent,
    })
    acediff.copyRightContainer.appendChild(arrow)
  }

  if (
    info.rightEndLine > info.rightStartLine &&
    acediff.options.right.copyLinkEnabled
  ) {
    const arrow = createArrow({
      className: acediff.options.classes.deletedCodeConnectorLink,
      topOffset: info.rightStartLine * acediff.lineHeight,
      tooltip: 'Copy to left',
      diffIndex,
      arrowContent: acediff.options.classes.deletedCodeConnectorLinkContent,
    })
    acediff.copyLeftContainer.appendChild(arrow)
  }
}

function positionCopyContainers(acediff) {
  const leftTopOffset = acediff.editors.left.ace.getSession().getScrollTop()
  const rightTopOffset = acediff.editors.right.ace.getSession().getScrollTop()

  acediff.copyRightContainer.style.cssText = `top: ${-leftTopOffset}px`
  acediff.copyLeftContainer.style.cssText = `top: ${-rightTopOffset}px`
}

/**
 // eslint-disable-next-line max-len
 * This method takes the raw diffing info from the Google lib and returns a nice clean object of the following
 * form:
 * {
 *   leftStartLine:
 *   leftEndLine:
 *   rightStartLine:
 *   rightEndLine:
 * }
 *
 * Ultimately, that's all the info we need to highlight the appropriate lines in the left + right editor, add the
 * SVG connectors, and include the appropriate <<, >> arrows.
 *
 * Note: leftEndLine and rightEndLine are always the start of the NEXT line, so for a single line diff, there will
 * be 1 separating the startLine and endLine values. So if leftStartLine === leftEndLine or rightStartLine ===
 * rightEndLine, it means that new content from the other editor is being inserted and a single 1px line will be
 * drawn.
 */
function computeDiff(acediff, diffType, offsetLeft, offsetRight, diffText) {
  let lineInfo = {}

  if (diffType === C.DIFF_INSERT) {
    // pretty confident this returns the right stuff for the left editor: start & end line & char
    let info = getSingleDiffInfo(acediff.editors.left, offsetLeft, diffText)

    // this is the ACTUAL undoctored current line in the other editor. It's always right. Doesn't mean it's
    // going to be used as the start line for the diff though.
    let currentLineOtherEditor = getLineForCharPosition(
      acediff.editors.right,
      offsetRight,
    )
    let numCharsOnLineOtherEditor = getCharsOnLine(
      acediff.editors.right,
      currentLineOtherEditor,
    )
    const numCharsOnLeftEditorStartLine = getCharsOnLine(
      acediff.editors.left,
      info.startLine,
    )

    let rightStartLine = currentLineOtherEditor
    let sameLineInsert = info.startLine === info.endLine

    // whether or not this diff is a plain INSERT into the other editor, or overwrites a line take a little work to
    // figure out. This feels like the hardest part of the entire script.
    let numRows = 0
    if (
      // dense, but this accommodates two scenarios:
      // 1. where a completely fresh new line is being inserted in left editor, we want the line on right to stay a 1px line
      // 2. where a new character is inserted at the start of a newline on the left but the line contains other stuff,
      //    we DO want to make it a full line
      (info.startChar > 0 ||
        (sameLineInsert && diffText.length < numCharsOnLeftEditorStartLine)) &&
      // if the right editor line was empty, it's ALWAYS a single line insert [not an OR above?]
      numCharsOnLineOtherEditor > 0 &&
      // if the text being inserted starts mid-line
      info.startChar < numCharsOnLeftEditorStartLine
    ) {
      numRows++
    }

    lineInfo = {
      leftStartLine: info.startLine,
      leftEndLine: info.endLine + 1,
      rightStartLine,
      rightEndLine: rightStartLine + numRows,
    }
  } else {
    let info = getSingleDiffInfo(acediff.editors.right, offsetRight, diffText)

    let currentLineOtherEditor = getLineForCharPosition(
      acediff.editors.left,
      offsetLeft,
    )
    let numCharsOnLineOtherEditor = getCharsOnLine(
      acediff.editors.left,
      currentLineOtherEditor,
    )
    const numCharsOnRightEditorStartLine = getCharsOnLine(
      acediff.editors.right,
      info.startLine,
    )

    let leftStartLine = currentLineOtherEditor
    let sameLineInsert = info.startLine === info.endLine
    let numRows = 0
    if (
      // dense, but this accommodates two scenarios:
      // 1. where a completely fresh new line is being inserted in left editor, we want the line on right to stay a 1px line
      // 2. where a new character is inserted at the start of a newline on the left but the line contains other stuff,
      //    we DO want to make it a full line
      (info.startChar > 0 ||
        (sameLineInsert && diffText.length < numCharsOnRightEditorStartLine)) &&
      // if the right editor line was empty, it's ALWAYS a single line insert [not an OR above?]
      numCharsOnLineOtherEditor > 0 &&
      // if the text being inserted starts mid-line
      info.startChar < numCharsOnRightEditorStartLine
    ) {
      numRows++
    }

    lineInfo = {
      leftStartLine,
      leftEndLine: leftStartLine + numRows,
      rightStartLine: info.startLine,
      rightEndLine: info.endLine + 1,
    }
  }

  return lineInfo
}

// helper to return the startline, endline, startChar and endChar for a diff in a particular editor. Pretty
// fussy function
function getSingleDiffInfo(editor, offset, diffString) {
  const info = {
    startLine: 0,
    startChar: 0,
    endLine: 0,
    endChar: 0,
  }
  const endCharNum = offset + diffString.length
  let runningTotal = 0
  let startLineSet = false
  let endLineSet = false

  editor.lineLengths.forEach((lineLength, lineIndex) => {
    runningTotal += lineLength

    if (!startLineSet && offset < runningTotal) {
      info.startLine = lineIndex
      info.startChar = offset - runningTotal + lineLength
      startLineSet = true
    }

    if (!endLineSet && endCharNum <= runningTotal) {
      info.endLine = lineIndex
      info.endChar = endCharNum - runningTotal + lineLength
      endLineSet = true
    }
  })

  // if the start char is the final char on the line, it's a newline & we ignore it
  if (
    info.startChar > 0 &&
    getCharsOnLine(editor, info.startLine) === info.startChar
  ) {
    info.startLine++
    info.startChar = 0
  }

  // if the end char is the first char on the line, we don't want to highlight that extra line
  if (info.endChar === 0) {
    info.endLine--
  }

  const endsWithNewline = /\n$/.test(diffString)
  if (info.startChar > 0 && endsWithNewline) {
    info.endLine++
  }

  return info
}

// note that this and everything else in this script uses 0-indexed row numbers
function getCharsOnLine(editor, line) {
  return getLine(editor, line).length
}

function getLineForCharPosition(editor, offsetChars) {
  const lines = editor.ace.getSession().doc.getAllLines()
  let foundLine = 0
  let runningTotal = 0

  for (let i = 0; i < lines.length; i += 1) {
    runningTotal += lines[i].length + 1 // +1 needed for newline char
    if (offsetChars < runningTotal) {
      foundLine = i
      break
    }
  }
  // If we're past the end of the buffer then we need to bump the line total
  // because we're basically inserting after the last line
  if (runningTotal > editor.ace.getSession().getValue().length) {
    foundLine += 1
  }
  return foundLine
}

function createGutter(acediff) {
  acediff.gutterHeight = document.getElementById(
    acediff.options.classes.gutterID,
  ).clientHeight
  acediff.gutterWidth = document.getElementById(
    acediff.options.classes.gutterID,
  ).clientWidth

  const leftHeight = getTotalHeight(acediff, C.EDITOR_LEFT)
  const rightHeight = getTotalHeight(acediff, C.EDITOR_RIGHT)
  const height = Math.max(leftHeight, rightHeight, acediff.gutterHeight)

  acediff.gutterSVG = document.createElementNS(C.SVG_NS, 'svg')
  acediff.gutterSVG.setAttribute('width', acediff.gutterWidth)
  acediff.gutterSVG.setAttribute('height', height)

  document
    .getElementById(acediff.options.classes.gutterID)
    .appendChild(acediff.gutterSVG)
}

// acediff.editors.left.ace.getSession().getLength() * acediff.lineHeight
function getTotalHeight(acediff, editor) {
  const ed =
    editor === C.EDITOR_LEFT ? acediff.editors.left : acediff.editors.right
  return ed.ace.getSession().getLength() * acediff.lineHeight
}

// creates two contains for positioning the copy left + copy right arrows
function createCopyContainers(acediff) {
  acediff.copyRightContainer = document.createElement('div')
  acediff.copyRightContainer.setAttribute(
    'class',
    acediff.options.classes.copyRightContainer,
  )
  acediff.copyLeftContainer = document.createElement('div')
  acediff.copyLeftContainer.setAttribute(
    'class',
    acediff.options.classes.copyLeftContainer,
  )

  document
    .getElementById(acediff.options.classes.gutterID)
    .appendChild(acediff.copyRightContainer)
  document
    .getElementById(acediff.options.classes.gutterID)
    .appendChild(acediff.copyLeftContainer)
}

function clearGutter(acediff) {
  // gutter.innerHTML = '';

  const gutterEl = document.getElementById(acediff.options.classes.gutterID)
  gutterEl.removeChild(acediff.gutterSVG)

  createGutter(acediff)
}

function clearArrows(acediff) {
  acediff.copyLeftContainer.innerHTML = ''
  acediff.copyRightContainer.innerHTML = ''
}

/*
 * This combines multiple rows where, say, line 1 => line 1, line 2 => line 2, line 3-4 => line 3. That could be
 * reduced to a single connector line 1=4 => line 1-3
 */
function simplifyDiffs(acediff, diffs) {
  const groupedDiffs = []

  function compare(val) {
    return acediff.options.diffGranularity === C.DIFF_GRANULARITY_SPECIFIC
      ? val < 1
      : val <= 1
  }

  diffs.forEach((diff, index) => {
    if (index === 0) {
      groupedDiffs.push(diff)
      return
    }

    // loop through all grouped diffs. If this new diff lies between an existing one, we'll just add to it, rather
    // than create a new one
    let isGrouped = false
    for (let i = 0; i < groupedDiffs.length; i += 1) {
      if (
        compare(Math.abs(diff.leftStartLine - groupedDiffs[i].leftEndLine)) &&
        compare(Math.abs(diff.rightStartLine - groupedDiffs[i].rightEndLine))
      ) {
        // update the existing grouped diff to expand its horizons to include this new diff start + end lines
        groupedDiffs[i].leftStartLine = Math.min(
          diff.leftStartLine,
          groupedDiffs[i].leftStartLine,
        )
        groupedDiffs[i].rightStartLine = Math.min(
          diff.rightStartLine,
          groupedDiffs[i].rightStartLine,
        )
        groupedDiffs[i].leftEndLine = Math.max(
          diff.leftEndLine,
          groupedDiffs[i].leftEndLine,
        )
        groupedDiffs[i].rightEndLine = Math.max(
          diff.rightEndLine,
          groupedDiffs[i].rightEndLine,
        )
        isGrouped = true
        break
      }
    }

    if (!isGrouped) {
      groupedDiffs.push(diff)
    }
  })

  // clear out any single line diffs (i.e. single line on both editors)
  const fullDiffs = []
  groupedDiffs.forEach((diff) => {
    if (
      diff.leftStartLine === diff.leftEndLine &&
      diff.rightStartLine === diff.rightEndLine
    ) {
      return
    }
    fullDiffs.push(diff)
  })

  return fullDiffs
}

function decorate(acediff) {
  clearGutter(acediff)
  clearArrows(acediff)

  acediff.diffs.forEach((info, diffIndex) => {
    if (acediff.options.showDiffs) {
      showDiff(
        acediff,
        C.EDITOR_LEFT,
        info.leftStartLine,
        info.leftEndLine,
        acediff.options.classes.diff,
      )
      showDiff(
        acediff,
        C.EDITOR_RIGHT,
        info.rightStartLine,
        info.rightEndLine,
        acediff.options.classes.diff,
      )

      if (acediff.options.showConnectors) {
        addConnector(
          acediff,
          info.leftStartLine,
          info.leftEndLine,
          info.rightStartLine,
          info.rightEndLine,
        )
      }
      addCopyArrows(acediff, info, diffIndex)
    }
  }, acediff)
}