IDEA-63443 Formatter: Improve formatter performance during operations that require big number of small changes

1. Formatter performs whole document text replacement instead of applying thousand of small changes if necessary;
2. Common algorithm of line translation between two text changes is moved to util class in order to be reused during bookmarks restoring on bulk formatter processing;
3. EditorImpl is document bulk processing aware now (size container recalculation is adapted);
4. Soft wrap model caches information necessary for 'is EDT' check;
This commit is contained in:
Denis Zhdanov
2010-12-23 15:50:04 +03:00
parent 3eab2806ec
commit 7003f54c9a
10 changed files with 481 additions and 44 deletions
@@ -0,0 +1,71 @@
/*
* Copyright 2000-2010 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.formatting;
import com.intellij.openapi.editor.TextChange;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Encapsulates logic of merging set of changes into particular text.
* <p/>
* Thread-safe.
*
* @author Denis Zhdanov
* @since 12/22/10 12:02 PM
*/
public class BulkChangesMerger {
/**
* Merges given changes within the given text and returns result.
*
* @param text text to apply given changes for
* @param changes changes to apply to the given text. It's assumed that there are no intersections between them and that they
* are sorted by offsets in ascending order
* @return merge result
*/
@SuppressWarnings({"MethodMayBeStatic"})
public CharSequence merge(@NotNull char[] text, @NotNull List<TextChange> changes) {
int newLength = text.length;
for (TextChange change : changes) {
newLength += change.getText().length() - (change.getEnd() - change.getStart());
}
char[] data = new char[newLength];
int oldEndOffset = text.length;
int newEndOffset = data.length;
for (int i = changes.size() - 1; i >= 0; i--) {
TextChange change = changes.get(i);
// Copy all unprocessed symbols from initial text that lay after the changed offset.
int symbolsToMoveNumber = oldEndOffset - change.getEnd();
System.arraycopy(text, change.getEnd(), data, newEndOffset - symbolsToMoveNumber, symbolsToMoveNumber);
newEndOffset -= symbolsToMoveNumber;
// Copy all change symbols.
char[] changeSymbols = change.getChars();
newEndOffset -= changeSymbols.length;
System.arraycopy(changeSymbols, 0, data, newEndOffset, changeSymbols.length);
oldEndOffset = change.getStart();
}
if (oldEndOffset > 0) {
System.arraycopy(text, 0, data, 0, oldEndOffset);
}
return new String(data);
}
}
@@ -0,0 +1,106 @@
/*
* Copyright 2000-2010 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.formatting;
import com.intellij.ide.bookmarks.Bookmark;
import com.intellij.ide.bookmarks.BookmarkManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.diff.Diff;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* There is a possible case that we understand that formatting introduces big number of changes to the underlying document.
* That number may be big enough for that their subsequent appliance is much slower than building resulting text directly
* and replacing the whole document text.
* <p/>
* Current class defines a contract for a listener interested in such situations.
* <p/>
* Not thread-safe.
*
* @author Denis Zhdanov
* @since 12/23/10 11:38 AM
*/
public class BulkReformatListener {
private final List<BookmarkData> myBookmarks = new ArrayList<BookmarkData>();
private final Project myProject;
private final VirtualFile myVirtualFile;
public BulkReformatListener(@NotNull Project project, VirtualFile virtualFile) {
myProject = project;
myVirtualFile = virtualFile;
}
/**
* Is expected to be called before bulk processing.
*
* @param document document which text is about to be reformatted at bulk mode
* @param newText formatted text that is about to replace the text at the given document
*/
public void beforeProcessing(@NotNull Document document, @NotNull CharSequence newText) {
reset();
Diff.Change change = Diff.buildChanges(document.getCharsSequence(), newText);
if (change == null) {
return;
}
BookmarkManager manager = BookmarkManager.getInstance(myProject);
for (Bookmark bookmark : manager.getValidBookmarks()) {
if (bookmark.isValid() && bookmark.getDocument() == document) {
int newLine = Diff.translateLine(change, bookmark.getLine());
if (newLine >= 0) {
myBookmarks.add(new BookmarkData(bookmark.getDescription(), bookmark.getLine()));
}
}
}
}
/**
* Is expected to be called just after bulk reformat processing.
*/
public void afterProcessing() {
if (myBookmarks.isEmpty()) {
return;
}
final BookmarkManager manager = BookmarkManager.getInstance(myProject);
for (BookmarkData bookmark : myBookmarks) {
manager.addTextBookmark(myVirtualFile, bookmark.line, bookmark.description);
}
reset();
}
private void reset() {
myBookmarks.clear();
}
private static class BookmarkData {
public final String description;
public final int line;
BookmarkData(String description, int line) {
this.description = description;
this.line = line;
}
}
}
@@ -18,7 +18,9 @@ package com.intellij.formatting;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.TextChange;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.impl.softwrap.TextChangeImpl;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
@@ -32,7 +34,16 @@ import java.util.*;
class FormatProcessor {
/**
* There is a possible case that formatting introduced big number of changes to the underlying document. That number may be
* big enough for that their subsequent appliance is much slower than direct replacing of the whole document text.
* <p/>
* Current constant holds minimum number of changes that should trigger such <code>'replace whole text'</code> optimization.
*/
private static final int BULK_REPLACE_OPTIMIZATION_CRITERIA = 3000;
private static final Logger LOG = Logger.getInstance("#com.intellij.formatting.FormatProcessor");
private static final BulkChangesMerger ourBulkChangesMerger = new BulkChangesMerger();
private LeafBlockWrapper myCurrentBlock;
@@ -74,7 +85,7 @@ class FormatProcessor {
private LeafBlockWrapper myFirstTokenBlock;
private LeafBlockWrapper myLastTokenBlock;
private SortedMap<TextRange, Pair<AbstractBlockWrapper, Boolean>> myPreviousDependencies =
new TreeMap<TextRange, Pair<AbstractBlockWrapper, Boolean>>(new Comparator<TextRange>() {
public int compare(final TextRange o1, final TextRange o2) {
@@ -91,6 +102,8 @@ class FormatProcessor {
private WhiteSpace myLastWhiteSpace;
private boolean myDisposed;
private CodeStyleSettings.IndentOptions myJavaIndentOptions;
private BulkReformatListener myBulkReformatListener;
public FormatProcessor(final FormattingDocumentModel docModel,
Block rootBlock,
@@ -198,15 +211,31 @@ class FormatProcessor {
myJavaIndentOptions = javaIndentOptions;
}
private static void doModify(final List<LeafBlockWrapper> blocksToModify, final FormattingModel model,
/**
* @param bulkReformatListener new listener to use; <code>null</code> to reset any listener registered before
*/
public void setBulkReformatListener(@Nullable BulkReformatListener bulkReformatListener) {
myBulkReformatListener = bulkReformatListener;
}
private void doModify(final List<LeafBlockWrapper> blocksToModify, final FormattingModel model,
CodeStyleSettings.IndentOptions indentOption, CodeStyleSettings.IndentOptions javaOptions) {
final int blocksToModifyCount = blocksToModify.size();
final boolean bulkReformat = blocksToModifyCount > 50;
final DocumentEx updatedDocument = bulkReformat ? getAffectedDocument(model) : null;
if (updatedDocument != null) {
updatedDocument.setInBulkUpdate(true);
}
DocumentEx updatedDocument = null;
try {
if (blocksToModifyCount > BULK_REPLACE_OPTIMIZATION_CRITERIA) {
if (applyChangesAtBulkMode(blocksToModify, model, indentOption)) {
return;
}
}
final boolean bulkReformat = blocksToModifyCount > 50;
updatedDocument = bulkReformat ? getAffectedDocument(model) : null;
if (updatedDocument != null) {
updatedDocument.setInBulkUpdate(true);
}
int shift = 0;
for (int i = 0; i < blocksToModifyCount; ++i) {
final LeafBlockWrapper block = blocksToModify.get(i);
@@ -226,6 +255,54 @@ class FormatProcessor {
}
}
/**
* Decides whether applying formatter changes should be applied incrementally one-by-one or merge result should be
* constructed locally and the whole document text should be replaced. Performs such single bulk change if necessary.
*
* @param blocksToModify changes introduced by formatter
* @param model current formatting model
* @param indentOption indent options to use
* @return <code>true</code> if given changes are applied to the document (i.e. no further processing is required);
* <code>false</code> otherwise
* @see BulkReformatListener
*/
private boolean applyChangesAtBulkMode(final List<LeafBlockWrapper> blocksToModify, final FormattingModel model,
CodeStyleSettings.IndentOptions indentOption)
{
FormattingDocumentModel documentModel = model.getDocumentModel();
Document document = documentModel.getDocument();
if (document == null) {
return false;
}
List<TextChange> changes = new ArrayList<TextChange>();
for (LeafBlockWrapper block : blocksToModify) {
WhiteSpace whiteSpace = block.getWhiteSpace();
CharSequence newWs = documentModel.adjustWhiteSpaceIfNecessary(
whiteSpace.generateWhiteSpace(indentOption), whiteSpace.getStartOffset(), whiteSpace.getEndOffset()
);
changes.add(new TextChangeImpl(newWs, whiteSpace.getStartOffset(), whiteSpace.getEndOffset()));
}
CharSequence mergeResult = ourBulkChangesMerger.merge(document.getChars(), changes);
if (myBulkReformatListener != null) {
myBulkReformatListener.beforeProcessing(document, mergeResult);
}
document.replaceString(0, document.getTextLength(), mergeResult);
cleanupBlocks(blocksToModify);
if (myBulkReformatListener != null) {
myBulkReformatListener.afterProcessing();
}
return true;
}
private static void cleanupBlocks(List<LeafBlockWrapper> blocks) {
for (LeafBlockWrapper block : blocks) {
block.getParent().dispose();
block.dispose();
}
blocks.clear();
}
@Nullable
private static DocumentEx getAffectedDocument(final FormattingModel model) {
if (model instanceof DocumentBasedFormattingModel) {
@@ -19,9 +19,12 @@ package com.intellij.formatting;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.formatter.DocumentBasedFormattingModel;
import com.intellij.psi.formatter.FormattingDocumentModelImpl;
import com.intellij.psi.formatter.PsiBasedFormattingModel;
import com.intellij.util.IncorrectOperationException;
@@ -131,13 +134,40 @@ public class FormatterImpl extends FormatterEx
FormatTextRanges affectedRanges) throws IncorrectOperationException {
disableFormatting();
try {
new FormatProcessor(model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, affectedRanges).format(model);
FormatProcessor processor =
new FormatProcessor(model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, affectedRanges);
setupFormatProcessorIfPossible(processor, model);
processor.format(model);
} finally {
enableFormatting();
}
}
private static void setupFormatProcessorIfPossible(@NotNull FormatProcessor processor, @NotNull FormattingModel model) {
if (!(model instanceof DocumentBasedFormattingModel)) {
return;
}
DocumentBasedFormattingModel formattingModel = (DocumentBasedFormattingModel)model;
Project project = formattingModel.getProject();
if (project == null) {
return;
}
PsiFile file = formattingModel.getFile();
if (file == null) {
return;
}
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) {
return;
}
processor.setBulkReformatListener(new BulkReformatListener(project, virtualFile));
}
public void formatWithoutModifications(FormattingDocumentModel model,
Block rootBlock,
CodeStyleSettings settings,
@@ -35,12 +35,13 @@ import org.jetbrains.annotations.Nullable;
* @author lesya
*/
public class DocumentBasedFormattingModel implements FormattingModel {
private final Block myRootBlock;
private final Block myRootBlock;
private final FormattingDocumentModel myDocumentModel;
private final Document myDocument;
private final Project myProject;
private final CodeStyleSettings mySettings;
private final FileType myFileType;
private final Document myDocument;
private final Project myProject;
private final CodeStyleSettings mySettings;
private final FileType myFileType;
private final PsiFile myFile;
public DocumentBasedFormattingModel(final Block rootBlock,
final Document document,
@@ -53,6 +54,7 @@ public class DocumentBasedFormattingModel implements FormattingModel {
myProject = project;
mySettings = settings;
myFileType = fileType;
myFile = file;
myDocumentModel = new FormattingDocumentModelImpl(document,file);
}
@@ -65,6 +67,7 @@ public class DocumentBasedFormattingModel implements FormattingModel {
myProject = project;
mySettings = settings;
myFileType = fileType;
myFile = file;
myDocumentModel = FormattingDocumentModelImpl.createOn(file);
myDocument = myDocumentModel.getDocument();
}
@@ -213,6 +216,10 @@ public class DocumentBasedFormattingModel implements FormattingModel {
return myProject;
}
public PsiFile getFile() {
return myFile;
}
@Nullable
public static String mergeWsWithCdataMarker(String whiteSpace, final String s, final int cdataPos) {
final int firstCrInGeneratedWs = whiteSpace.indexOf('\n');
@@ -0,0 +1,62 @@
/*
* Copyright 2000-2010 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.formatting;
import com.intellij.openapi.editor.TextChange;
import com.intellij.openapi.editor.impl.softwrap.TextChangeImpl;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
/**
* @author Denis Zhdanov
* @since 12/22/2010
*/
public class BulkChangesMergerTest {
private BulkChangesMerger myMerger;
@Before
public void setUp() {
myMerger = new BulkChangesMerger();
}
@Test
public void simpleReplaceInTheMiddle() {
doTest("abcd", "a123d", c("123", 1, 3));
}
@Test
public void disjointInserts() {
doTest("abcd", "a1b2c3d45", c("1", 1), c("2", 2), c("3", 3), c("45", 4));
}
private static TextChange c(String text, int offset) {
return c(text, offset, offset);
}
private static TextChange c(String text, int start, int end) {
return new TextChangeImpl(text, start, end);
}
private void doTest(String initial, String expected, TextChange ... changes) {
CharSequence actual = myMerger.merge(initial.toCharArray(), Arrays.asList(changes));
assertEquals(expected, actual.toString());
}
}
@@ -147,7 +147,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
private final Key<Object> MOUSE_DRAGGED_GROUP = Key.create("MouseDraggedGroup");
private final DocumentListener myEditorDocumentAdapter;
private final DocumentListener myEditorDocumentAdapter;
private final SettingsImpl mySettings;
@@ -270,6 +270,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
myCommandProcessor = CommandProcessor.getInstance();
myEditorDocumentAdapter = new EditorDocumentAdapter();
DocumentBulkUpdateListener editorDocumentBulkAdapter = new EditorDocumentBulkUpdateAdapter();
project.getMessageBus().connect().subscribe(DocumentBulkUpdateListener.TOPIC, editorDocumentBulkAdapter);
myMouseMotionListeners = ContainerUtil.createEmptyCOWList();
myMarkupModelListener = new MarkupModelListener() {
@@ -1316,13 +1318,35 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), height);
}
private void beforeChangedUpdate(DocumentEvent e) {
if (!myDocument.isInBulkUpdate()) {
Rectangle visibleArea = getScrollingModel().getVisibleArea();
Point pos = visualPositionToXY(getCaretModel().getVisualPosition());
myCaretUpdateVShift = pos.y - visibleArea.y;
private void bulkUpdateStarted() {
}
private void bulkUpdateFinished() {
if (myScrollPane == null) {
return;
}
stopOptimizedScrolling();
mySelectionModel.removeBlockSelection();
mySizeContainer.reset();
validateSize();
updateGutterSize();
repaintToScreenBottom(0);
updateCaretCursor();
}
private void beforeChangedUpdate(DocumentEvent e) {
if (myDocument.isInBulkUpdate()) {
// Assuming that the job is done at bulk listener callback methods.
return;
}
Rectangle visibleArea = getScrollingModel().getVisibleArea();
Point pos = visualPositionToXY(getCaretModel().getVisualPosition());
myCaretUpdateVShift = pos.y - visibleArea.y;
// We assume that size container is already notified with the visual line widths during soft wraps processing
if (!mySoftWrapModel.isSoftWrappingEnabled()) {
mySizeContainer.beforeChange(e);
@@ -1330,7 +1354,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
}
private void changedUpdate(DocumentEvent e) {
if (myScrollPane == null) return;
if (myScrollPane == null || myDocument.isInBulkUpdate()) return;
stopOptimizedScrolling();
mySelectionModel.removeBlockSelection();
@@ -5129,6 +5153,18 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
}
}
class EditorDocumentBulkUpdateAdapter implements DocumentBulkUpdateListener {
@Override
public void updateStarted(Document doc) {
bulkUpdateStarted();
}
@Override
public void updateFinished(Document doc) {
bulkUpdateFinished();
}
}
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private class EditorSizeContainer {
@@ -27,6 +27,7 @@ import com.intellij.openapi.editor.impl.softwrap.*;
import com.intellij.openapi.editor.impl.softwrap.mapping.CachingSoftWrapDataMapper;
import com.intellij.openapi.editor.impl.softwrap.mapping.SoftWrapApplianceManager;
import com.intellij.openapi.editor.impl.softwrap.mapping.SoftWrapAwareVisualSizeManager;
import com.intellij.reference.SoftReference;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -72,6 +73,12 @@ public class SoftWrapModelImpl implements SoftWrapModelEx, PrioritizedDocumentLi
private final SoftWrapAwareVisualSizeManager myVisualSizeManager;
private final EditorEx myEditor;
/**
* We don't want to use soft wraps-aware processing from non-EDT and profiling shows that 'is EDT' check that is called too
* often is rather expensive. Hence, we use caching here for performance improvement.
*/
private SoftReference<Thread> myLastEdt = new SoftReference<Thread>(null);
/** Holds number of 'active' calls, i.e. number of methods calls of the current object within the current call stack. */
private int myActive;
private boolean myUseSoftWraps;
@@ -142,8 +149,16 @@ public class SoftWrapModelImpl implements SoftWrapModelEx, PrioritizedDocumentLi
// We check that current thread is EDT because attempt to retrieve information about visible area width may fail otherwise
Application application = ApplicationManager.getApplication();
if (!application.isDispatchThread()) {
return false;
Thread lastEdt = myLastEdt.get();
Thread currentThread = Thread.currentThread();
if (lastEdt != currentThread) {
if (application.isDispatchThread()) {
myLastEdt = new SoftReference<Thread>(currentThread);
}
else {
myLastEdt = new SoftReference<Thread>(null);
return false;
}
}
Rectangle visibleArea = myEditor.getScrollingModel().getVisibleArea();
@@ -17,7 +17,6 @@ package com.intellij.openapi.editor.impl.event;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.diff.Diff;
import org.jetbrains.annotations.NotNull;
@@ -165,33 +164,15 @@ public class DocumentEventImpl extends DocumentEvent {
public int translateLineViaDiffStrict(int line) {
if (myChange == null) buildDiff();
if (myChange == null) return line;
Diff.Change change = myChange;
int newLine = line;
while (change != null) {
if (line < change.line0) break;
if (line >= change.line0 + change.deleted) {
newLine += change.inserted - change.deleted;
} else {
return -1;
}
change = change.link;
}
return newLine;
if (change == null) return line;
return Diff.translateLine(change, line);
}
private void buildDiff() {
final String[] strings1 = LineTokenizer.tokenize(myOldString, false);
final String[] strings2 = LineTokenizer.tokenize(myNewString, false);
//Diff diff = new Diff(strings1, strings2);
//myChange = diff.diff_2(false);
myChange = Diff.buildChanges(strings1, strings2);
myChange = Diff.buildChanges(myOldString, myNewString);
}
public int getOptimizedLineShift() {
@@ -16,9 +16,12 @@
package com.intellij.util.diff;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.util.containers.Enumerator;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
@@ -28,6 +31,13 @@ import java.util.ArrayList;
public class Diff {
private static final Logger LOG = Logger.getInstance("#com.intellij.util.diff.Diff");
@Nullable
public static Change buildChanges(@NotNull CharSequence before, @NotNull CharSequence after) {
final String[] strings1 = LineTokenizer.tokenize(before, false);
final String[] strings2 = LineTokenizer.tokenize(after, false);
return buildChanges(strings1, strings2);
}
public static <T> Change buildChanges(T[] objects1, T[] objects2) {
// Old variant of enumerator worked incorrectly with null values.
// This check is to ensure that the corrected version does not introduce bugs.
@@ -46,6 +56,48 @@ public class Diff {
return builder.getFirstChange();
}
/**
* Tries to translate given line that pointed to the text before change to the line that points to the same text after the change.
*
* @param before text before change
* @param after text after change
* @param line target line before change
* @return translated line if the processing is ok; negative value otherwise
*/
public static int translateLine(@NotNull CharSequence before, @NotNull CharSequence after, int line) {
Change change = buildChanges(before, after);
if (change == null) {
return -1;
}
return translateLine(change, line);
}
/**
* Tries to translate given line that pointed to the text before change to the line that points to the same text after the change.
*
* @param change target change
* @param line target line before change
* @return translated line if the processing is ok; negative value otherwise
*/
public static int translateLine(@NotNull Change change, int line) {
int result = line;
Change currentChange = change;
while (currentChange != null) {
if (line < currentChange.line0) break;
if (line >= currentChange.line0 + currentChange.deleted) {
result += currentChange.inserted - currentChange.deleted;
} else {
return -1;
}
currentChange = currentChange.link;
}
return result;
}
public static class Change {
// todo remove. Return lists instead.
/**