/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.editor.impl; import com.intellij.openapi.editor.ex.LineIterator; import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.util.BitUtil; import com.intellij.util.text.CharArrayUtil; import com.intellij.util.text.MergingCharSequence; import gnu.trove.TByteArrayList; import gnu.trove.TIntArrayList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; import java.util.Arrays; /** * Data structure specialized for working with document text lines, i.e. stores information about line mapping to document * offsets and provides convenient ways to work with that information like retrieving target line by document offset etc. * <p/> * Immutable. */ public class LineSet{ private static final int MODIFIED_MASK = 0x4; private static final int SEPARATOR_MASK = 0x3; private final int[] myStarts; private final byte[] myFlags; private final int myLength; private LineSet(int[] starts, byte[] flags, int length) { myStarts = starts; myFlags = flags; myLength = length; } public static LineSet createLineSet(CharSequence text) { return createLineSet(text, false); } @NotNull private static LineSet createLineSet(@NotNull CharSequence text, boolean markModified) { TIntArrayList starts = new TIntArrayList(); TByteArrayList flags = new TByteArrayList(); LineTokenizer lineTokenizer = new LineTokenizer(text); while (!lineTokenizer.atEnd()) { starts.add(lineTokenizer.getOffset()); flags.add((byte) (lineTokenizer.getLineSeparatorLength() | (markModified ? MODIFIED_MASK : 0))); lineTokenizer.advance(); } return new LineSet(starts.toNativeArray(), flags.toNativeArray(), text.length()); } @NotNull LineSet update(@NotNull CharSequence prevText, int start, int end, @NotNull CharSequence replacement, boolean wholeTextReplaced) { if (myLength == 0) { return createLineSet(replacement, !wholeTextReplaced); } LineSet result = isSingleLineChange(start, end, replacement) ? updateInsideOneLine(findLineIndex(start), replacement.length() - (end - start)) : genericUpdate(prevText, start, end, replacement); if (doTest) { MergingCharSequence newText = new MergingCharSequence( new MergingCharSequence(prevText.subSequence(0, start), replacement), prevText.subSequence(end, prevText.length())); result.checkEquals(createLineSet(newText)); } return wholeTextReplaced ? result.clearModificationFlags() : result; } private boolean isSingleLineChange(int start, int end, @NotNull CharSequence replacement) { if (start == 0 && end == myLength && replacement.length() == 0) return false; int startLine = findLineIndex(start); return startLine == findLineIndex(end) && !CharArrayUtil.containLineBreaks(replacement) && !isLastEmptyLine(startLine); } @NotNull private LineSet updateInsideOneLine(int line, int lengthDelta) { int[] starts = myStarts.clone(); for (int i = line + 1; i < starts.length; i++) { starts[i] += lengthDelta; } byte[] flags = myFlags.clone(); flags[line] |= MODIFIED_MASK; return new LineSet(starts, flags, myLength + lengthDelta); } private LineSet genericUpdate(CharSequence prevText, int _start, int _end, CharSequence replacement) { int startOffset = _start; if (replacement.length() > 0 && replacement.charAt(0) == '\n' && startOffset > 0 && prevText.charAt(startOffset - 1) == '\r') { startOffset--; } int startLine = findLineIndex(startOffset); startOffset = getLineStart(startLine); int endOffset = _end; if (replacement.length() > 0 && replacement.charAt(replacement.length() - 1) == '\r' && endOffset < prevText.length() && prevText.charAt(endOffset) == '\n') { endOffset++; } int endLine = findLineIndex(endOffset); endOffset = getLineEnd(endLine); if (!isLastEmptyLine(endLine)) endLine++; replacement = new MergingCharSequence( new MergingCharSequence(prevText.subSequence(startOffset, _start), replacement), prevText.subSequence(_end, endOffset)); LineSet patch = createLineSet(replacement, true); return applyPatch(startOffset, endOffset, startLine, endLine, patch); } private void checkEquals(@NotNull LineSet fresh) { if (getLineCount() != fresh.getLineCount()) { throw new AssertionError(); } for (int i = 0; i < getLineCount(); i++) { boolean start = getLineStart(i) != fresh.getLineStart(i); boolean end = getLineEnd(i) != fresh.getLineEnd(i); boolean sep = getSeparatorLength(i) != fresh.getSeparatorLength(i); if (start || end || sep) { throw new AssertionError(); } } } @NotNull private LineSet applyPatch(int startOffset, int endOffset, int startLine, int endLine, @NotNull LineSet patch) { int lineShift = patch.myStarts.length - (endLine - startLine); int lengthShift = patch.myLength - (endOffset - startOffset); int newLineCount = myStarts.length + lineShift; int[] starts = new int[newLineCount]; byte[] flags = new byte[newLineCount]; for (int i = 0; i < startLine; i++) { starts[i] = myStarts[i]; flags[i] = myFlags[i]; } for (int i = 0; i < patch.myStarts.length; i++) { starts[startLine + i] = patch.myStarts[i] + startOffset; flags[startLine + i] = patch.myFlags[i]; } for (int i = endLine; i < myStarts.length; i++) { starts[lineShift + i] = myStarts[i] + lengthShift; flags[lineShift + i] = myFlags[i]; } return new LineSet(starts, flags, myLength + lengthShift); } public int findLineIndex(int offset) { if (offset < 0 || offset > myLength) { throw new IndexOutOfBoundsException("Wrong offset: " + offset + ". Should be in range: [0, " + myLength + "]"); } if (myLength == 0) return 0; if (offset == myLength) return getLineCount() - 1; int bsResult = Arrays.binarySearch(myStarts, offset); return bsResult >= 0 ? bsResult : -bsResult - 2; } @NotNull public LineIterator createIterator() { return new LineIteratorImpl(this); } public final int getLineStart(int index) { checkLineIndex(index); return isLastEmptyLine(index) ? myLength : myStarts[index]; } private boolean isLastEmptyLine(int index) { return index == myFlags.length && index > 0 && (myFlags[index - 1] & SEPARATOR_MASK) > 0; } public final int getLineEnd(int index) { checkLineIndex(index); return index >= myStarts.length - 1 ? myLength : myStarts[index + 1]; } private void checkLineIndex(int index) { if (index < 0 || index >= getLineCount()) { throw new IndexOutOfBoundsException("Wrong line: " + index + ". Available lines count: " + getLineCount()); } } final boolean isModified(int index) { checkLineIndex(index); return !isLastEmptyLine(index) && BitUtil.isSet(myFlags[index], MODIFIED_MASK); } @NotNull final LineSet setModified(int index) { if (isLastEmptyLine(index) || isModified(index)) return this; byte[] flags = myFlags.clone(); flags[index] |= MODIFIED_MASK; return new LineSet(myStarts, flags, myLength); } @NotNull LineSet clearModificationFlags(int startLine, int endLine) { if (startLine > endLine) { throw new IllegalArgumentException("endLine < startLine: " + endLine + " < " + startLine + "; lineCount: " + getLineCount()); } checkLineIndex(startLine); checkLineIndex(endLine - 1); if (isLastEmptyLine(endLine - 1)) endLine--; if (startLine >= endLine) return this; byte[] flags = myFlags.clone(); for (int i = startLine; i < endLine; i++) { flags[i] &= ~MODIFIED_MASK; } return new LineSet(myStarts, flags, myLength); } @NotNull LineSet clearModificationFlags() { byte[] flags = myFlags.clone(); for (int i = 0; i < flags.length; i++) { flags[i] &= ~MODIFIED_MASK; } return new LineSet(myStarts, flags, myLength); } final int getSeparatorLength(int index) { checkLineIndex(index); return index < myFlags.length ? myFlags[index] & SEPARATOR_MASK : 0; } final int getLineCount() { return myStarts.length + (isLastEmptyLine(myStarts.length) ? 1 : 0); } @TestOnly public static void setTestingMode(boolean testMode) { doTest = testMode; } private static boolean doTest; int getLength() { return myLength; } }