Repository: h2oai/wave
File: ui/src/text_annotator.tsx (TypeScript)

Code before:
        if (timeoutRef.current) {
          clearTimeout(timeoutRef.current)
          timeoutRef.current = null
        }
        const
          selectedText = window.getSelection(),
          startElData = selectedText?.anchorNode?.parentElement?.dataset,
          endElData = selectedText?.focusNode?.parentElement?.dataset

        if (!startElData || !endElData) return

        const
          selectedTextStr = selectedText.toString(),
          startTokenIdx = parseInt(smart_selection ? startElData!.key! : startElData!.start!),
          endTokenIdx = target ? (startTokenIdx + selectedTextStr.length) : parseInt(smart_selection ? endElData!.key! : endElData!.end!),
          max = Math.max(startTokenIdx, endTokenIdx),
          min = Math.min(startTokenIdx, endTokenIdx)

        if (target && selectedTextStr.length) {
          // smart selection on double click
          const targetDataIdx = parseInt(target.dataset.key!)
          let strLength = 0 // number of characters right to the selection target; if not 0, we loop to the left direction
          for (let i = 0; i <= selectedTextStr.length; i++) {
            const tokenIdx = targetDataIdx + (!strLength ? i : (strLength - i))
            tokens[tokenIdx].tag = activeTag
            if (/[^a-z0-9]/i.test(tokens[tokenIdx + (!strLength ? 1 : -1)]?.text)) {
              strLength = i + 1
            }
          }
        } else {
          // selection by dragging or clicking
          for (let i = min; i <= max; i++) {
            // HACK: Ignore accompanying spaces selected by double clicking in Firefox
            // HACK: Ignore characters returned by Firefox when user hovers over the part of the prev/next character
            if (!smart_selection && max - min + 1 !== selectedTextStr.length) {
              if (i === min && selectedTextStr.charAt(0) !== tokens[i].text) continue
              if (i === max && selectedTextStr.charAt(selectedTextStr.length - 1) !== tokens[i].text) continue
            }
            tokens[i].tag = activeTag
          }
        }

        setTokens([...tokens])
        submitWaveArgs()
        window.getSelection()?.removeAllRanges()
      },
      onMarkHover = (idx: U) => () => setHoveredTagIdx(idx),
      onMarkMouseOut = () => setHoveredTagIdx(null),
      shouldShowRemoveIcon = (idx: U, tag: S) => {
        if (hoveredTagIdx === null) return false
        // Check if same tag as hovered and first mark of an annotation.

Review comment (suggestion):

```suggestion
            if (/[^a-z0-9]/i.test(tokens[tokenIdx + (strLength ? -1 : 1)]?.text)) {
```

Code after:
          return arr
        }, [] as TextAnnotatorItem[]) as unknown as Rec[]
        if (trigger) wave.push()
      },
      activateTag = (tagName: S) => () => setActiveTag(tagName),
      removeAnnotation = (idx: U) => (ev: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
        ev.stopPropagation() // Stop event bubbling so that annotate is not called.
        const tagToRemove = tokens[idx].tag
        for (let i = idx; tokens[i]?.tag === tagToRemove; i++) tokens[i].tag = undefined
        startElPropsRef.current = undefined
        setTokens([...tokens])
        submitWaveArgs()
      },
      annotate = (endElProps: TokenMouseEventProps) => {
        if (!startElPropsRef.current) return
        const
          startElProps = startElPropsRef.current,
          max = smartSelection ? Math.max(startElProps.end, endElProps.end) : Math.max(startElProps.key, endElProps.key),
          min = smartSelection ? Math.min(startElProps.start, endElProps.start) : Math.min(startElProps.key, endElProps.key),
          // Remove new line characters because Firefox does count them.
          selectedStr = window.getSelection()?.toString().replace(/\r?\n|\r/g, "")

        for (let i = min; i <= max; i++) {
          // Ignore characters returned when user hovers over the part of the prev/next character.
          if (!smartSelection && selectedStr && max - min + 1 !== selectedStr.length) {
            // Check whether the first character highlighted by the browser corresponds with the character returned by the mouse event.
            if (i === min && selectedStr.charAt(0) !== tokens[i].text) continue
            // Check whether the last character highlighted by the browser corresponds with the character returned by the mouse event.
            if (i === max && selectedStr.charAt(selectedStr.length - 1) !== tokens[i].text) continue
          }
          tokens[i].tag = activeTag
        }

        if (smartSelection) {
          if (!isNaN(+tokens[startElProps.key].text)) {
            annotateNumbersAroundToken(startElProps.key, tokens, activeTag)
          }
          if (startElProps.key !== endElProps.key && !isNaN(+tokens[endElProps.key].text)) {
            annotateNumbersAroundToken(endElProps.key, tokens, activeTag)
          }
        }

        setTokens([...tokens])
        submitWaveArgs()
        window.getSelection()?.removeAllRanges()
        startElPropsRef.current = undefined
      },
      onMarkHover = (idx: U) => () => setHoveredTagIdx(idx),
      onMarkMouseOut = () => setHoveredTagIdx(null),
      shouldShowRemoveIcon = (idx: U, tag: S) => {
        if (hoveredTagIdx === null) return false