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 void setLineState(int line, TState state) {
    synchronized (lock) {
      lineStates.set(line, state);
      if (!state.getIsDirty() && firstDirtyLine != null && firstDirtyLine.equals(line)) {
        firstDirtyLine++;
      }

      if (!state.getIsDirty() && lastDirtyLine != null && lastDirtyLine.equals(line)) {
        firstDirtyLine = null;
        lastDirtyLine = null;
      }
    }
  }
  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);
  }