<commit_before>import { debounce } from 'lodash'
import disposableEvent from 'disposable-event'
import { CompositeDisposable, Disposable, Emitter, Range } from 'atom'
// $FlowIgnore: Cursor is a type
import type { TextEditor, DisplayMarker, Marker, Gutter, Point, Cursor } from 'atom'

import Tooltip from '../tooltip'
import { $range, filterMessagesByRangeOrPoint } from '../helpers'
import { hasParent, mouseEventNearPosition, getBufferPositionFromMouseEvent } from './helpers'
import type { LinterMessage } from '../types'

export default class Editor {
  gutter: Gutter | null | undefined
  tooltip: Tooltip | null | undefined
  emitter: Emitter
  markers: Map<string, Array<DisplayMarker | Marker>>
  messages: Map<string, LinterMessage>
  textEditor: TextEditor
  showTooltip: boolean
  subscriptions: CompositeDisposable
  cursorPosition: Point | null | undefined
  gutterPosition: boolean
  tooltipFollows: string
  showDecorations: boolean
  showProviderName: boolean
  ignoreTooltipInvocation: boolean
  currentLineMarker: DisplayMarker | null | undefined
  lastRange: Range | null | undefined
  lastEmpty: Range | null | undefined
  lastCursorPositions: WeakMap<Cursor, Point>

