<commit_before>import * as React from 'react'
import { clipboard } from 'electron'
import { Editor, Doc } from 'codemirror'

import {
  DiffHunk,
  DiffLineType,
  DiffSelection,
  DiffLine,
} from '../../models/diff'
import {
  WorkingDirectoryFileChange,
  CommittedFileChange,
} from '../../models/status'

import { OcticonSymbol } from '../octicons'

import { IEditorConfigurationExtra } from './editor-configuration-extra'
import { DiffSyntaxMode, IDiffSyntaxModeSpec } from './diff-syntax-mode'
import { CodeMirrorHost } from './code-mirror-host'
import {
  diffLineForIndex,
  findInteractiveDiffRange,
  lineNumberForDiffLine,
} from './diff-explorer'

import {
  getLineFilters,
  getFileContents,
  highlightContents,
} from './syntax-highlighting'
import { relativeChanges } from './changed-range'
import { Repository } from '../../models/repository'
import memoizeOne from 'memoize-one'
import { structuralEquals } from '../../lib/equality'
import { assertNever } from '../../lib/fatal-error'
import { clamp } from '../../lib/clamp'
import { uuid } from '../../lib/uuid'

/** The longest line for which we'd try to calculate a line diff. */
const MaxIntraLineDiffStringLength = 4096

// This is a custom version of the no-newline octicon that's exactly as
// tall as it needs to be (8px) which helps with aligning it on the line.
const narrowNoNewlineSymbol = new OcticonSymbol(
  16,
  8,
  'm 16,1 0,3 c 0,0.55 -0.45,1 -1,1 l -3,0 0,2 -3,-3 3,-3 0,2 2,0 0,-2 2,0 z M 8,4 C 8,6.2 6.2,8 4,8 1.8,8 0,6.2 0,4 0,1.8 1.8,0 4,0 6.2,0 8,1.8 8,4 Z M 1.5,5.66 5.66,1.5 C 5.18,1.19 4.61,1 4,1 2.34,1 1,2.34 1,4 1,4.61 1.19,5.17 1.5,5.66 Z M 7,4 C 7,3.39 6.81,2.83 6.5,2.34 L 2.34,6.5 C 2.82,6.81 3.39,7 4,7 5.66,7 7,5.66 7,4 Z'
)

type ChangedFile = WorkingDirectoryFileChange | CommittedFileChange

/**
 * Checks to see if any key parameters in the props object that are used
 * when performing highlighting has changed. This is used to determine
 * whether highlighting should abort in between asynchronous operations
 * due to some factor (like which file is currently selected) have changed
 * and thus rendering the in-flight highlighting data useless.
 */
function highlightParametersEqual(
  newProps: ITextDiffProps,
  prevProps: ITextDiffProps
) {
  return (
    newProps === prevProps ||
    (newProps.file.id === prevProps.file.id && newProps.text === prevProps.text)
  )
}

type SelectionKind = 'hunk' | 'range'

interface ISelection {
  readonly from: number
  readonly to: number
  readonly kind: SelectionKind
  readonly isSelected: boolean
}

function createNoNewlineIndicatorWidget() {
  const widget = document.createElement('span')
  const titleId = uuid()

  const { w, h, d } = narrowNoNewlineSymbol

  const xmlns = 'http://www.w3.org/2000/svg'
  const svgElem = document.createElementNS(xmlns, 'svg')
  svgElem.setAttribute('version', '1.1')
  svgElem.setAttribute('viewBox', `0 0 ${w} ${h}`)
  svgElem.setAttribute('role', 'img')
  svgElem.setAttribute('aria-labelledby', titleId)
  svgElem.classList.add('no-newline')

  const titleElem = document.createElementNS(xmlns, 'title')
  titleElem.setAttribute('id', titleId)
  titleElem.setAttribute('lang', 'en')
  titleElem.textContent = 'No newline at end of file'
  svgElem.appendChild(titleElem)

  const pathElem = document.createElementNS(xmlns, 'path')
  pathElem.setAttribute('role', 'presentation')
  pathElem.setAttribute('d', d)
  pathElem.textContent = 'No newline at end of file'
  svgElem.appendChild(pathElem)

  widget.appendChild(svgElem)
  return widget
}

