protected boolean isMultiLineToken(TokenSourceWithStateV4<TState> lexer, Token token) {
    /*if (lexer != null && lexer.getLine() > token.getLine()) {
        return true;
    }*/

    int startLine = snapshot.findLineNumber(token.getStartIndex());
    int stopLine = snapshot.findLineNumber(token.getStopIndex() + 1);
    return startLine != stopLine;
  }
  protected ParseRequest<TState> adjustParseSpan(OffsetRegion span) {
    int start = span.getStart();
    int end = span.getEnd();

    if (firstDirtyLine != null) {
      int firstDirtyLineOffset =
          snapshot.findLineFromLineNumber(firstDirtyLine).getStart().getOffset();
      start = Math.min(start, firstDirtyLineOffset);
    }

    TState state = null;
    int startLine = snapshot.findLineNumber(start);
    while (startLine > 0) {
      TState lineState = lineStates.get(startLine - 1);
      if (!lineState.getIsMultiLineToken()) {
        state = lineState;
        break;
      }

      startLine--;
    }

    if (startLine == 0) {
      state = getStartState();
    }

    start = snapshot.findLineFromLineNumber(startLine).getStart().getOffset();
    int length = end - start;
    ParseRequest<TState> request = new ParseRequest<>(new OffsetRegion(start, length), state);
    return request;
  }
  protected AbstractTokensTaskTaggerSnapshot(
      @NonNull AbstractTokensTaskTaggerSnapshot<TState> reference,
      @NonNull DocumentSnapshot snapshot) {
    Parameters.notNull("reference", reference);
    Parameters.notNull("snapshot", snapshot);

    if (!snapshot.getVersionedDocument().equals(reference.snapshot.getVersionedDocument())) {
      throw new IllegalArgumentException("The target snapshot is not from the same document.");
    }

    if (snapshot.getVersion().getVersionNumber()
        <= reference.snapshot.getVersion().getVersionNumber()) {
      throw new UnsupportedOperationException(
          "The target snapshot must be a future version of the reference document.");
    }

    this.snapshot = snapshot;
    this.lineStates.addAll(reference.lineStates);
    this.firstDirtyLine = reference.firstDirtyLine;
    this.lastDirtyLine = reference.lastDirtyLine;
    Integer firstChangedLine = null;
    Integer lastChangedLine = null;

    for (DocumentVersion version = reference.snapshot.getVersion();
        version != null && version.getVersionNumber() < snapshot.getVersion().getVersionNumber();
        version = version.getNext()) {
      DocumentSnapshot source = version.getSnapshot();
      DocumentVersion targetVersion = version.getNext();
      assert targetVersion != null;
      DocumentSnapshot target = targetVersion.getSnapshot();
      NormalizedDocumentChangeCollection changes = version.getChanges();
      assert changes != null;
      for (int i = changes.size() - 1; i >= 0; i--) {
        DocumentChange change = changes.get(i);
        int lineCountDelta = change.getLineCountDelta();
        int oldOffset = change.getOldOffset();
        int oldLength = change.getOldLength();
        int newOffset = change.getNewOffset();
        int newLength = change.getNewLength();

        /* processChange */
        int oldStartLine = source.findLineNumber(oldOffset);
        int oldEndLine =
            oldLength == 0 ? oldStartLine : source.findLineNumber(oldOffset + oldLength - 1);
        if (lineCountDelta < 0) {
          lineStates.subList(oldStartLine, oldStartLine + Math.abs(lineCountDelta)).clear();
        } else if (lineCountDelta > 0) {
          TState endLineState = lineStates.get(oldStartLine);
          ArrayList<TState> insertedElements = new ArrayList<>();
          for (int j = 0; j < lineCountDelta; j++) {
            insertedElements.add(endLineState);
          }
          lineStates.addAll(oldStartLine, insertedElements);
        }

        if (lastDirtyLine != null && lastDirtyLine > oldStartLine) {
          lastDirtyLine += lineCountDelta;
        }

        if (lastChangedLine != null && lastChangedLine > oldStartLine) {
          lastChangedLine += lineCountDelta;
        }

        for (int j = oldStartLine; j <= oldEndLine + lineCountDelta; j++) {
          TState state = lineStates.get(i);
          lineStates.set(j, state.createDirtyState());
        }

        firstChangedLine =
            firstChangedLine != null ? Math.min(firstChangedLine, oldStartLine) : oldStartLine;
        lastChangedLine =
            lastChangedLine != null ? Math.max(lastChangedLine, oldEndLine) : oldEndLine;

        /* processAfterChange */
        if (firstChangedLine != null && lastChangedLine != null) {
          int startLine = firstChangedLine;
          int endLineInclusive = Math.min(lastChangedLine, source.getLineCount() - 1);

          firstChangedLine = null;
          lastChangedLine = null;

          /* forceRehighlightLines(startRehighlightLine, endRehighlightLine); */
          firstDirtyLine = firstDirtyLine != null ? Math.min(firstDirtyLine, startLine) : startLine;
          lastDirtyLine =
              lastDirtyLine != null ? Math.max(lastDirtyLine, endLineInclusive) : endLineInclusive;

          //                    int start =
          // currentSnapshot.findLineFromLineNumber(startLine).getStart().getOffset();
          //                    int end = (endLineInclusive == lineStates.size() - 1) ?
          // currentSnapshot.length() : currentSnapshot.findLineFromLineNumber(endLineInclusive +
          // 1).getStart().getOffset();
          //                    if (FIX_HIGHLIGHTER_UPDATE_BUG) {
          //                        fireHighlightsChange(start, document.getLength());
          //                    } else {
          //                        fireHighlightsChange(start, end);
          //                    }
        }
      }
    }

    firstDirtyLine = Math.min(firstDirtyLine, snapshot.getLineCount() - 1);
    lastDirtyLine = Math.min(lastDirtyLine, snapshot.getLineCount() - 1);
  }
 protected boolean tokenSkippedLines(int endLinePrevious, Token token) {
   int startLineCurrent = snapshot.findLineNumber(token.getStartIndex());
   return startLineCurrent > endLinePrevious + 1;
 }
  public List<TaggedPositionRegion<TokenTag<Token>>> getHighlights(int startOffset, int endOffset) {
    List<TaggedPositionRegion<TokenTag<Token>>> tags = new ArrayList<>();
    boolean updateOffsets = true;

    if (endOffset == Integer.MAX_VALUE) {
      endOffset = snapshot.length();
    }

    OffsetRegion span = OffsetRegion.fromBounds(startOffset, endOffset);

    if (failedTimeout) {
      return tags;
    }

    boolean spanExtended = false;

    int extendMultiLineSpanToLine = 0;
    OffsetRegion extendedSpan = span;

    synchronized (lock) {
      OffsetRegion requestedSpan = span;

      ParseRequest<TState> request = adjustParseSpan(span);
      TState startState = request.getState();
      span = request.getRegion();

      CharStream input;
      try {
        input = createInputStream(span);
      } catch (BadLocationException ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
        return tags;
      }

      TokenSourceWithStateV4<TState> lexer = createLexer(input, startState);
      lexer.setTokenFactory(new DocumentSnapshotTokenFactory(getEffectiveTokenSource(lexer)));

      Token previousToken = null;
      boolean previousTokenEndsLine = false;

      /* this is held outside the loop because only tokens which end at the end of a line
       * impact its value.
       */
      boolean lineStateChanged = false;

      while (true) {
        // TODO: perform this under a read lock
        Token token = lexer.nextToken();

        // The latter is true for EOF token with span.getEnd() at the end of the document
        boolean inBounds =
            token.getStartIndex() < span.getEnd() || token.getStopIndex() < span.getEnd();

        if (updateOffsets) {
          int startLineCurrent;
          if (token.getType() == Token.EOF) startLineCurrent = snapshot.getLineCount();
          else startLineCurrent = snapshot.findLineNumber(token.getStartIndex());

          // endLinePrevious is the line number the previous token ended on
          int endLinePrevious;
          if (previousToken != null)
            endLinePrevious = snapshot.findLineNumber(previousToken.getStopIndex() + 1);
          else endLinePrevious = snapshot.findLineNumber(span.getStart()) - 1;

          if (startLineCurrent > endLinePrevious + 1
              || (startLineCurrent == endLinePrevious + 1 && !previousTokenEndsLine)) {
            int firstMultilineLine = endLinePrevious;
            if (previousToken == null || previousTokenEndsLine) firstMultilineLine++;

            for (int i = firstMultilineLine; i < startLineCurrent; i++) {
              if (!lineStates.get(i).getIsMultiLineToken() || lineStateChanged)
                extendMultiLineSpanToLine = i + 1;

              if (inBounds) setLineState(i, lineStates.get(i).createMultiLineState());
            }
          }
        }

        if (token.getType() == Token.EOF) break;

        if (updateOffsets && isMultiLineToken(lexer, token)) {
          int startLine = snapshot.findLineNumber(token.getStartIndex());
          int stopLine = snapshot.findLineNumber(token.getStopIndex() + 1);
          for (int i = startLine; i < stopLine; i++) {
            if (!lineStates.get(i).getIsMultiLineToken()) extendMultiLineSpanToLine = i + 1;

            if (inBounds) setLineState(i, lineStates.get(i).createMultiLineState());
          }
        }

        boolean tokenEndsLine = tokenEndsAtEndOfLine(lexer, token);
        if (updateOffsets && tokenEndsLine) {
          TState stateAtEndOfLine = lexer.getCurrentState();
          int line = snapshot.findLineNumber(token.getStopIndex() + 1);
          lineStateChanged =
              lineStates.get(line).getIsMultiLineToken()
                  || !lineStates.get(line).equals(stateAtEndOfLine);

          // even if the state didn't change, we call SetLineState to make sure the
          // _first/_lastChangedLine values get updated.
          // have to check bounds for this one or the editor might not get an update (if the token
          // ends a line)
          if (updateOffsets && inBounds) setLineState(line, stateAtEndOfLine);

          if (lineStateChanged) {
            if (line < snapshot.getLineCount() - 1) {
              /* update the span's end position or the line state change won't be reflected
               * in the editor
               */
              int endPosition =
                  line < snapshot.getLineCount() - 2
                      ? snapshot.findLineFromLineNumber(line + 2).getStart().getOffset()
                      : snapshot.length();
              if (endPosition > extendedSpan.getEnd()) {
                spanExtended = true;
                extendedSpan = OffsetRegion.fromBounds(extendedSpan.getStart(), endPosition);
              }
            }
          }
        }

        if (token.getStartIndex() >= span.getEnd()) {
          break;
        }

        previousToken = token;
        previousTokenEndsLine = tokenEndsLine;

        if (token.getStopIndex() < requestedSpan.getStart()) {
          continue;
        }

        Collection<TaggedPositionRegion<TokenTag<Token>>> tokenClassificationSpans =
            getTagsForToken(token);
        if (tokenClassificationSpans != null) {
          tags.addAll(tokenClassificationSpans);
        }

        if (!inBounds) {
          break;
        }
      }
    }

    if (updateOffsets && extendMultiLineSpanToLine > 0) {
      int endPosition =
          extendMultiLineSpanToLine < snapshot.getLineCount() - 1
              ? snapshot
                  .findLineFromLineNumber(extendMultiLineSpanToLine + 1)
                  .getStart()
                  .getOffset()
              : snapshot.length();
      if (endPosition > extendedSpan.getEnd()) {
        spanExtended = true;
        extendedSpan = OffsetRegion.fromBounds(extendedSpan.getStart(), endPosition);
      }
    }

    if (updateOffsets && spanExtended) {
      /* Subtract 1 from each of these because the spans include the line break on their last
       * line, forcing it to appear as the first position on the following line.
       */
      assert extendedSpan.getEnd() > span.getEnd();
      int firstLine = snapshot.findLineNumber(span.getEnd());
      int lastLine = snapshot.findLineNumber(extendedSpan.getEnd()) - 1;
      // when considering the last line of a document, span and extendedSpan may end on the same
      // line
      forceRehighlightLines(firstLine, Math.max(firstLine, lastLine));
    }

    return tags;
  }