blob: 9d2c82c3c38ccbd9ff0fd164ec10a94398869cce [file] [log] [blame]
/*
* Copyright 2000-2012 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.psi.codeStyle.arrangement.engine;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.arrangement.*;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementMatchRule;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementSectionRule;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementSettingsToken;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementStandardSettingsAware;
import com.intellij.psi.codeStyle.arrangement.std.StdArrangementTokens;
import com.intellij.util.containers.*;
import com.intellij.util.containers.HashSet;
import com.intellij.util.containers.Stack;
import com.intellij.util.text.CharArrayUtil;
import gnu.trove.TIntArrayList;
import gnu.trove.TObjectIntHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.intellij.psi.codeStyle.arrangement.std.StdArrangementTokens.Section.END_SECTION;
import static com.intellij.psi.codeStyle.arrangement.std.StdArrangementTokens.Section.START_SECTION;
/**
* Encapsulates generic functionality of arranging file elements by the predefined rules.
* <p/>
* I.e. the general idea is to have a language-specific rules hidden by generic arrangement API and common arrangement
* engine which works on top of that API and performs the arrangement.
*
* @author Denis Zhdanov
* @since 7/20/12 1:56 PM
*/
public class ArrangementEngine {
/**
* Arranges given PSI root contents that belong to the given ranges.
* <b>Note:</b> After arrangement editor foldings we'll be preserved.
*
* @param editor
* @param file target PSI root
* @param ranges target ranges to use within the given root
*/
public void arrange(@NotNull final Editor editor, @NotNull PsiFile file, Collection<TextRange> ranges) {
arrange(file, ranges, new RestoreFoldArrangementCallback(editor));
}
/**
* Arranges given PSI root contents that belong to the given ranges.
* <b>Note:</b> Editor foldings are not expected to be preserved.
*
* @param file target PSI root
* @param ranges target ranges to use within the given root
*/
public void arrange(@NotNull PsiFile file, @NotNull Collection<TextRange> ranges) {
arrange(file, ranges, null);
}
/**
* Arranges given PSI root contents that belong to the given ranges.
*
* @param file target PSI root
* @param ranges target ranges to use within the given root
*/
@SuppressWarnings("MethodMayBeStatic")
public void arrange(@NotNull PsiFile file, @NotNull Collection<TextRange> ranges, @Nullable final ArrangementCallback callback) {
final Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
if (document == null) {
return;
}
final Rearranger<?> rearranger = Rearranger.EXTENSION.forLanguage(file.getLanguage());
if (rearranger == null) {
return;
}
final CodeStyleSettings settings = CodeStyleSettingsManager.getInstance(file.getProject()).getCurrentSettings();
ArrangementSettings arrangementSettings = settings.getCommonSettings(file.getLanguage()).getArrangementSettings();
if (arrangementSettings == null && rearranger instanceof ArrangementStandardSettingsAware) {
arrangementSettings = ((ArrangementStandardSettingsAware)rearranger).getDefaultSettings();
}
if (arrangementSettings == null) {
return;
}
final DocumentEx documentEx;
if (document instanceof DocumentEx && !((DocumentEx)document).isInBulkUpdate()) {
documentEx = (DocumentEx)document;
}
else {
documentEx = null;
}
final Context<? extends ArrangementEntry> context = Context.from(
rearranger, document, file, ranges, arrangementSettings, settings
);
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
if (documentEx != null) {
//documentEx.setInBulkUpdate(true);
}
try {
doArrange(context);
if (callback != null) {
callback.afterArrangement(context.moveInfos);
}
}
finally {
if (documentEx != null) {
//documentEx.setInBulkUpdate(false);
}
}
}
});
}
@SuppressWarnings("unchecked")
private static <E extends ArrangementEntry> void doArrange(Context<E> context) {
// The general idea is to process entries bottom-up where every processed group belongs to the same parent. We may not bother
// with entries text ranges then. We use a list and a stack for achieving that than.
//
// Example:
// Entry1 Entry2
// / \ / \
// Entry11 Entry12 Entry21 Entry22
//
// --------------------------
// Stage 1:
// list: Entry1 Entry2 <-- entries to process
// stack: [0, 0, 2] <-- holds current iteration info at the following format:
// (start entry index at the auxiliary list (inclusive); current index; end index (exclusive))
// --------------------------
// Stage 2:
// list: Entry1 Entry2 Entry11 Entry12
// stack: [0, 1, 2]
// [2, 2, 4]
// --------------------------
// Stage 3:
// list: Entry1 Entry2 Entry11 Entry12
// stack: [0, 1, 2]
// [2, 3, 4]
// --------------------------
// Stage 4:
// list: Entry1 Entry2 Entry11 Entry12
// stack: [0, 1, 2]
// [2, 4, 4]
// --------------------------
// arrange 'Entry11 Entry12'
// --------------------------
// Stage 5:
// list: Entry1 Entry2
// stack: [0, 1, 2]
// --------------------------
// Stage 6:
// list: Entry1 Entry2 Entry21 Entry22
// stack: [0, 2, 2]
// [2, 2, 4]
// --------------------------
// Stage 7:
// list: Entry1 Entry2 Entry21 Entry22
// stack: [0, 2, 2]
// [2, 3, 4]
// --------------------------
// Stage 8:
// list: Entry1 Entry2 Entry21 Entry22
// stack: [0, 2, 2]
// [2, 4, 4]
// --------------------------
// arrange 'Entry21 Entry22'
// --------------------------
// Stage 9:
// list: Entry1 Entry2
// stack: [0, 2, 2]
// --------------------------
// arrange 'Entry1 Entry2'
List<ArrangementEntryWrapper<E>> entries = new ArrayList<ArrangementEntryWrapper<E>>();
Stack<StackEntry> stack = new Stack<StackEntry>();
entries.addAll(context.wrappers);
stack.push(new StackEntry(0, context.wrappers.size()));
while (!stack.isEmpty()) {
StackEntry stackEntry = stack.peek();
if (stackEntry.current >= stackEntry.end) {
List<ArrangementEntryWrapper<E>> subEntries = entries.subList(stackEntry.start, stackEntry.end);
// arrange entries even if subEntries.size() == 1, because we don't want to miss new section comments here
doArrange(subEntries, context);
subEntries.clear();
stack.pop();
}
else {
ArrangementEntryWrapper<E> wrapper = entries.get(stackEntry.current++);
List<ArrangementEntryWrapper<E>> children = wrapper.getChildren();
if (!children.isEmpty()) {
entries.addAll(children);
stack.push(new StackEntry(stackEntry.end, children.size()));
}
}
}
}
/**
* Arranges (re-orders) given entries according to the given rules.
*
* @param entries entries to arrange
* @param sectionRules rules to use for arrangement
* @param rulesByPriority rules sorted by priority ('public static' rule will have higher priority than 'public')
* @param entryToSection mapping from arrangement entry to the parent section
* @return arranged list of the given rules
*/
@SuppressWarnings("AssignmentToForLoopParameter")
@NotNull
public static <E extends ArrangementEntry> List<E> arrange(@NotNull Collection<E> entries,
@NotNull List<ArrangementSectionRule> sectionRules,
@NotNull List<? extends ArrangementMatchRule> rulesByPriority,
@Nullable Map<E, ArrangementSectionRule> entryToSection)
{
List<E> arranged = ContainerUtilRt.newArrayList();
Set<E> unprocessed = ContainerUtilRt.newLinkedHashSet();
List<Pair<Set<ArrangementEntry>, E>> dependent = ContainerUtilRt.newArrayList();
for (E entry : entries) {
List<? extends ArrangementEntry> dependencies = entry.getDependencies();
if (dependencies == null) {
unprocessed.add(entry);
}
else {
if (dependencies.size() == 1 && dependencies.get(0) == entry.getParent()) {
// Handle a situation when the entry is configured to be at the first parent's children.
arranged.add(entry);
}
else {
Set<ArrangementEntry> first = new HashSet<ArrangementEntry>(dependencies);
dependent.add(Pair.create(first, entry));
}
}
}
Set<E> matched = new HashSet<E>();
MultiMap<ArrangementMatchRule, E> elementsByRule = new MultiMap<ArrangementMatchRule, E>();
for (ArrangementMatchRule rule : rulesByPriority) {
matched.clear();
for (E entry : unprocessed) {
if (entry.canBeMatched() && rule.getMatcher().isMatched(entry)) {
elementsByRule.putValue(rule, entry);
matched.add(entry);
}
}
unprocessed.removeAll(matched);
}
for (ArrangementSectionRule sectionRule : sectionRules) {
for (ArrangementMatchRule rule : sectionRule.getMatchRules()) {
final Collection<E> arrangedEntries = arrangeByRule(arranged, elementsByRule, rule);
if (entryToSection != null && arrangedEntries != null) {
for (E entry : arrangedEntries) {
entryToSection.put(entry, sectionRule);
}
}
}
}
arranged.addAll(unprocessed);
for (int i = 0; i < arranged.size() && !dependent.isEmpty(); i++) {
E e = arranged.get(i);
List<E> shouldBeAddedAfterCurrentElement = ContainerUtil.newArrayList();
for (Iterator<Pair<Set<ArrangementEntry>, E>> iterator = dependent.iterator(); iterator.hasNext(); ) {
Pair<Set<ArrangementEntry>, E> pair = iterator.next();
pair.first.remove(e);
if (pair.first.isEmpty()) {
iterator.remove();
shouldBeAddedAfterCurrentElement.add(pair.second);
}
}
// add dependent entries to the same section as main entry
if (entryToSection != null && entryToSection.containsKey(e)) {
final ArrangementSectionRule rule = entryToSection.get(e);
for (E e1 : shouldBeAddedAfterCurrentElement) {
entryToSection.put(e1, rule);
}
}
arranged.addAll(i + 1, shouldBeAddedAfterCurrentElement);
}
return arranged;
}
@Nullable
private static <E extends ArrangementEntry> Collection<E> arrangeByRule(@NotNull List<E> arranged,
@NotNull MultiMap<ArrangementMatchRule, E> elementsByRule,
@NotNull ArrangementMatchRule rule) {
if (elementsByRule.containsKey(rule)) {
final Collection<E> arrangedEntries = elementsByRule.remove(rule);
// Sort by name if necessary.
if (StdArrangementTokens.Order.BY_NAME.equals(rule.getOrderType())) {
sortByName((List<E>)arrangedEntries);
}
arranged.addAll(arrangedEntries);
return arrangedEntries;
}
return null;
}
private static <E extends ArrangementEntry> void sortByName(@NotNull List<E> entries) {
if (entries.size() < 2) {
return;
}
final TObjectIntHashMap<E> weights = new TObjectIntHashMap<E>();
int i = 0;
for (E e : entries) {
weights.put(e, ++i);
}
ContainerUtil.sort(entries, new Comparator<E>() {
@Override
public int compare(E e1, E e2) {
String name1 = e1 instanceof NameAwareArrangementEntry ? ((NameAwareArrangementEntry)e1).getName() : null;
String name2 = e2 instanceof NameAwareArrangementEntry ? ((NameAwareArrangementEntry)e2).getName() : null;
if (name1 != null && name2 != null) {
return name1.compareTo(name2);
}
else if (name1 == null && name2 == null) {
return weights.get(e1) - weights.get(e2);
}
else if (name2 == null) {
return -1;
}
else {
return 1;
}
}
});
}
@SuppressWarnings("unchecked")
private static <E extends ArrangementEntry> void doArrange(@NotNull List<ArrangementEntryWrapper<E>> wrappers,
@NotNull Context<E> context) {
if (wrappers.isEmpty()) {
return;
}
Map<E, ArrangementSectionRule> entryToSection = ContainerUtilRt.newHashMap();
Map<E, ArrangementEntryWrapper<E>> map = ContainerUtilRt.newHashMap();
List<E> arranged = ContainerUtilRt.newArrayList();
List<E> toArrange = ContainerUtilRt.newArrayList();
for (ArrangementEntryWrapper<E> wrapper : wrappers) {
E entry = wrapper.getEntry();
map.put(wrapper.getEntry(), wrapper);
if (!entry.canBeMatched()) {
// Split entries to arrange by 'can not be matched' rules.
// See IDEA-104046 for a problem use-case example.
if (toArrange.isEmpty()) {
arranged.addAll(arrange(toArrange, context.sectionRules, context.rulesByPriority, entryToSection));
}
arranged.add(entry);
toArrange.clear();
}
else {
toArrange.add(entry);
}
}
if (!toArrange.isEmpty()) {
arranged.addAll(arrange(toArrange, context.sectionRules, context.rulesByPriority, entryToSection));
}
final NewSectionInfo<E> newSectionsInfo = NewSectionInfo.create(arranged, entryToSection);
context.changer.prepare(wrappers, context);
// We apply changes from the last position to the first position in order not to bother with offsets shifts.
for (int i = arranged.size() - 1; i >= 0; i--) {
ArrangementEntryWrapper<E> arrangedWrapper = map.get(arranged.get(i));
ArrangementEntryWrapper<E> initialWrapper = wrappers.get(i);
ArrangementEntryWrapper<E> previous = i > 0 ? map.get(arranged.get(i - 1)) : null;
ArrangementEntryWrapper<E> previousInitial = i > 0 ? wrappers.get(i - 1) : null;
final ArrangementEntryWrapper<E> parentWrapper = initialWrapper.getParent();
if (arrangedWrapper.equals(initialWrapper)) {
if (previous != null && previous.equals(previousInitial) || previous == null && previousInitial == null) {
final int beforeOffset = arrangedWrapper.getStartOffset();
final int afterOffset = arrangedWrapper.getEndOffset();
context.changer.insertSection(context, arranged.get(i), newSectionsInfo, parentWrapper, beforeOffset, afterOffset);
continue;
}
}
ArrangementEntryWrapper<E> next = i < arranged.size() - 1 ? map.get(arranged.get(i + 1)) : null;
context.changer.replace(arrangedWrapper, initialWrapper, previous, next, context);
context.changer.insertSection(context, arranged.get(i), newSectionsInfo, arrangedWrapper, initialWrapper, parentWrapper);
}
}
private static class NewSectionInfo<E extends ArrangementEntry> {
private final Map<E, String> mySectionStarts = ContainerUtil.newHashMap();
private final Map<E, String> mySectionEnds = ContainerUtil.newHashMap();
private static <E extends ArrangementEntry> NewSectionInfo create(@NotNull List<E> arranged,
@NotNull Map<E, ArrangementSectionRule> entryToSection) {
final NewSectionInfo<E> info = new NewSectionInfo<E>();
boolean sectionIsOpen = false;
ArrangementSectionRule prevSection = null;
E prev = null;
for (E e : arranged) {
final ArrangementSectionRule section = entryToSection.get(e);
if (section != prevSection) {
closeSection(prevSection, prev, info, sectionIsOpen);
sectionIsOpen = false;
if (section != null) {
final String startComment = section.getStartComment();
if (StringUtil.isNotEmpty(startComment) && !isSectionEntry(e, startComment)) {
sectionIsOpen = true;
info.addSectionStart(e, startComment);
}
}
prevSection = section;
}
prev = e;
}
closeSection(prevSection, prev, info, sectionIsOpen);
return info;
}
public static boolean isSectionEntry(@NotNull ArrangementEntry entry, @NotNull String sectionText) {
if (entry instanceof TypeAwareArrangementEntry && entry instanceof TextAwareArrangementEntry) {
final Set<ArrangementSettingsToken> types = ((TypeAwareArrangementEntry)entry).getTypes();
if (types.size() == 1) {
final ArrangementSettingsToken type = types.iterator().next();
if (type.equals(START_SECTION) || type.equals(END_SECTION)) {
return StringUtil.equals(((TextAwareArrangementEntry)entry).getText(), sectionText);
}
}
}
return false;
}
private static <E extends ArrangementEntry> void closeSection(@Nullable ArrangementSectionRule section,
@Nullable E entry,
@NotNull NewSectionInfo<E> info,
boolean sectionIsOpen) {
if (sectionIsOpen) {
assert section != null && entry != null;
if (StringUtil.isNotEmpty(section.getEndComment())) {
info.addSectionEnd(entry, section.getEndComment());
}
}
}
private void addSectionStart(E entry, String comment) {
mySectionStarts.put(entry, comment);
}
private void addSectionEnd(E entry, String comment) {
mySectionEnds.put(entry, comment);
}
@Nullable
public String getStartComment(E entry) {
return mySectionStarts.get(entry);
}
@Nullable
public String getEndComment(E entry) {
return mySectionEnds.get(entry);
}
}
private static class Context<E extends ArrangementEntry> {
@NotNull public final List<ArrangementMoveInfo> moveInfos = ContainerUtilRt.newArrayList();
@NotNull public final Rearranger<E> rearranger;
@NotNull public final Collection<ArrangementEntryWrapper<E>> wrappers;
@NotNull public final Document document;
@NotNull public final List<? extends ArrangementMatchRule> rulesByPriority;
@NotNull public final CodeStyleSettings settings;
@NotNull public final Changer changer;
@NotNull public final List<ArrangementSectionRule> sectionRules;
private Context(@NotNull Rearranger<E> rearranger,
@NotNull Collection<ArrangementEntryWrapper<E>> wrappers,
@NotNull Document document,
@NotNull List<ArrangementSectionRule> sectionRules,
@NotNull List<? extends ArrangementMatchRule> rulesByPriority,
@NotNull CodeStyleSettings settings, @NotNull Changer changer)
{
this.rearranger = rearranger;
this.wrappers = wrappers;
this.document = document;
this.sectionRules = sectionRules;
this.rulesByPriority = rulesByPriority;
this.settings = settings;
this.changer = changer;
}
public void addMoveInfo(int oldStart, int oldEnd, int newStart) {
moveInfos.add(new ArrangementMoveInfo(oldStart, oldEnd, newStart));
}
public static <T extends ArrangementEntry> Context<T> from(@NotNull Rearranger<T> rearranger,
@NotNull Document document,
@NotNull PsiElement root,
@NotNull Collection<TextRange> ranges,
@NotNull ArrangementSettings arrangementSettings,
@NotNull CodeStyleSettings codeStyleSettings)
{
Collection<T> entries = rearranger.parse(root, document, ranges, arrangementSettings);
Collection<ArrangementEntryWrapper<T>> wrappers = new ArrayList<ArrangementEntryWrapper<T>>();
ArrangementEntryWrapper<T> previous = null;
for (T entry : entries) {
ArrangementEntryWrapper<T> wrapper = new ArrangementEntryWrapper<T>(entry);
if (previous != null) {
previous.setNext(wrapper);
wrapper.setPrevious(previous);
}
wrappers.add(wrapper);
previous = wrapper;
}
Changer changer;
if (document instanceof DocumentEx) {
changer = new RangeMarkerAwareChanger<T>((DocumentEx)document);
}
else {
changer = new DefaultChanger();
}
final List<? extends ArrangementMatchRule> rulesByPriority = arrangementSettings.getRulesSortedByPriority();
final List<ArrangementSectionRule> sectionRules = arrangementSettings.getSections();
return new Context<T>(rearranger, wrappers, document, sectionRules, rulesByPriority, codeStyleSettings, changer);
}
}
private static class StackEntry {
public int start;
public int current;
public int end;
StackEntry(int start, int count) {
this.start = start;
current = start;
end = start + count;
}
}
private abstract static class Changer<E extends ArrangementEntry> {
public abstract void prepare(@NotNull List<ArrangementEntryWrapper<E>> toArrange, @NotNull Context<E> context);
/**
* Replaces given 'old entry' by the given 'new entry'.
*
* @param newWrapper wrapper for an entry which text should replace given 'old entry' range
* @param oldWrapper wrapper for an entry which range should be replaced by the given 'new entry'
* @param previous wrapper which will be previous for the entry referenced via the given 'new wrapper'
* @param next wrapper which will be next for the entry referenced via the given 'new wrapper'
* @param context current context
*/
public abstract void replace(@NotNull ArrangementEntryWrapper<E> newWrapper,
@NotNull ArrangementEntryWrapper<E> oldWrapper,
@Nullable ArrangementEntryWrapper<E> previous,
@Nullable ArrangementEntryWrapper<E> next,
@NotNull Context<E> context);
public abstract void insert(@NotNull Context<E> context, int startOffset, @NotNull String text);
public abstract void insertSection(@NotNull Context<E> context,
@NotNull E entry,
@NotNull NewSectionInfo<E> newSectionsInfo,
@NotNull ArrangementEntryWrapper<E> arranged,
@NotNull ArrangementEntryWrapper<E> initial,
@Nullable ArrangementEntryWrapper<E> parent);
protected abstract void insertSection(@NotNull Context<E> context,
@NotNull E entry,
@NotNull NewSectionInfo<E> newSectionsInfo,
@Nullable ArrangementEntryWrapper<E> parent, int beforeOffset, int afterOffset);
protected int getBlankLines(@NotNull Context<E> context,
@Nullable ArrangementEntryWrapper<E> parentWrapper,
@NotNull ArrangementEntryWrapper<E> targetWrapper,
@Nullable ArrangementEntryWrapper<E> previousWrapper,
@Nullable ArrangementEntryWrapper<E> nextWrapper) {
final E target = targetWrapper.getEntry();
final E previous = previousWrapper == null ? null : previousWrapper.getEntry();
if (isTypeOf(target, END_SECTION) || isTypeOf(previous, START_SECTION)) {
return 0;
}
final E next = nextWrapper == null ? null : nextWrapper.getEntry();
if (next != null && isTypeOf(target, START_SECTION)) {
return context.rearranger.getBlankLines(context.settings, parentWrapper == null ? null : parentWrapper.getEntry(), previous, next);
}
return context.rearranger.getBlankLines(context.settings, parentWrapper == null ? null : parentWrapper.getEntry(), previous, target);
}
private boolean isTypeOf(@Nullable E element, @NotNull ArrangementSettingsToken token) {
if (element instanceof TypeAwareArrangementEntry) {
Set<ArrangementSettingsToken> types = ((TypeAwareArrangementEntry)element).getTypes();
return types.size() == 1 && token.equals(types.iterator().next());
}
return false;
}
}
private static class DefaultChanger<E extends ArrangementEntry> extends Changer<E> {
@NotNull private String myParentText;
private int myParentShift;
@Override
public void prepare(@NotNull List<ArrangementEntryWrapper<E>> toArrange, @NotNull Context<E> context) {
ArrangementEntryWrapper<E> parent = toArrange.get(0).getParent();
if (parent == null) {
myParentText = context.document.getText();
myParentShift = 0;
}
else {
myParentText = context.document.getCharsSequence().subSequence(parent.getStartOffset(), parent.getEndOffset()).toString();
myParentShift = parent.getStartOffset();
}
}
@SuppressWarnings("AssignmentToForLoopParameter")
@Override
public void replace(@NotNull ArrangementEntryWrapper<E> newWrapper,
@NotNull ArrangementEntryWrapper<E> oldWrapper,
@Nullable ArrangementEntryWrapper<E> previous,
@Nullable ArrangementEntryWrapper<E> next,
@NotNull Context<E> context)
{
// Calculate blank lines before the arrangement.
int blankLinesBefore = 0;
TIntArrayList lineFeedOffsets = new TIntArrayList();
int oldStartLine = context.document.getLineNumber(oldWrapper.getStartOffset());
if (oldStartLine > 0) {
int lastLineFeed = context.document.getLineStartOffset(oldStartLine) - 1;
lineFeedOffsets.add(lastLineFeed);
for (int i = lastLineFeed - 1 - myParentShift; i >= 0; i--) {
i = CharArrayUtil.shiftBackward(myParentText, i, " \t");
if (myParentText.charAt(i) == '\n') {
blankLinesBefore++;
lineFeedOffsets.add(i + myParentShift);
}
else {
break;
}
}
}
ArrangementEntryWrapper<E> parentWrapper = oldWrapper.getParent();
int desiredBlankLinesNumber = getBlankLines(context, parentWrapper, newWrapper, previous, next);
if (desiredBlankLinesNumber == blankLinesBefore && newWrapper.equals(oldWrapper)) {
return;
}
String newEntryText = myParentText.substring(newWrapper.getStartOffset() - myParentShift, newWrapper.getEndOffset() - myParentShift);
int lineFeedsDiff = desiredBlankLinesNumber - blankLinesBefore;
if (lineFeedsDiff == 0 || desiredBlankLinesNumber < 0) {
context.addMoveInfo(newWrapper.getStartOffset() - myParentShift,
newWrapper.getEndOffset() - myParentShift,
oldWrapper.getStartOffset());
context.document.replaceString(oldWrapper.getStartOffset(), oldWrapper.getEndOffset(), newEntryText);
return;
}
if (lineFeedsDiff > 0) {
// Insert necessary number of blank lines.
StringBuilder buffer = new StringBuilder(StringUtil.repeat("\n", lineFeedsDiff));
buffer.append(newEntryText);
context.document.replaceString(oldWrapper.getStartOffset(), oldWrapper.getEndOffset(), buffer);
}
else {
// Cut exceeding blank lines.
int replacementStartOffset = lineFeedOffsets.get(-lineFeedsDiff) + 1;
context.document.replaceString(replacementStartOffset, oldWrapper.getEndOffset(), newEntryText);
}
// Update wrapper ranges.
ArrangementEntryWrapper<E> parent = oldWrapper.getParent();
if (parent == null) {
return;
}
Deque<ArrangementEntryWrapper<E>> parents = new ArrayDeque<ArrangementEntryWrapper<E>>();
do {
parents.add(parent);
parent.setEndOffset(parent.getEndOffset() + lineFeedsDiff);
parent = parent.getParent();
}
while (parent != null);
while (!parents.isEmpty()) {
for (ArrangementEntryWrapper<E> wrapper = parents.removeLast().getNext(); wrapper != null; wrapper = wrapper.getNext()) {
wrapper.applyShift(lineFeedsDiff);
}
}
}
@Override
public void insert(@NotNull Context<E> context, int startOffset, @NotNull String text) {
context.document.insertString(startOffset, text);
}
@Override
public void insertSection(@NotNull Context<E> context,
@NotNull E entry,
@NotNull NewSectionInfo<E> newSectionsInfo,
@NotNull ArrangementEntryWrapper<E> arrangedWrapper,
@NotNull ArrangementEntryWrapper<E> initialWrapper,
@Nullable ArrangementEntryWrapper<E> parent) {
final int beforeOffset = arrangedWrapper.equals(initialWrapper) ? arrangedWrapper.getStartOffset() : initialWrapper.getStartOffset();
final int length = arrangedWrapper.getEndOffset() - arrangedWrapper.getStartOffset();
int afterOffset = arrangedWrapper.equals(initialWrapper) ? arrangedWrapper.getEndOffset() : beforeOffset + length;
insertSection(context, entry, newSectionsInfo, parent, beforeOffset, afterOffset);
}
@Override
protected void insertSection(@NotNull Context<E> context,
@NotNull E entry,
@NotNull NewSectionInfo<E> newSectionsInfo,
ArrangementEntryWrapper<E> parent, int beforeOffset, int afterOffset) {
final String afterComment = newSectionsInfo.getEndComment(entry);
if (afterComment != null) {
insert(context, afterOffset, "\n" + afterComment);
}
final String beforeComment = newSectionsInfo.getStartComment(entry);
if (beforeComment != null) {
insert(context, beforeOffset, beforeComment + "\n");
}
}
}
private static class RangeMarkerAwareChanger<E extends ArrangementEntry> extends Changer<E> {
@NotNull private final List<ArrangementEntryWrapper<E>> myWrappers = new ArrayList<ArrangementEntryWrapper<E>>();
@NotNull private final DocumentEx myDocument;
RangeMarkerAwareChanger(@NotNull DocumentEx document) {
myDocument = document;
}
@Override
public void prepare(@NotNull List<ArrangementEntryWrapper<E>> toArrange, @NotNull Context<E> context) {
myWrappers.clear();
myWrappers.addAll(toArrange);
for (ArrangementEntryWrapper<E> wrapper : toArrange) {
wrapper.updateBlankLines(myDocument);
}
}
@SuppressWarnings("AssignmentToForLoopParameter")
@Override
public void replace(@NotNull ArrangementEntryWrapper<E> newWrapper,
@NotNull ArrangementEntryWrapper<E> oldWrapper,
@Nullable ArrangementEntryWrapper<E> previous,
@Nullable ArrangementEntryWrapper<E> next,
@NotNull Context<E> context)
{
// Calculate blank lines before the arrangement.
int blankLinesBefore = oldWrapper.getBlankLinesBefore();
ArrangementEntryWrapper<E> parentWrapper = oldWrapper.getParent();
int desiredBlankLinesNumber = getBlankLines(context, parentWrapper, newWrapper, previous, next);
if ((desiredBlankLinesNumber < 0 || desiredBlankLinesNumber == blankLinesBefore) && newWrapper.equals(oldWrapper)) {
return;
}
int lineFeedsDiff = desiredBlankLinesNumber - blankLinesBefore;
int insertionOffset = oldWrapper.getStartOffset();
if (oldWrapper.getStartOffset() > newWrapper.getStartOffset()) {
insertionOffset -= newWrapper.getEndOffset() - newWrapper.getStartOffset();
}
if (newWrapper.getStartOffset() != oldWrapper.getStartOffset() || !newWrapper.equals(oldWrapper)) {
context.addMoveInfo(newWrapper.getStartOffset(), newWrapper.getEndOffset(), oldWrapper.getStartOffset());
myDocument.moveText(newWrapper.getStartOffset(), newWrapper.getEndOffset(), oldWrapper.getStartOffset());
for (int i = myWrappers.size() - 1; i >= 0; i--) {
ArrangementEntryWrapper<E> w = myWrappers.get(i);
if (w == newWrapper) {
continue;
}
if (w.getStartOffset() >= oldWrapper.getStartOffset() && w.getStartOffset() < newWrapper.getStartOffset()) {
w.applyShift(newWrapper.getEndOffset() - newWrapper.getStartOffset());
}
else if (oldWrapper != w && w.getStartOffset() <= oldWrapper.getStartOffset() &&
w.getStartOffset() > newWrapper.getStartOffset()) {
w.applyShift(newWrapper.getStartOffset() - newWrapper.getEndOffset());
}
}
}
if (desiredBlankLinesNumber >= 0 && lineFeedsDiff > 0) {
myDocument.insertString(insertionOffset, StringUtil.repeat("\n", lineFeedsDiff));
shiftOffsets(lineFeedsDiff, insertionOffset);
}
if (desiredBlankLinesNumber >= 0 && lineFeedsDiff < 0) {
// Cut exceeding blank lines.
int replacementStartOffset = getBlankLineOffset(-lineFeedsDiff, insertionOffset);
myDocument.deleteString(replacementStartOffset, insertionOffset);
shiftOffsets(replacementStartOffset - insertionOffset, insertionOffset);
}
if (desiredBlankLinesNumber < 0) {
return;
}
updateAllWrapperRanges(parentWrapper, lineFeedsDiff);
}
protected void updateAllWrapperRanges(@Nullable ArrangementEntryWrapper<E> parentWrapper, int lineFeedsDiff) {
// Update wrapper ranges.
if (lineFeedsDiff == 0 || parentWrapper == null) {
return;
}
Deque<ArrangementEntryWrapper<E>> parents = new ArrayDeque<ArrangementEntryWrapper<E>>();
do {
parents.add(parentWrapper);
parentWrapper.setEndOffset(parentWrapper.getEndOffset() + lineFeedsDiff);
parentWrapper = parentWrapper.getParent();
}
while (parentWrapper != null);
while (!parents.isEmpty()) {
for (ArrangementEntryWrapper<E> wrapper = parents.removeLast().getNext(); wrapper != null; wrapper = wrapper.getNext()) {
wrapper.applyShift(lineFeedsDiff);
}
}
}
@Override
public void insert(@NotNull Context<E> context, int startOffset, @NotNull String text) {
myDocument.insertString(startOffset, text);
int shift = text.length();
for (int i = myWrappers.size() - 1; i >= 0; i--) {
ArrangementEntryWrapper<E> wrapper = myWrappers.get(i);
if (wrapper.getStartOffset() >= startOffset) {
wrapper.applyShift(shift);
}
}
}
@Override
public void insertSection(@NotNull Context<E> context,
@NotNull E entry,
@NotNull NewSectionInfo<E> newSectionsInfo,
@NotNull ArrangementEntryWrapper<E> arrangedWrapper,
@NotNull ArrangementEntryWrapper<E> initialWrapper,
@Nullable ArrangementEntryWrapper<E> parent) {
final int afterOffset = arrangedWrapper.equals(initialWrapper) ? arrangedWrapper.getEndOffset() : initialWrapper.getStartOffset();
final int length = arrangedWrapper.getEndOffset() - arrangedWrapper.getStartOffset();
final int beforeOffset = arrangedWrapper.equals(initialWrapper) ? arrangedWrapper.getStartOffset() : afterOffset - length;
insertSection(context, entry, newSectionsInfo, parent, beforeOffset, afterOffset);
}
@Override
protected void insertSection(@NotNull Context<E> context,
@NotNull E entry,
@NotNull NewSectionInfo<E> newSectionsInfo,
@Nullable ArrangementEntryWrapper<E> parent,
int beforeOffset, int afterOffset) {
int diff = 0;
final String afterComment = newSectionsInfo.getEndComment(entry);
if (afterComment != null) {
insert(context, afterOffset, "\n" + afterComment);
diff += afterComment.length() + 1;
}
final String beforeComment = newSectionsInfo.getStartComment(entry);
if (beforeComment != null) {
insert(context, beforeOffset, beforeComment + "\n");
diff += beforeComment.length() + 1;
}
updateAllWrapperRanges(parent, diff);
}
/**
* @return position <code>x</code> for which <code>myDocument.getText().substring(x, startOffset)</code> contains
* <code>blankLinesNumber</code> line feeds and <code>myDocument.getText.charAt(x-1) == '\n'</code>
*/
private int getBlankLineOffset(int blankLinesNumber, int startOffset) {
int startLine = myDocument.getLineNumber(startOffset);
if (startLine <= 0) {
return 0;
}
CharSequence text = myDocument.getCharsSequence();
for (int i = myDocument.getLineStartOffset(startLine - 1) - 1; i >= 0; i = CharArrayUtil.lastIndexOf(text, "\n", i - 1)) {
if (--blankLinesNumber <= 0) {
return i + 1;
}
}
return 0;
}
private void shiftOffsets(int shift, int changeOffset) {
for (int i = myWrappers.size() - 1; i >= 0; i--) {
ArrangementEntryWrapper<E> wrapper = myWrappers.get(i);
if (wrapper.getStartOffset() < changeOffset) {
break;
}
wrapper.applyShift(shift);
}
}
}
}