/**
 * Utility function to check whether a selection exists, and whether
 * the given index is contained within the selection.
 */
function inSelection(s: ISelection | null, ix: number): s is ISelection {
  if (s === null) {
    return false
  }
  return ix >= Math.min(s.from, s.to) && ix <= Math.max(s.to, s.from)
}

/** Utility function for checking whether an event target has a given CSS class */
function targetHasClass(target: EventTarget | null, token: string) {
  return target instanceof HTMLElement && target.classList.contains(token)
}

/** Utility function for checking whether a file supports selection */
function canSelect(file: ChangedFile): file is WorkingDirectoryFileChange {
  return file instanceof WorkingDirectoryFileChange
}

interface ITextDiffProps {
  readonly repository: Repository
  readonly file: ChangedFile
  readonly readOnly: boolean
  readonly onIncludeChanged?: (diffSelection: DiffSelection) => void
  readonly text: string
  readonly hunks: ReadonlyArray<DiffHunk>
}

const diffGutterName = 'diff-gutter'

function showSearch(cm: Editor) {
  cm.execCommand('findPersistent')
  const wrapper = cm.getWrapperElement()

  if (!wrapper) {
    return
  }

  const dialog = wrapper.querySelector('.CodeMirror-dialog')

  if (!dialog) {
    return
  }

  dialog.classList.add('CodeMirror-search-dialog')
  const searchLabel = dialog.querySelector('.CodeMirror-search-label')
  const searchField = dialog.querySelector('.CodeMirror-search-field')

  if (
    searchLabel instanceof HTMLElement &&
    searchField instanceof HTMLInputElement
  ) {
    searchLabel.style.display = 'none'
    searchField.placeholder = 'Search'
    searchField.style.width = null
  }
}

const defaultEditorOptions: IEditorConfigurationExtra = {
  lineNumbers: false,
  readOnly: true,
  showCursorWhenSelecting: false,
  cursorBlinkRate: -1,
  lineWrapping: true,
  mode: { name: DiffSyntaxMode.ModeName },
  // Make sure CodeMirror doesn't capture Tab (and Shift-Tab) and thus destroy tab navigation
  extraKeys: {
    Tab: false,
    'Shift-Tab': false,
    // Steal the default key binding so that we can launch our
    // custom search UI.
    [__DARWIN__ ? 'Cmd-F' : 'Ctrl-F']: showSearch,
    // Disable all other search-related shortcuts so that they
    // don't interfer with global app shortcuts.
    [__DARWIN__ ? 'Cmd-G' : 'Ctrl-G']: false, // findNext
    [__DARWIN__ ? 'Shift-Cmd-G' : 'Shift-Ctrl-G']: false, // findPrev
    [__DARWIN__ ? 'Cmd-Alt-F' : 'Shift-Ctrl-F']: false, // replace
    [__DARWIN__ ? 'Shift-Cmd-Alt-F' : 'Shift-Ctrl-R']: false, // replaceAll
  },
  scrollbarStyle: __DARWIN__ ? 'simple' : 'native',
  styleSelectedText: true,
  lineSeparator: '\n',
  specialChars: /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/,
  gutters: [diffGutterName],
}

export class TextDiff extends React.Component<ITextDiffProps, {}> {
  private codeMirror: Editor | null = null

