/** * Store given change merging it with previously stored ones if necessary. * <p/> * <b>Note:</b> it's assumed that given change offsets are related to the current state of the text (<code>'client text'</code>), * i.e. with all stored changes applied to it. Example: * <ol> * <li>Say, we have initial text <code>'12345'</code>;</li> * <li> * Suppose the change <code>'replace text at [2; 3) range with 'ABC''</code> is applied to it (stored at the current object). * End-users see the text <code>'12ABC45'</code> now; * </li> * <li> * This method is called with change like <code>'replace text at [1; 6) range with 'XY''</code>. Change range is assumed to * be related to the text visible to end-user, not initial one (<code>'12ABC45'</code>, not <code>'12345'</code>). * I.e. the user will see text <code>'1XY5'</code> now; * </li> * </ol> * * @param change change to store */ public void store(@NotNull TextChange change) { if (myChanges.isEmpty()) { myChanges.add(new ChangeEntry(new TextChangeImpl(change.getText(), change.getStart(), change.getEnd()), change.getStart())); return; } // There is a big chance that the document is processed sequentially from start to end, hence, it makes sense // to check if given change lays beyond other registered changes and register it quickly in case of success. ChangeEntry last = myChanges.get(myChanges.size() - 1); if (last.clientStartOffset + last.change.getText().length() < change.getStart()) { int clientShift = last.clientStartOffset - last.change.getStart() + last.change.getDiff(); myChanges.add(new ChangeEntry( new TextChangeImpl(change.getText(), change.getStart() - clientShift, change.getEnd() - clientShift), change.getStart() )); return; } int insertionIndex = doStore(change); if (insertionIndex < 0) { return; } mergeIfNecessary(insertionIndex); }
/** * Store given change merging it with previously stored ones if necessary. * <p/> * <b>Note:</b> it's assumed that given change offsets are related to the current state of the text (<code>'client text'</code>), * i.e. with all stored changes applied to it. Example: * <ol> * <li>Say, we have initial text <code>'12345'</code>;</li> * <li> * Suppose the change <code>'replace text at [2; 3) range with 'ABC''</code> is applied to it (stored at the current object). * End-users see the text <code>'12ABC45'</code> now; * </li> * <li> * This method is called with change like <code>'replace text at [1; 6) range with 'XY''</code>. Change range is assumed to * be related to the text visible to end-user, not initial one (<code>'12ABC45'</code>, not <code>'12345'</code>). * I.e. the user will see text <code>'1XY5'</code> now; * </li> * </ol> * * @param change change to store */ public void store(@Nonnull TextChange change) { if (myChanges.isEmpty()) { myChanges.add(new ChangeEntry(new TextChangeImpl(change.getText(), change.getStart(), change.getEnd()), change.getStart())); return; } // There is a big chance that the document is processed sequentially from start to end, hence, it makes sense // to check if given change lays beyond other registered changes and register it quickly in case of success. ChangeEntry last = myChanges.get(myChanges.size() - 1); if (last.clientStartOffset + last.change.getText().length() < change.getStart()) { int clientShift = last.clientStartOffset - last.change.getStart() + last.change.getDiff(); myChanges.add(new ChangeEntry( new TextChangeImpl(change.getText(), change.getStart() - clientShift, change.getEnd() - clientShift), change.getStart() )); return; } int insertionIndex = doStore(change); if (insertionIndex < 0) { return; } mergeIfNecessary(insertionIndex); }
@Test public void propertiesExposing() { int start = 1; int end = 10; String text = "test"; TextChange textChange = new TextChangeImpl(text, start, end); assertTrue(StringUtil.equals(text, textChange.getText())); assertArrayEquals(text.toCharArray(), textChange.getChars()); assertEquals(start, textChange.getStart()); assertEquals(end, textChange.getEnd()); }
/** * Given an offset of some location in the document, returns offset of this location after application of given changes. List of changes * is supposed to satisfy the same constraints as required by {@link #mergeToCharSequence(char[], int, List)} method. */ public int updateOffset(int originalOffset, @NotNull List<? extends TextChange> changes) { int offset = originalOffset; for (TextChange change : changes) { if (originalOffset > change.getStart()) { offset += change.getText().length() - (change.getEnd() - change.getStart()); } } return offset; }
/** * Allows to ask the storage for the list of changes that have intersections with the target text range (identified by the given * arguments). * * @param start target range start offset (inclusive) * @param end target range end offset (exclusive) * @return list that contains all registered changes that have intersections with the target text range */ @NotNull public List<? extends TextChange> getChanges(int start, int end) { assert start <= end; int changeStartIndex = getChangeIndex(start); if (changeStartIndex < 0) { changeStartIndex = -changeStartIndex - 1; } if (changeStartIndex >= myChanges.size()) { return Collections.emptyList(); } int changeEndIndex = getChangeIndex(end); boolean endInclusive = true; if (changeEndIndex < 0) { changeEndIndex = -changeEndIndex - 1; endInclusive = false; } List<TextChange> result = null; for (int i = changeStartIndex; i <= changeEndIndex; i++) { if (!endInclusive && i == changeEndIndex) { break; } if (result == null) { result = new ArrayList<TextChange>(); } result.add(myChanges.get(i).change); } return result == null ? Collections.<TextChange>emptyList() : result; }
/** * Given an offset of some location in the document, returns offset of this location after application of given changes. List of changes * is supposed to satisfy the same constraints as required by {@link #mergeToCharSequence(char[], int, List)} method. */ public int updateOffset(int originalOffset, @Nonnull List<? extends TextChange> changes) { int offset = originalOffset; for (TextChange change : changes) { if (originalOffset > change.getStart()) { offset += change.getText().length() - (change.getEnd() - change.getStart()); } } return offset; }
/** * Allows to ask the storage for the list of changes that have intersections with the target text range (identified by the given * arguments). * * @param start target range start offset (inclusive) * @param end target range end offset (exclusive) * @return list that contains all registered changes that have intersections with the target text range */ @Nonnull public List<? extends TextChange> getChanges(int start, int end) { assert start <= end; int changeStartIndex = getChangeIndex(start); if (changeStartIndex < 0) { changeStartIndex = -changeStartIndex - 1; } if (changeStartIndex >= myChanges.size()) { return Collections.emptyList(); } int changeEndIndex = getChangeIndex(end); boolean endInclusive = true; if (changeEndIndex < 0) { changeEndIndex = -changeEndIndex - 1; endInclusive = false; } List<TextChange> result = null; for (int i = changeStartIndex; i <= changeEndIndex; i++) { if (!endInclusive && i == changeEndIndex) { break; } if (result == null) { result = new ArrayList<TextChange>(); } result.add(myChanges.get(i).change); } return result == null ? Collections.<TextChange>emptyList() : result; }
private void checkResult(TextChange ... expected) { List<? extends TextChange> actual = myCollector.getChanges(); assertEquals(asList(expected), actual); }
private void update(@NotNull List<? extends TextChange> changes) { BulkChangesMerger merger = BulkChangesMerger.INSTANCE; for (Map.Entry<Editor, Integer> entry : myCaretOffsets.entrySet()) { entry.setValue(merger.updateOffset(entry.getValue(), changes)); } }
public void update(@Nonnull List<? extends TextChange> changes) { BulkChangesMerger merger = BulkChangesMerger.INSTANCE; for (Map.Entry<Editor, Integer> entry : myCaretOffsets.entrySet()) { entry.setValue(merger.updateOffset(entry.getValue(), changes)); } }
/** * Merges given changes within the given text and returns result as a new char sequence. * * @param text text to apply given changes for * @param textLength interested number of symbols from the given text to use * @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 */ public CharSequence mergeToCharSequence(@NotNull char[] text, int textLength, @NotNull List<? extends TextChange> changes) { return StringFactory.createShared(mergeToCharArray(text, textLength, changes)); }
/** * Returns aggregated and merged document changes sorted by their start offsets in ascending order. * <p/> * <b>Example</b> * <pre> * <ol> * <li>Suppose we have a document with text '0123456';</li> * <li> * Document range <code>[1; 3)</code> is removed (text <code>'12'</code> is removed, current text is * <code>'03456'</code>); * </li> * <li> * Document range <code>[1; 4)</code> (<code>'345'</code>) is replaced to <code>'abc'</code> * (current text is <code>'0abc6'</code>); * </li> * </ol> * </pre> * <p/> * So, initial document is <code>'0123456'</code>; final one is <code>'0abc6'</code>. We expect single aggregated change * to be returned then - <code>[1; 4)</code> with text <code>'12345'</code>, i.e. the range defines offset within the * current document and text shows initial document text at that range. * * @return aggregated and merged document changes sorted by their start offsets in ascending order */ @NotNull public List<? extends TextChange> getChanges() { return myChanges; }
/** * Merges given changes within the given text and returns result as a new char sequence. * * @param text text to apply given changes for * @param textLength interested number of symbols from the given text to use * @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 */ public CharSequence mergeToCharSequence(@Nonnull char[] text, int textLength, @Nonnull List<? extends TextChange> changes) { return StringFactory.createShared(mergeToCharArray(text, textLength, changes)); }
/** * Returns aggregated and merged document changes sorted by their start offsets in ascending order. * <p/> * <b>Example</b> * <pre> * <ol> * <li>Suppose we have a document with text '0123456';</li> * <li> * Document range <code>[1; 3)</code> is removed (text <code>'12'</code> is removed, current text is * <code>'03456'</code>); * </li> * <li> * Document range <code>[1; 4)</code> (<code>'345'</code>) is replaced to <code>'abc'</code> * (current text is <code>'0abc6'</code>); * </li> * </ol> * </pre> * <p/> * So, initial document is <code>'0123456'</code>; final one is <code>'0abc6'</code>. We expect single aggregated change * to be returned then - <code>[1; 4)</code> with text <code>'12345'</code>, i.e. the range defines offset within the * current document and text shows initial document text at that range. * * @return aggregated and merged document changes sorted by their start offsets in ascending order */ @Nonnull public List<? extends TextChange> getChanges() { return myChanges; }