blob: 0c6a3766342d6ed57a74cfa063fab1dbd0a13883 [file] [log] [blame]
/*
* Copyright 2000-2014 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.find.replaceInProject;
import com.intellij.find.*;
import com.intellij.find.actions.FindInPathAction;
import com.intellij.find.findInProject.FindInProjectManager;
import com.intellij.find.impl.FindInProjectUtil;
import com.intellij.find.impl.FindManagerImpl;
import com.intellij.ide.DataManager;
import com.intellij.notification.NotificationGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.KeyboardShortcut;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Factory;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.Segment;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.StatusBar;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.ui.content.Content;
import com.intellij.usageView.UsageInfo;
import com.intellij.usages.*;
import com.intellij.usages.impl.UsageViewImpl;
import com.intellij.usages.rules.UsageInFile;
import com.intellij.util.AdapterProcessor;
import com.intellij.util.Processor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
public class ReplaceInProjectManager {
static final NotificationGroup NOTIFICATION_GROUP = FindInPathAction.NOTIFICATION_GROUP;
private final Project myProject;
private boolean myIsFindInProgress = false;
public static ReplaceInProjectManager getInstance(Project project) {
return ServiceManager.getService(project, ReplaceInProjectManager.class);
}
public ReplaceInProjectManager(Project project) {
myProject = project;
}
public static boolean hasReadOnlyUsages(final Collection<Usage> usages) {
for (Usage usage : usages) {
if (usage.isReadOnly()) return true;
}
return false;
}
static class ReplaceContext {
private final UsageView usageView;
private final FindModel findModel;
private Set<Usage> excludedSet;
ReplaceContext(@NotNull UsageView usageView, @NotNull FindModel findModel) {
this.usageView = usageView;
this.findModel = findModel;
}
@NotNull
public FindModel getFindModel() {
return findModel;
}
@NotNull
public UsageView getUsageView() {
return usageView;
}
@NotNull
public Set<Usage> getExcludedSetCached() {
if (excludedSet == null) excludedSet = usageView.getExcludedUsages();
return excludedSet;
}
public void invalidateExcludedSetCache() {
excludedSet = null;
}
}
public void replaceInProject(@NotNull DataContext dataContext) {
final boolean isOpenInNewTabEnabled;
final boolean toOpenInNewTab;
final Content selectedContent = com.intellij.usageView.UsageViewManager.getInstance(myProject).getSelectedContent(true);
if (selectedContent != null && selectedContent.isPinned()) {
toOpenInNewTab = true;
isOpenInNewTabEnabled = false;
}
else {
toOpenInNewTab = FindSettings.getInstance().isShowResultsInSeparateView();
isOpenInNewTabEnabled = com.intellij.usageView.UsageViewManager.getInstance(myProject).getReusableContentsCount() > 0;
}
final FindManager findManager = FindManager.getInstance(myProject);
final FindModel findModel = findManager.getFindInProjectModel().clone();
findModel.setReplaceState(true);
findModel.setOpenInNewTabVisible(true);
findModel.setOpenInNewTabEnabled(isOpenInNewTabEnabled);
findModel.setOpenInNewTab(toOpenInNewTab);
FindInProjectUtil.setDirectoryName(findModel, dataContext);
Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
FindUtil.initStringToFindWithSelection(findModel, editor);
findManager.showFindDialog(findModel, new Runnable() {
@Override
public void run() {
final PsiDirectory psiDirectory = FindInProjectUtil.getPsiDirectory(findModel, myProject);
if (!findModel.isProjectScope() &&
psiDirectory == null &&
findModel.getModuleName() == null &&
findModel.getCustomScope() == null) {
return;
}
UsageViewManager manager = UsageViewManager.getInstance(myProject);
if (manager == null) return;
findManager.getFindInProjectModel().copyFrom(findModel);
final FindModel findModelCopy = findModel.clone();
final UsageViewPresentation presentation = FindInProjectUtil.setupViewPresentation(findModel.isOpenInNewTab(), findModelCopy);
final FindUsagesProcessPresentation processPresentation = FindInProjectUtil.setupProcessPresentation(myProject, true, presentation);
UsageSearcherFactory factory = new UsageSearcherFactory(findModelCopy, psiDirectory, processPresentation);
searchAndShowUsages(manager, factory, findModelCopy, presentation, processPresentation, findManager);
}
});
}
public void searchAndShowUsages(@NotNull UsageViewManager manager,
@NotNull Factory<UsageSearcher> usageSearcherFactory,
@NotNull FindModel findModelCopy,
@NotNull FindManager findManager) {
final UsageViewPresentation presentation = FindInProjectUtil.setupViewPresentation(true, findModelCopy);
final FindUsagesProcessPresentation processPresentation = FindInProjectUtil.setupProcessPresentation(myProject, true, presentation);
searchAndShowUsages(manager, usageSearcherFactory, findModelCopy, presentation, processPresentation, findManager);
}
private static class ReplaceInProjectTarget extends FindInProjectUtil.StringUsageTarget {
public ReplaceInProjectTarget(@NotNull Project project, @NotNull FindModel findModel) {
super(project, findModel);
}
@NotNull
@Override
public String getLongDescriptiveName() {
UsageViewPresentation presentation = FindInProjectUtil.setupViewPresentation(false, myFindModel);
return "Replace "+ StringUtil.decapitalize(presentation.getToolwindowTitle())+" with '"+ myFindModel.getStringToReplace()+"'";
}
@Override
public KeyboardShortcut getShortcut() {
return ActionManager.getInstance().getKeyboardShortcut("ReplaceInPath");
}
@Override
public void showSettings() {
Content selectedContent = com.intellij.usageView.UsageViewManager.getInstance(myProject).getSelectedContent(true);
JComponent component = selectedContent == null ? null : selectedContent.getComponent();
ReplaceInProjectManager findInProjectManager = getInstance(myProject);
findInProjectManager.replaceInProject(DataManager.getInstance().getDataContext(component));
}
}
public void searchAndShowUsages(@NotNull UsageViewManager manager,
@NotNull Factory<UsageSearcher> usageSearcherFactory,
@NotNull final FindModel findModelCopy,
@NotNull UsageViewPresentation presentation,
@NotNull FindUsagesProcessPresentation processPresentation,
final FindManager findManager) {
presentation.setMergeDupLinesAvailable(false);
final ReplaceContext[] context = new ReplaceContext[1];
final ReplaceInProjectTarget target = new ReplaceInProjectTarget(myProject, findModelCopy);
((FindManagerImpl)FindManager.getInstance(myProject)).getFindUsagesManager().addToHistory(target);
manager.searchAndShowUsages(new UsageTarget[]{target},
usageSearcherFactory, processPresentation, presentation, new UsageViewManager.UsageViewStateListener() {
@Override
public void usageViewCreated(@NotNull UsageView usageView) {
context[0] = new ReplaceContext(usageView, findModelCopy);
addReplaceActions(context[0]);
}
@Override
public void findingUsagesFinished(final UsageView usageView) {
if (context[0] != null && findManager.getFindInProjectModel().isPromptOnReplace()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
replaceWithPrompt(context[0]);
context[0].invalidateExcludedSetCache();
}
});
}
}
});
}
private void replaceWithPrompt(final ReplaceContext replaceContext) {
final List<Usage> _usages = replaceContext.getUsageView().getSortedUsages();
if (hasReadOnlyUsages(_usages)) {
WindowManager.getInstance().getStatusBar(myProject)
.setInfo(FindBundle.message("find.replace.occurrences.found.in.read.only.files.status"));
return;
}
final Usage[] usages = _usages.toArray(new Usage[_usages.size()]);
//usageView.expandAll();
for (int i = 0; i < usages.length; ++i) {
final Usage usage = usages[i];
final UsageInfo usageInfo = ((UsageInfo2UsageAdapter)usage).getUsageInfo();
final PsiElement elt = usageInfo.getElement();
if (elt == null) continue;
final PsiFile psiFile = elt.getContainingFile();
if (!psiFile.isWritable()) continue;
Runnable selectOnEditorRunnable = new Runnable() {
@Override
public void run() {
final VirtualFile virtualFile = psiFile.getVirtualFile();
if (virtualFile != null && ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
@Override
public Boolean compute() {
return virtualFile.isValid() ? Boolean.TRUE : Boolean.FALSE;
}
}).booleanValue()) {
if (usage.isValid()) {
usage.highlightInEditor();
replaceContext.getUsageView().selectUsages(new Usage[]{usage});
}
}
}
};
CommandProcessor.getInstance()
.executeCommand(myProject, selectOnEditorRunnable, FindBundle.message("find.replace.select.on.editor.command"), null);
String title = FindBundle.message("find.replace.found.usage.title", i + 1, usages.length);
int result;
try {
replaceUsage(usage, replaceContext.getFindModel(), replaceContext.getExcludedSetCached(), true);
result = FindManager.getInstance(myProject).showPromptDialog(replaceContext.getFindModel(), title);
}
catch (FindManager.MalformedReplacementStringException e) {
markAsMalformedReplacement(replaceContext, usage);
result = FindManager.getInstance(myProject).showMalformedReplacementPrompt(replaceContext.getFindModel(), title, e);
}
if (result == FindManager.PromptResult.CANCEL) {
return;
}
if (result == FindManager.PromptResult.SKIP) {
continue;
}
final int currentNumber = i;
if (result == FindManager.PromptResult.OK) {
final Ref<Boolean> success = Ref.create();
Runnable runnable = new Runnable() {
@Override
public void run() {
success.set(replaceUsageAndRemoveFromView(usage, replaceContext));
}
};
CommandProcessor.getInstance().executeCommand(myProject, runnable, FindBundle.message("find.replace.command"), null);
if (closeUsageViewIfEmpty(replaceContext.getUsageView(), success.get())) {
return;
}
}
if (result == FindManager.PromptResult.ALL_IN_THIS_FILE) {
final int[] nextNumber = new int[1];
Runnable runnable = new Runnable() {
@Override
public void run() {
int j = currentNumber;
boolean success = true;
for (; j < usages.length; j++) {
final Usage usage = usages[j];
final UsageInfo usageInfo = ((UsageInfo2UsageAdapter)usage).getUsageInfo();
final PsiElement elt = usageInfo.getElement();
if (elt == null) continue;
PsiFile otherPsiFile = elt.getContainingFile();
if (!otherPsiFile.equals(psiFile)) {
break;
}
if (!replaceUsageAndRemoveFromView(usage, replaceContext)) {
success = false;
}
}
closeUsageViewIfEmpty(replaceContext.getUsageView(), success);
nextNumber[0] = j;
}
};
CommandProcessor.getInstance().executeCommand(myProject, runnable, FindBundle.message("find.replace.command"), null);
//noinspection AssignmentToForLoopParameter
i = nextNumber[0] - 1;
}
if (result == FindManager.PromptResult.ALL_FILES) {
CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
@Override
public void run() {
final boolean success = replaceUsages(replaceContext, _usages);
closeUsageViewIfEmpty(replaceContext.getUsageView(), success);
}
}, FindBundle.message("find.replace.command"), null);
break;
}
}
}
private boolean replaceUsageAndRemoveFromView(Usage usage, ReplaceContext replaceContext) {
try {
if (replaceUsage(usage, replaceContext.getFindModel(), replaceContext.getExcludedSetCached(), false)) {
replaceContext.getUsageView().removeUsage(usage);
}
}
catch (FindManager.MalformedReplacementStringException e) {
markAsMalformedReplacement(replaceContext, usage);
return false;
}
return true;
}
private void addReplaceActions(final ReplaceContext replaceContext) {
final Runnable replaceRunnable = new Runnable() {
@Override
public void run() {
replaceUsagesUnderCommand(replaceContext, replaceContext.getUsageView().getUsages());
}
};
replaceContext.getUsageView().addButtonToLowerPane(replaceRunnable, FindBundle.message("find.replace.all.action"));
final Runnable replaceSelectedRunnable = new Runnable() {
@Override
public void run() {
replaceUsagesUnderCommand(replaceContext, replaceContext.getUsageView().getSelectedUsages());
}
};
replaceContext.getUsageView().addButtonToLowerPane(replaceSelectedRunnable, FindBundle.message("find.replace.selected.action"));
}
private boolean replaceUsages(@NotNull ReplaceContext replaceContext, @NotNull Collection<Usage> usages) {
if (!ensureUsagesWritable(replaceContext, usages)) {
return true;
}
int replacedCount = 0;
boolean success = true;
for (final Usage usage : usages) {
try {
if (replaceUsage(usage, replaceContext.getFindModel(), replaceContext.getExcludedSetCached(), false)) {
replacedCount++;
}
}
catch (FindManager.MalformedReplacementStringException e) {
markAsMalformedReplacement(replaceContext, usage);
success = false;
}
}
replaceContext.getUsageView().removeUsagesBulk(usages);
reportNumberReplacedOccurrences(myProject, replacedCount);
return success;
}
private static void markAsMalformedReplacement(ReplaceContext replaceContext, Usage usage) {
replaceContext.getUsageView().excludeUsages(new Usage[]{usage});
}
public static void reportNumberReplacedOccurrences(Project project, int occurrences) {
if (occurrences != 0) {
final StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);
if (statusBar != null) {
statusBar.setInfo(FindBundle.message("0.occurrences.replaced", occurrences));
}
}
}
public boolean replaceUsage(@NotNull final Usage usage,
@NotNull final FindModel findModel,
@NotNull final Set<Usage> excludedSet,
final boolean justCheck)
throws FindManager.MalformedReplacementStringException {
final Ref<FindManager.MalformedReplacementStringException> exceptionResult = Ref.create();
final boolean result = ApplicationManager.getApplication().runWriteAction(new Computable<Boolean>() {
@Override
public Boolean compute() {
if (excludedSet.contains(usage)) {
return false;
}
final Document document = ((UsageInfo2UsageAdapter)usage).getDocument();
if (!document.isWritable()) return false;
boolean result = ((UsageInfo2UsageAdapter)usage).processRangeMarkers(new Processor<Segment>() {
@Override
public boolean process(Segment segment) {
final int textOffset = segment.getStartOffset();
final int textEndOffset = segment.getEndOffset();
final Ref<String> stringToReplace = Ref.create();
try {
if (!getStringToReplace(textOffset, textEndOffset, document, findModel, stringToReplace)) return true;
if (!stringToReplace.isNull() && !justCheck) {
document.replaceString(textOffset, textEndOffset, stringToReplace.get());
}
}
catch (FindManager.MalformedReplacementStringException e) {
exceptionResult.set(e);
return false;
}
return true;
}
});
return result;
}
});
if (!exceptionResult.isNull()) {
throw exceptionResult.get();
}
return result;
}
private boolean getStringToReplace(int textOffset,
int textEndOffset,
Document document, FindModel findModel, Ref<String> stringToReplace)
throws FindManager.MalformedReplacementStringException {
if (textOffset < 0 || textOffset >= document.getTextLength()) {
return false;
}
if (textEndOffset < 0 || textOffset > document.getTextLength()) {
return false;
}
FindManager findManager = FindManager.getInstance(myProject);
final CharSequence foundString = document.getCharsSequence().subSequence(textOffset, textEndOffset);
PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
FindResult findResult = findManager.findString(document.getCharsSequence(), textOffset, findModel, file != null ? file.getVirtualFile() : null);
if (!findResult.isStringFound()) {
return false;
}
stringToReplace.set(
FindManager.getInstance(myProject).getStringToReplace(foundString.toString(), findModel, textOffset, document.getText()));
return true;
}
private void replaceUsagesUnderCommand(@NotNull final ReplaceContext replaceContext, @Nullable final Set<Usage> usagesSet) {
if (usagesSet == null) {
return;
}
final List<Usage> usages = new ArrayList<Usage>(usagesSet);
Collections.sort(usages, UsageViewImpl.USAGE_COMPARATOR);
if (!ensureUsagesWritable(replaceContext, usages)) return;
CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
@Override
public void run() {
final boolean success = replaceUsages(replaceContext, usages);
final UsageView usageView = replaceContext.getUsageView();
if (closeUsageViewIfEmpty(usageView, success)) return;
usageView.getComponent().requestFocus();
}
}, FindBundle.message("find.replace.command"), null);
replaceContext.invalidateExcludedSetCache();
}
private boolean ensureUsagesWritable(ReplaceContext replaceContext, Collection<Usage> selectedUsages) {
Set<VirtualFile> readOnlyFiles = null;
for (final Usage usage : selectedUsages) {
final VirtualFile file = ((UsageInFile)usage).getFile();
if (file != null && !file.isWritable()) {
if (readOnlyFiles == null) readOnlyFiles = new HashSet<VirtualFile>();
readOnlyFiles.add(file);
}
}
if (readOnlyFiles != null) {
ReadonlyStatusHandler.getInstance(myProject).ensureFilesWritable(VfsUtilCore.toVirtualFileArray(readOnlyFiles));
}
if (hasReadOnlyUsages(selectedUsages)) {
int result = Messages.showOkCancelDialog(replaceContext.getUsageView().getComponent(),
FindBundle.message("find.replace.occurrences.in.read.only.files.prompt"),
FindBundle.message("find.replace.occurrences.in.read.only.files.title"),
Messages.getWarningIcon());
if (result != Messages.OK) {
return false;
}
}
return true;
}
private boolean closeUsageViewIfEmpty(UsageView usageView, boolean success) {
if (usageView.getUsages().isEmpty()) {
usageView.close();
return true;
}
if (!success) {
NOTIFICATION_GROUP.createNotification("One or more malformed replacement strings", MessageType.ERROR).notify(myProject);
}
return false;
}
public boolean isWorkInProgress() {
return myIsFindInProgress;
}
public boolean isEnabled() {
return !myIsFindInProgress && !FindInProjectManager.getInstance(myProject).isWorkInProgress();
}
private class UsageSearcherFactory implements Factory<UsageSearcher> {
private final FindModel myFindModelCopy;
private final PsiDirectory myPsiDirectory;
private final FindUsagesProcessPresentation myProcessPresentation;
private UsageSearcherFactory(@NotNull FindModel findModelCopy,
PsiDirectory psiDirectory,
@NotNull FindUsagesProcessPresentation processPresentation) {
myFindModelCopy = findModelCopy;
myPsiDirectory = psiDirectory;
myProcessPresentation = processPresentation;
}
@Override
public UsageSearcher create() {
return new UsageSearcher() {
@Override
public void generate(@NotNull final Processor<Usage> processor) {
try {
myIsFindInProgress = true;
FindInProjectUtil.findUsages(myFindModelCopy, myPsiDirectory, myProject,
new AdapterProcessor<UsageInfo, Usage>(processor, UsageInfo2UsageAdapter.CONVERTER),
myProcessPresentation);
}
finally {
myIsFindInProgress = false;
}
}
};
}
}
}