  private getCodeMirrorDocument = memoizeOne(
    (text: string, noNewlineIndicatorLines: ReadonlyArray<number>) => {
      const { mode, firstLineNumber, lineSeparator } = defaultEditorOptions
      // If the text looks like it could have been formatted using Windows<commit_msg>Make key up/down and page up/down scroll the editor without moving the cursor<commit_after>import * as React from 'react'
import { clipboard } from 'electron'
import { Editor, Doc } from 'codemirror'

import {
  DiffHunk,
  DiffLineType,
  DiffSelection,
  DiffLine,
} from '../../models/diff'
import {
  WorkingDirectoryFileChange,
  CommittedFileChange,
} from '../../models/status'

import { OcticonSymbol } from '../octicons'

import { IEditorConfigurationExtra } from './editor-configuration-extra'
import { DiffSyntaxMode, IDiffSyntaxModeSpec } from './diff-syntax-mode'
import { CodeMirrorHost } from './code-mirror-host'
import {
  diffLineForIndex,
  findInteractiveDiffRange,
  lineNumberForDiffLine,
} from './diff-explorer'

import {
  getLineFilters,
  getFileContents,
  highlightContents,
} from './syntax-highlighting'
import { relativeChanges } from './changed-range'
import { Repository } from '../../models/repository'
import memoizeOne from 'memoize-one'
import { structuralEquals } from '../../lib/equality'
import { assertNever } from '../../lib/fatal-error'
import { clamp } from '../../lib/clamp'
import { uuid } from '../../lib/uuid'

/** The longest line for which we'd try to calculate a line diff. */
const MaxIntraLineDiffStringLength = 4096

// This is a custom version of the no-newline octicon that's exactly as
// tall as it needs to be (8px) which helps with aligning it on the line.
const narrowNoNewlineSymbol = new OcticonSymbol(
  16,
  8,
  'm 16,1 0,3 c 0,0.55 -0.45,1 -1,1 l -3,0 0,2 -3,-3 3,-3 0,2 2,0 0,-2 2,0 z M 8,4 C 8,6.2 6.2,8 4,8 1.8,8 0,6.2 0,4 0,1.8 1.8,0 4,0 6.2,0 8,1.8 8,4 Z M 1.5,5.66 5.66,1.5 C 5.18,1.19 4.61,1 4,1 2.34,1 1,2.34 1,4 1,4.61 1.19,5.17 1.5,5.66 Z M 7,4 C 7,3.39 6.81,2.83 6.5,2.34 L 2.34,6.5 C 2.82,6.81 3.39,7 4,7 5.66,7 7,5.66 7,4 Z'
)

type ChangedFile = WorkingDirectoryFileChange | CommittedFileChange

/**
 * Checks to see if any key parameters in the props object that are used
 * when performing highlighting has changed. This is used to determine
 * whether highlighting should abort in between asynchronous operations
 * due to some factor (like which file is currently selected) have changed
 * and thus rendering the in-flight highlighting data useless.
 */
function highlightParametersEqual(
  newProps: ITextDiffProps,
  prevProps: ITextDiffProps
) {
  return (
    newProps === prevProps ||
    (newProps.file.id === prevProps.file.id && newProps.text === prevProps.text)
  )
}

type SelectionKind = 'hunk' | 'range'

interface ISelection {
  readonly from: number
  readonly to: number
  readonly kind: SelectionKind
  readonly isSelected: boolean
}

function createNoNewlineIndicatorWidget() {
  const widget = document.createElement('span')
  const titleId = uuid()

  const { w, h, d } = narrowNoNewlineSymbol

  const xmlns = 'http://www.w3.org/2000/svg'
  const svgElem = document.createElementNS(xmlns, 'svg')
  svgElem.setAttribute('version', '1.1')
  svgElem.setAttribute('viewBox', `0 0 ${w} ${h}`)
  svgElem.setAttribute('role', 'img')
  svgElem.setAttribute('aria-labelledby', titleId)
  svgElem.classList.add('no-newline')

  const titleElem = document.createElementNS(xmlns, 'title')
  titleElem.setAttribute('id', titleId)
  titleElem.setAttribute('lang', 'en')
  titleElem.textContent = 'No newline at end of file'
  svgElem.appendChild(titleElem)

  const pathElem = document.createElementNS(xmlns, 'path')
  pathElem.setAttribute('role', 'presentation')
  pathElem.setAttribute('d', d)
  pathElem.textContent = 'No newline at end of file'
  svgElem.appendChild(pathElem)

  widget.appendChild(svgElem)
  return widget
}

/**
 * Utility function to check whether a selection exists, and whether
 * the given index is contained within the selection.
 */
function inSelection(s: ISelection | null, ix: number): s is ISelection {
  if (s === null) {
    return false
  }
  return ix >= Math.min(s.from, s.to) && ix <= Math.max(s.to, s.from)
}

/** Utility function for checking whether an event target has a given CSS class */
function targetHasClass(target: EventTarget | null, token: string) {
  return target instanceof HTMLElement && target.classList.contains(token)
}

/** Utility function for checking whether a file supports selection */
function canSelect(file: ChangedFile): file is WorkingDirectoryFileChange {
  return file instanceof WorkingDirectoryFileChange
}

interface ITextDiffProps {
  readonly repository: Repository
  readonly file: ChangedFile
  readonly readOnly: boolean
  readonly onIncludeChanged?: (diffSelection: DiffSelection) => void
  readonly text: string
  readonly hunks: ReadonlyArray<DiffHunk>
}

const diffGutterName = 'diff-gutter'

function showSearch(cm: Editor) {
  cm.execCommand('findPersistent')
  const wrapper = cm.getWrapperElement()

  if (!wrapper) {
    return
  }

  const dialog = wrapper.querySelector('.CodeMirror-dialog')

  if (!dialog) {
    return
  }

  dialog.classList.add('CodeMirror-search-dialog')
  const searchLabel = dialog.querySelector('.CodeMirror-search-label')
  const searchField = dialog.querySelector('.CodeMirror-search-field')

  if (
    searchLabel instanceof HTMLElement &&
    searchField instanceof HTMLInputElement
  ) {
    searchLabel.style.display = 'none'
    searchField.placeholder = 'Search'
    searchField.style.width = null
  }
}

function scrollEditorVertically(step: number, unit: 'line' | 'page') {
  return (cm: Editor) => {
    const lineHeight = Math.round(cm.defaultTextHeight() + 4)
    const scrollInfo = cm.getScrollInfo()

    if (unit === 'line') {
      cm.scrollTo(undefined, scrollInfo.top + step * lineHeight)
    } else {
      const pageHeight = scrollInfo.clientHeight - lineHeight
      cm.scrollTo(undefined, scrollInfo.top + step * pageHeight)
    }
  }
}

const defaultEditorOptions: IEditorConfigurationExtra = {
  lineNumbers: false,
  readOnly: true,
  showCursorWhenSelecting: false,
  cursorBlinkRate: -1,
  lineWrapping: true,
  mode: { name: DiffSyntaxMode.ModeName },
  // Make sure CodeMirror doesn't capture Tab (and Shift-Tab) and thus destroy tab navigation
  extraKeys: {
    Tab: false,
    'Shift-Tab': false,
    // Steal the default key binding so that we can launch our
    // custom search UI.
    [__DARWIN__ ? 'Cmd-F' : 'Ctrl-F']: showSearch,
    // Disable all other search-related shortcuts so that they
    // don't interfer with global app shortcuts.
    [__DARWIN__ ? 'Cmd-G' : 'Ctrl-G']: false, // findNext
    [__DARWIN__ ? 'Shift-Cmd-G' : 'Shift-Ctrl-G']: false, // findPrev
    [__DARWIN__ ? 'Cmd-Alt-F' : 'Shift-Ctrl-F']: false, // replace
    [__DARWIN__ ? 'Shift-Cmd-Alt-F' : 'Shift-Ctrl-R']: false, // replaceAll
    Down: scrollEditorVertically(1, 'line'),
    Up: scrollEditorVertically(-1, 'line'),
    PageDown: scrollEditorVertically(1, 'page'),
    PageUp: scrollEditorVertically(-1, 'page'),
  },
  scrollbarStyle: __DARWIN__ ? 'simple' : 'native',
  styleSelectedText: true,
  lineSeparator: '\n',
  specialChars: /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/,
  gutters: [diffGutterName],
}

export class TextDiff extends React.Component<ITextDiffProps, {}> {
  private codeMirror: Editor | null = null

  private getCodeMirrorDocument = memoizeOne(
    (text: string, noNewlineIndicatorLines: ReadonlyArray<number>) => {
      const { mode, firstLineNumber, lineSeparator } = defaultEditorOptions
      // If the text looks like it could have been formatted using Windows
{'fraction_non_alphanumeric': 0.1239803094233474, 'fraction_numerical': 0.029324894514767934, 'mean_word_length': 3.9654329608938546, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 8}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3194777', 'n_tokens_mistral': 4787, 'n_tokens_neox': 4536, 'n_words': 1301}