SSR: fix replace tool window issues

- ask for all files at once when replacing selected read-only files
- don't cache excluded usages, so they can be changed after a replace
- allow preview on multiple selected files
- select a sensible next usage after replace
- more efficient when replacing many usages by using removeUsagesBulk()
This commit is contained in:
Bas Leijdekkers
2017-06-29 20:59:49 +02:00
parent e60108fc06
commit a775610324
@@ -19,6 +19,7 @@ import com.intellij.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.structuralsearch.MatchResult;
import com.intellij.structuralsearch.SSRBundle;
import com.intellij.structuralsearch.plugin.replace.ReplacementInfo;
@@ -26,21 +27,18 @@ import com.intellij.structuralsearch.plugin.replace.impl.Replacer;
import com.intellij.structuralsearch.plugin.ui.Configuration;
import com.intellij.structuralsearch.plugin.ui.SearchContext;
import com.intellij.structuralsearch.plugin.ui.UsageViewContext;
import com.intellij.usageView.UsageInfo;
import com.intellij.usages.Usage;
import com.intellij.usages.UsageInfo2UsageAdapter;
import com.intellij.usages.UsageView;
import com.intellij.util.ObjectUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
class ReplaceUsageViewContext extends UsageViewContext {
private final HashMap<Usage,ReplacementInfo> usage2ReplacementInfo = new HashMap<>();
private final Replacer replacer = new Replacer(mySearchContext.getProject(), ((ReplaceConfiguration)myConfiguration).getReplaceOptions());
private UsageView myUsageView;
private Set<Usage> myExcludedSet;
ReplaceUsageViewContext(SearchContext context, Configuration configuration, Runnable searchStarter) {
super(configuration, context, searchStarter);
@@ -51,128 +49,89 @@ class ReplaceUsageViewContext extends UsageViewContext {
myUsageView = usageView;
}
public Replacer getReplacer() {
return replacer;
}
public void addReplaceUsage(Usage usage, MatchResult result) {
usage2ReplacementInfo.put(usage, getReplacer().buildReplacement(result));
usage2ReplacementInfo.put(usage, replacer.buildReplacement(result));
}
private boolean isValid(UsageInfo2UsageAdapter info) {
final UsageInfo usageInfo = info.getUsageInfo();
return !isExcluded(info) && usageInfo.getElement() != null && usageInfo.getElement().isValid();
private static boolean isValid(UsageInfo2UsageAdapter info) {
final PsiElement element = info.getUsageInfo().getElement();
return element != null && element.isValid();
}
@Override
protected void configureActions() {
final Runnable replaceRunnable = () -> {
LocalHistoryAction labelAction = LocalHistory.getInstance().startAction(SSRBundle.message("structural.replace.title"));
doReplace();
myUsageView.close();
labelAction.finish();
};
//noinspection HardCodedStringLiteral
myUsageView.addPerformOperationAction(replaceRunnable, "Replace All", null, SSRBundle.message("do.replace.all.button"));
final Runnable replaceSelected = () -> {
final Set<Usage> infos = myUsageView.getSelectedUsages();
if (infos == null || infos.isEmpty()) return;
LocalHistoryAction labelAction = LocalHistory.getInstance().startAction(SSRBundle.message("structural.replace.title"));
for (final Usage info : infos) {
final UsageInfo2UsageAdapter usage = (UsageInfo2UsageAdapter)info;
if (isValid(usage)) {
replaceOne(usage, false);
}
}
labelAction.finish();
if (myUsageView.getUsagesCount() > 0) {
for (Usage usage : myUsageView.getSortedUsages()) {
if (!isExcluded(usage)) {
myUsageView.selectUsages(new Usage[]{usage});
return;
}
}
}
};
myUsageView.addButtonToLowerPane(replaceSelected, SSRBundle.message("replace.selected.button"));
myUsageView.addButtonToLowerPane(() -> replace(myUsageView.getSortedUsages()), SSRBundle.message("do.replace.all.button"));
myUsageView.addButtonToLowerPane(() -> replace(myUsageView.getSelectedUsages()), SSRBundle.message("replace.selected.button"));
final Runnable previewReplacement = () -> {
Set<Usage> selection = myUsageView.getSelectedUsages();
if (selection != null && !selection.isEmpty()) {
UsageInfo2UsageAdapter usage = (UsageInfo2UsageAdapter)selection.iterator().next();
if (isValid(usage)) {
replaceOne(usage, true);
final Set<Usage> selection = myUsageView.getSelectedUsages();
if (selection == null || selection.isEmpty()) {
return;
}
for (Usage usage : selection) {
final UsageInfo2UsageAdapter info = (UsageInfo2UsageAdapter)usage;
if (!isValid(info) || myUsageView.getExcludedUsages().contains(usage)) {
continue;
}
final ReplacementInfo replacementInfo = usage2ReplacementInfo.get(usage);
final ReplacementPreviewDialog previewDialog =
new ReplacementPreviewDialog(mySearchContext.getProject(), info.getUsageInfo(), replacementInfo.getReplacement());
if (!previewDialog.showAndGet()) {
return;
}
replace(Collections.singleton(info));
}
};
myUsageView.addButtonToLowerPane(previewReplacement, SSRBundle.message("preview.replacement.button"));
super.configureActions();
}
private static void ensureFileWritable(final UsageInfo2UsageAdapter usage) {
final VirtualFile file = usage.getFile();
private void replace(Collection<Usage> usages) {
if (usages == null) {
return;
}
final Set<Usage> excluded = myUsageView.getExcludedUsages();
usages = usages.stream().filter(u -> !excluded.contains(u)).filter(u -> isValid((UsageInfo2UsageAdapter)u)).collect(Collectors.toList());
if (file != null && !file.isWritable()) {
ReadonlyStatusHandler.getInstance(usage.getElement().getProject()).ensureFilesWritable(file);
final List<VirtualFile> files = usages.stream().map(i -> ((UsageInfo2UsageAdapter)i).getFile()).collect(Collectors.toList());
if (ReadonlyStatusHandler.getInstance(mySearchContext.getProject()).ensureFilesWritable(files).hasReadonlyFiles()) {
return;
}
removeUsagesAndSelectNext(usages, excluded);
final LocalHistoryAction action = LocalHistory.getInstance().startAction(SSRBundle.message("structural.replace.title"));
try {
replacer.replaceAll(usages.stream().map(usage2ReplacementInfo::get).collect(Collectors.toList()));
} finally {
action.finish();
}
}
private void replaceOne(UsageInfo2UsageAdapter info, boolean doConfirm) {
ReplacementInfo replacementInfo = usage2ReplacementInfo.get(info);
boolean approved;
if (doConfirm) {
ReplacementPreviewDialog wrapper =
new ReplacementPreviewDialog(mySearchContext.getProject(), info.getUsageInfo(), replacementInfo.getReplacement());
approved = wrapper.showAndGet();
private void removeUsagesAndSelectNext(Collection<Usage> usages, Collection<Usage> excluded) {
final List<Usage> sortedUsages = myUsageView.getSortedUsages();
if (sortedUsages.size() == usages.size()) {
myUsageView.close();
}
else {
approved = true;
}
if (approved) {
ensureFileWritable(info);
myUsageView.removeUsage(info);
getReplacer().replace(replacementInfo);
if (myUsageView.getUsagesCount() == 0) {
myUsageView.close();
Usage firstValid = null;
Usage select = null;
for (Usage usage : sortedUsages) {
if (usages.contains(usage)) {
select = null;
continue;
}
if (excluded.contains(usage) || !isValid((UsageInfo2UsageAdapter)usage)) {
continue;
}
if (select == null) {
select = usage;
}
if (firstValid == null) {
firstValid = usage;
}
}
myUsageView.removeUsagesBulk(usages);
myUsageView.selectUsages(new Usage[]{
ObjectUtils.coalesce(select, firstValid, myUsageView.getSortedUsages().get(0))
});
}
}
private void doReplace() {
List<Usage> infos = myUsageView.getSortedUsages();
List<ReplacementInfo> results = new ArrayList<>(infos.size());
for (final Usage info : infos) {
UsageInfo2UsageAdapter usage = (UsageInfo2UsageAdapter)info;
if (isValid(usage)) {
results.add(usage2ReplacementInfo.get(usage));
}
}
getReplacer().replaceAll(results);
}
private boolean isExcluded(Usage usage) {
if (myExcludedSet == null) myExcludedSet = myUsageView.getExcludedUsages();
return myExcludedSet.contains(usage);
}
}