  constructor(textEditor: TextEditor) {
    this.tooltip = null
    this.emitter = new Emitter()
    this.markers = new Map()
    this.messages = new Map()
    this.textEditor = textEditor
    this.subscriptions = new CompositeDisposable()
    this.ignoreTooltipInvocation = false
    this.currentLineMarker = null
    this.lastRange = null
    this.lastEmpty = null
    this.lastCursorPositions = new WeakMap()

    this.subscriptions.add(this.emitter)
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.showTooltip', showTooltip => {
        this.showTooltip = showTooltip
        if (!this.showTooltip && this.tooltip) {
          this.removeTooltip()
        }
      }),
    )
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.showProviderName', showProviderName => {
        this.showProviderName = showProviderName
      }),
    )
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.showDecorations', showDecorations => {
        const notInitial = typeof this.showDecorations !== 'undefined'
        this.showDecorations = showDecorations
        if (notInitial) {
          this.updateGutter()
        }
      }),
    )
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.gutterPosition', gutterPosition => {
        const notInitial = typeof this.gutterPosition !== 'undefined'
        this.gutterPosition = gutterPosition
        if (notInitial) {
          this.updateGutter()
        }
      }),
    )
    this.subscriptions.add(
      textEditor.onDidDestroy(() => {
        this.dispose()
      }),
    )

    let tooltipSubscription: CompositeDisposable | null = null
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.tooltipFollows', tooltipFollows => {
        this.tooltipFollows = tooltipFollows
        if (tooltipSubscription) {
          tooltipSubscription.dispose()
        }
        tooltipSubscription = new CompositeDisposable()
        if (tooltipFollows === 'Mouse' || tooltipFollows === 'Both') {
          tooltipSubscription.add(this.listenForMouseMovement())
        }
        if (tooltipFollows === 'Keyboard' || tooltipFollows === 'Both') {
          tooltipSubscription.add(this.listenForKeyboardMovement())
        }
        this.removeTooltip()
      }),
    )
    this.subscriptions.add(
      new Disposable(function () {
        tooltipSubscription.dispose()
      }),
    )

    this.subscriptions.add(
      textEditor.onDidChangeCursorPosition(({ cursor, newBufferPosition }) => {
        const lastBufferPosition = this.lastCursorPositions.get(cursor)
        if (!lastBufferPosition || !lastBufferPosition.isEqual(newBufferPosition)) {
          this.lastCursorPositions.set(cursor, newBufferPosition)
          this.ignoreTooltipInvocation = false
        }
        if (this.tooltipFollows === 'Mouse') {
          this.removeTooltip()
        }
      }),
    )
    this.subscriptions.add(
      textEditor.getBuffer().onDidChangeText(() => {
        const cursors = textEditor.getCursors()
        cursors.forEach(cursor => {
          this.lastCursorPositions.set(cursor, cursor.getBufferPosition())
        })
        if (this.tooltipFollows !== 'Mouse') {
          this.ignoreTooltipInvocation = true
          this.removeTooltip()
        }
      }),
    )
    this.updateGutter()
    this.listenForCurrentLine()
  }
  listenForCurrentLine() {
    this.subscriptions.add(
      this.textEditor.observeCursors(cursor => {
        const handlePositionChange = ({ start, end }) => {
          const gutter = this.gutter
          if (!gutter || this.subscriptions.disposed) return
          // We need that Range.fromObject hack below because when we focus index 0 on multi-line selection
          // end.column is the column of the last line but making a range out of two and then accesing
          // the end seems to fix it (black magic?)
          const currentRange = Range.fromObject([start, end])
          const linesRange = Range.fromObject([
            [start.row, 0],
            [end.row, Infinity],
          ])
          const currentEmpty = currentRange.isEmpty()

          // NOTE: Atom does not paint gutter if multi-line and last line has zero index
          if (start.row !== end.row && currentRange.end.column === 0) {
            linesRange.end.row--
          }
          if (this.lastRange && this.lastRange.isEqual(linesRange) && currentEmpty === this.lastEmpty) return
          if (this.currentLineMarker) {
            this.currentLineMarker.destroy()
            this.currentLineMarker = null
          }
          this.lastRange = linesRange
          this.lastEmpty = currentEmpty

          this.currentLineMarker = this.textEditor.markScreenRange(linesRange, {
            invalidate: 'never',
          })
          const item = document.createElement('span')
          item.className = `line-number cursor-line linter-cursor-line ${currentEmpty ? 'cursor-line-no-selection' : ''}`
          gutter.decorateMarker(this.currentLineMarker, {
            item,
            class: 'linter-row',
          })
        }

        const cursorMarker = cursor.getMarker()
        const subscriptions = new CompositeDisposable()
        subscriptions.add(
          cursorMarker.onDidChange(({ newHeadScreenPosition, newTailScreenPosition }) => {
            handlePositionChange({
              start: newHeadScreenPosition,
              end: newTailScreenPosition,<commit_msg>chore: fix lastEmpty type and rename variables<commit_after>import { debounce } from 'lodash'
import disposableEvent from 'disposable-event'
import { CompositeDisposable, Disposable, Emitter, Range } from 'atom'
// $FlowIgnore: Cursor is a type
import type { TextEditor, DisplayMarker, Marker, Gutter, Point, Cursor } from 'atom'

import Tooltip from '../tooltip'
import { $range, filterMessagesByRangeOrPoint } from '../helpers'
import { hasParent, mouseEventNearPosition, getBufferPositionFromMouseEvent } from './helpers'
import type { LinterMessage } from '../types'

export default class Editor {
  gutter: Gutter | null | undefined
  tooltip: Tooltip | null | undefined
  emitter: Emitter
  markers: Map<string, Array<DisplayMarker | Marker>>
  messages: Map<string, LinterMessage>
  textEditor: TextEditor
  showTooltip: boolean
  subscriptions: CompositeDisposable
  cursorPosition: Point | null | undefined
  gutterPosition: boolean
  tooltipFollows: string
  showDecorations: boolean
  showProviderName: boolean
  ignoreTooltipInvocation: boolean
  currentLineMarker: DisplayMarker | null | undefined
  lastRange: Range | null | undefined
  lastIsEmpty: boolean | null | undefined
  lastCursorPositions: WeakMap<Cursor, Point>

  constructor(textEditor: TextEditor) {
    this.tooltip = null
    this.emitter = new Emitter()
    this.markers = new Map()
    this.messages = new Map()
    this.textEditor = textEditor
    this.subscriptions = new CompositeDisposable()
    this.ignoreTooltipInvocation = false
    this.currentLineMarker = null
    this.lastRange = null
    this.lastIsEmpty = null
    this.lastCursorPositions = new WeakMap()

    this.subscriptions.add(this.emitter)
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.showTooltip', showTooltip => {
        this.showTooltip = showTooltip
        if (!this.showTooltip && this.tooltip) {
          this.removeTooltip()
        }
      }),
    )
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.showProviderName', showProviderName => {
        this.showProviderName = showProviderName
      }),
    )
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.showDecorations', showDecorations => {
        const notInitial = typeof this.showDecorations !== 'undefined'
        this.showDecorations = showDecorations
        if (notInitial) {
          this.updateGutter()
        }
      }),
    )
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.gutterPosition', gutterPosition => {
        const notInitial = typeof this.gutterPosition !== 'undefined'
        this.gutterPosition = gutterPosition
        if (notInitial) {
          this.updateGutter()
        }
      }),
    )
    this.subscriptions.add(
      textEditor.onDidDestroy(() => {
        this.dispose()
      }),
    )

    let tooltipSubscription: CompositeDisposable | null = null
    this.subscriptions.add(
      atom.config.observe('linter-ui-default.tooltipFollows', tooltipFollows => {
        this.tooltipFollows = tooltipFollows
        if (tooltipSubscription) {
          tooltipSubscription.dispose()
        }
        tooltipSubscription = new CompositeDisposable()
        if (tooltipFollows === 'Mouse' || tooltipFollows === 'Both') {
          tooltipSubscription.add(this.listenForMouseMovement())
        }
        if (tooltipFollows === 'Keyboard' || tooltipFollows === 'Both') {
          tooltipSubscription.add(this.listenForKeyboardMovement())
        }
        this.removeTooltip()
      }),
    )
    this.subscriptions.add(
      new Disposable(function () {
        tooltipSubscription.dispose()
      }),
    )

    this.subscriptions.add(
      textEditor.onDidChangeCursorPosition(({ cursor, newBufferPosition }) => {
        const lastBufferPosition = this.lastCursorPositions.get(cursor)
        if (!lastBufferPosition || !lastBufferPosition.isEqual(newBufferPosition)) {
          this.lastCursorPositions.set(cursor, newBufferPosition)
          this.ignoreTooltipInvocation = false
        }
        if (this.tooltipFollows === 'Mouse') {
          this.removeTooltip()
        }
      }),
    )
    this.subscriptions.add(
      textEditor.getBuffer().onDidChangeText(() => {
        const cursors = textEditor.getCursors()
        cursors.forEach(cursor => {
          this.lastCursorPositions.set(cursor, cursor.getBufferPosition())
        })
        if (this.tooltipFollows !== 'Mouse') {
          this.ignoreTooltipInvocation = true
          this.removeTooltip()
        }
      }),
    )
    this.updateGutter()
    this.listenForCurrentLine()
  }
  listenForCurrentLine() {
    this.subscriptions.add(
      this.textEditor.observeCursors(cursor => {
        const handlePositionChange = ({ start, end }) => {
          const gutter = this.gutter
          if (!gutter || this.subscriptions.disposed) return
          // We need that Range.fromObject hack below because when we focus index 0 on multi-line selection
          // end.column is the column of the last line but making a range out of two and then accesing
          // the end seems to fix it (black magic?)
          const currentRange = Range.fromObject([start, end])
          const linesRange = Range.fromObject([
            [start.row, 0],
            [end.row, Infinity],
          ])
          const currentIsEmpty = currentRange.isEmpty()

          // NOTE: Atom does not paint gutter if multi-line and last line has zero index
          if (start.row !== end.row && currentRange.end.column === 0) {
            linesRange.end.row--
          }
          if (this.lastRange && this.lastRange.isEqual(linesRange) && currentIsEmpty === this.lastIsEmpty) return
          if (this.currentLineMarker) {
            this.currentLineMarker.destroy()
            this.currentLineMarker = null
          }
          this.lastRange = linesRange
          this.lastIsEmpty = currentIsEmpty

          this.currentLineMarker = this.textEditor.markScreenRange(linesRange, {
            invalidate: 'never',
          })
          const item = document.createElement('span')
          item.className = `line-number cursor-line linter-cursor-line ${currentIsEmpty ? 'cursor-line-no-selection' : ''}`
          gutter.decorateMarker(this.currentLineMarker, {
            item,
            class: 'linter-row',
          })
        }

        const cursorMarker = cursor.getMarker()
        const subscriptions = new CompositeDisposable()
        subscriptions.add(
          cursorMarker.onDidChange(({ newHeadScreenPosition, newTailScreenPosition }) => {
            handlePositionChange({
              start: newHeadScreenPosition,
              end: newTailScreenPosition,
{'fraction_non_alphanumeric': 0.10349472423856919, 'fraction_numerical': 0.00044900097283544113, 'mean_word_length': 2.650368751707184, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, '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': '2838482', 'n_tokens_mistral': 3659, 'n_tokens_neox': 3627, 'n_words': 771}