| package org.jetbrains.android.refactoring; |
| |
| import com.android.resources.ResourceType; |
| import com.intellij.ide.highlighter.XmlFileType; |
| import com.intellij.lang.injection.InjectedLanguageManager; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.undo.UndoUtil; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleManager; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.project.ProjectUtil; |
| import com.intellij.openapi.roots.ModuleRootManager; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.PsiManager; |
| import com.intellij.psi.XmlRecursiveElementVisitor; |
| import com.intellij.psi.impl.cache.CacheManager; |
| import com.intellij.psi.search.GlobalSearchScope; |
| import com.intellij.psi.search.UsageSearchContext; |
| import com.intellij.psi.xml.XmlAttribute; |
| import com.intellij.psi.xml.XmlAttributeValue; |
| import com.intellij.psi.xml.XmlFile; |
| import com.intellij.psi.xml.XmlTag; |
| import com.intellij.refactoring.BaseRefactoringProcessor; |
| import com.intellij.refactoring.ui.UsageViewDescriptorAdapter; |
| import com.intellij.usageView.UsageInfo; |
| import com.intellij.usageView.UsageViewBundle; |
| import com.intellij.usageView.UsageViewDescriptor; |
| import com.intellij.util.containers.HashMap; |
| import com.intellij.util.containers.HashSet; |
| import com.intellij.util.xml.DomElement; |
| import com.intellij.util.xml.DomManager; |
| import org.jetbrains.android.dom.AndroidDomUtil; |
| import org.jetbrains.android.dom.converters.AndroidResourceReferenceBase; |
| import org.jetbrains.android.dom.layout.LayoutDomFileDescription; |
| import org.jetbrains.android.dom.layout.LayoutViewElement; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.resourceManagers.ValueResourceInfoImpl; |
| import org.jetbrains.android.uipreview.AndroidLayoutPreviewToolWindowManager; |
| import org.jetbrains.android.util.AndroidBundle; |
| import org.jetbrains.android.util.AndroidResourceUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| |
| /** |
| * @author Eugene.Kudelevsky |
| */ |
| public class AndroidFindStyleApplicationsProcessor extends BaseRefactoringProcessor { |
| private final Module myModule; |
| private final Map<AndroidAttributeInfo, String> myAttrMap; |
| private final String myStyleName; |
| private final XmlTag myStyleTag; |
| private final XmlAttributeValue myStyleNameAttrValue; |
| private final PsiElement myParentStyleNameAttrValue; |
| private final PsiFile myContext; |
| private boolean mySearchOnlyInCurrentModule; |
| private VirtualFile myFileToScan; |
| |
| protected AndroidFindStyleApplicationsProcessor(@NotNull Module module, |
| @NotNull Map<AndroidAttributeInfo, String> attrMap, |
| @NotNull String styleName, |
| @NotNull XmlTag styleTag, |
| @NotNull XmlAttributeValue styleNameAttrValue, |
| @Nullable PsiElement parentStyleNameAttrValue, |
| @Nullable PsiFile context) { |
| super(module.getProject()); |
| myModule = module; |
| myAttrMap = attrMap; |
| myStyleName = styleName; |
| myStyleTag = styleTag; |
| myParentStyleNameAttrValue = parentStyleNameAttrValue; |
| myStyleNameAttrValue = styleNameAttrValue; |
| myContext = context; |
| } |
| |
| @NotNull |
| @Override |
| protected UsageViewDescriptor createUsageViewDescriptor(UsageInfo[] usages) { |
| return new UsageViewDescriptorAdapter() { |
| @NotNull |
| @Override |
| public PsiElement[] getElements() { |
| return new PsiElement[]{myStyleTag}; |
| } |
| |
| @Override |
| public String getProcessedElementsHeader() { |
| return "Style to use"; |
| } |
| |
| @Override |
| public String getCodeReferencesText(int usagesCount, int filesCount) { |
| return "Tags the reference to the style will be added to " + |
| UsageViewBundle.getOccurencesString(usagesCount, filesCount); |
| } |
| }; |
| } |
| |
| @NotNull |
| @Override |
| protected UsageInfo[] findUsages() { |
| final List<UsageInfo> usages = findAllStyleApplications(); |
| return usages.toArray(new UsageInfo[usages.size()]); |
| } |
| |
| @Override |
| protected boolean preprocessUsages(Ref<UsageInfo[]> refUsages) { |
| super.preprocessUsages(refUsages); |
| |
| if (refUsages.get().length == 0) { |
| Messages.showInfoMessage(myProject, "IDEA has not found any possible applications of style '" + myStyleName + "'", |
| AndroidBundle.message("android.find.style.applications.title")); |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| protected void performRefactoring(UsageInfo[] usages) { |
| final Set<Pair<String, String>> attrsInStyle = new HashSet<Pair<String, String>>(); |
| |
| for (AndroidAttributeInfo info : myAttrMap.keySet()) { |
| attrsInStyle.add(Pair.create(info.getNamespace(), info.getName())); |
| } |
| |
| for (UsageInfo usage : usages) { |
| final PsiElement element = usage.getElement(); |
| final DomElement domElement = element instanceof XmlTag |
| ? DomManager.getDomManager(myProject).getDomElement((XmlTag)element) |
| : null; |
| if (domElement instanceof LayoutViewElement) { |
| final List<XmlAttribute> attributesToDelete = new ArrayList<XmlAttribute>(); |
| |
| for (XmlAttribute attribute : ((XmlTag)element).getAttributes()) { |
| if (attrsInStyle.contains(Pair.create(attribute.getNamespace(), |
| attribute.getLocalName()))) { |
| attributesToDelete.add(attribute); |
| } |
| } |
| |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| for (XmlAttribute attribute : attributesToDelete) { |
| attribute.delete(); |
| } |
| ((LayoutViewElement)domElement).getStyle().setStringValue("@style/" + myStyleName); |
| } |
| }); |
| } |
| } |
| final PsiFile file = myStyleTag.getContainingFile(); |
| |
| if (file != null) { |
| UndoUtil.markPsiFileForUndo(file); |
| } |
| |
| if (myContext != null) { |
| UndoUtil.markPsiFileForUndo(myContext); |
| AndroidLayoutPreviewToolWindowManager.renderIfApplicable(myContext.getProject()); |
| } |
| } |
| |
| @Override |
| protected String getCommandName() { |
| return "Use Style '" + myStyleName + "' Where Possible"; |
| } |
| |
| @NotNull |
| static List<Module> getAllModulesToScan(@NotNull Module module) { |
| final List<Module> result = new ArrayList<Module>(); |
| |
| for (Module m : ModuleManager.getInstance(module.getProject()).getModules()) { |
| if (m.equals(module) || ModuleRootManager.getInstance(m).isDependsOn(module)) { |
| result.add(module); |
| } |
| } |
| return result; |
| } |
| |
| public Collection<PsiFile> collectFilesToProcess() { |
| final Project project = myModule.getProject(); |
| final List<VirtualFile> resDirs = new ArrayList<VirtualFile>(); |
| |
| if (mySearchOnlyInCurrentModule) { |
| collectResDir(myModule, myStyleNameAttrValue, myStyleName, resDirs); |
| } |
| else { |
| for (Module m : getAllModulesToScan(myModule)) { |
| collectResDir(m, myStyleNameAttrValue, myStyleName, resDirs); |
| } |
| } |
| final List<VirtualFile> subdirs = AndroidResourceUtil.getResourceSubdirs( |
| ResourceType.LAYOUT.getName(), resDirs.toArray(new VirtualFile[resDirs.size()])); |
| |
| List<VirtualFile> filesToProcess = new ArrayList<VirtualFile>(); |
| |
| for (VirtualFile subdir : subdirs) { |
| for (VirtualFile child : subdir.getChildren()) { |
| if (child.getFileType() == XmlFileType.INSTANCE && |
| (myFileToScan == null || myFileToScan.equals(child))) { |
| filesToProcess.add(child); |
| } |
| } |
| } |
| |
| if (filesToProcess.size() == 0) { |
| return Collections.emptyList(); |
| } |
| final Set<PsiFile> psiFilesToProcess = new HashSet<PsiFile>(); |
| |
| for (VirtualFile file : filesToProcess) { |
| final PsiFile psiFile = PsiManager.getInstance(project).findFile(file); |
| |
| if (psiFile != null) { |
| psiFilesToProcess.add(psiFile); |
| } |
| } |
| final CacheManager cacheManager = CacheManager.SERVICE.getInstance(project); |
| final GlobalSearchScope projectScope = GlobalSearchScope.projectScope(project); |
| |
| for (Map.Entry<AndroidAttributeInfo, String> entry : myAttrMap.entrySet()) { |
| filterFilesToScan(cacheManager, entry.getKey().getName(), psiFilesToProcess, projectScope); |
| filterFilesToScan(cacheManager, entry.getValue(), psiFilesToProcess, projectScope); |
| } |
| |
| return psiFilesToProcess; |
| } |
| |
| @NotNull |
| private List<UsageInfo> findAllStyleApplications() { |
| Collection<PsiFile> psiFilesToProcess = collectFilesToProcess(); |
| if (psiFilesToProcess.size() == 0) { |
| return Collections.emptyList(); |
| } |
| final int n = psiFilesToProcess.size(); |
| int i = 0; |
| |
| final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); |
| if (indicator != null) { |
| indicator.setText("Searching for style applications"); |
| } |
| final List<UsageInfo> usages = new ArrayList<UsageInfo>(); |
| |
| for (PsiFile psiFile : psiFilesToProcess) { |
| ProgressManager.checkCanceled(); |
| |
| final VirtualFile vFile = psiFile.getVirtualFile(); |
| if (vFile == null) { |
| continue; |
| } |
| |
| if (indicator != null) { |
| indicator.setFraction((double)i / n); |
| indicator.setText2(ProjectUtil.calcRelativeToProjectPath(vFile, myProject)); |
| } |
| findAllStyleApplications(vFile, usages); |
| } |
| return usages; |
| } |
| |
| private static void collectResDir(Module module, XmlAttributeValue styleNameAttrValue, String styleName, List<VirtualFile> resDirs) { |
| final AndroidFacet f = AndroidFacet.getInstance(module); |
| if (f == null) { |
| return; |
| } |
| final List<ValueResourceInfoImpl> resolvedStyles = f.getLocalResourceManager().findValueResourceInfos( |
| ResourceType.STYLE.getName(), styleName, true, false); |
| |
| if (resolvedStyles.size() == 1) { |
| final XmlAttributeValue resolvedStyleNameElement = resolvedStyles.get(0).computeXmlElement(); |
| |
| if (resolvedStyleNameElement != null && |
| resolvedStyleNameElement.equals(styleNameAttrValue)) { |
| resDirs.addAll(f.getAllResourceDirectories()); |
| } |
| } |
| } |
| |
| private static void filterFilesToScan(CacheManager cacheManager, |
| String s, |
| Set<PsiFile> result, |
| GlobalSearchScope scope) { |
| for (String word : StringUtil.getWordsInStringLongestFirst(s)) { |
| final PsiFile[] files = cacheManager.getFilesWithWord(word, UsageSearchContext.ANY, scope, true); |
| result.retainAll(Arrays.asList(files)); |
| } |
| } |
| |
| private void findAllStyleApplications(final VirtualFile layoutVFile, final List<UsageInfo> usages) { |
| ApplicationManager.getApplication().runReadAction(new Runnable() { |
| @Override |
| public void run() { |
| final PsiFile layoutFile = PsiManager.getInstance(myProject).findFile(layoutVFile); |
| |
| if (!(layoutFile instanceof XmlFile)) { |
| return; |
| } |
| if (!(DomManager.getDomManager(myProject).getDomFileDescription( |
| (XmlFile)layoutFile) instanceof LayoutDomFileDescription)) { |
| return; |
| } |
| collectPossibleStyleApplications(layoutFile, usages); |
| PsiManager.getInstance(myProject).dropResolveCaches(); |
| InjectedLanguageManager.getInstance(myProject).dropFileCaches(layoutFile); |
| } |
| }); |
| } |
| |
| public void collectPossibleStyleApplications(PsiFile layoutFile, final List<UsageInfo> usages) { |
| layoutFile.accept(new XmlRecursiveElementVisitor() { |
| @Override |
| public void visitXmlTag(XmlTag tag) { |
| super.visitXmlTag(tag); |
| |
| if (isPossibleApplicationOfStyle(tag)) { |
| usages.add(new UsageInfo(tag)); |
| } |
| } |
| }); |
| } |
| |
| @Nullable |
| private static PsiElement getStyleNameAttrValueForTag(@NotNull LayoutViewElement element) { |
| final AndroidResourceReferenceBase styleRef = AndroidDomUtil. |
| getAndroidResourceReference(element.getStyle(), false); |
| |
| if (styleRef != null) { |
| final PsiElement[] styleElements = styleRef.computeTargetElements(); |
| |
| if (styleElements.length == 1) { |
| return styleElements[0]; |
| } |
| } |
| return null; |
| } |
| |
| private boolean isPossibleApplicationOfStyle(XmlTag candidate) { |
| final DomElement domCandidate = DomManager.getDomManager(myProject).getDomElement(candidate); |
| |
| if (!(domCandidate instanceof LayoutViewElement)) { |
| return false; |
| } |
| final LayoutViewElement candidateView = (LayoutViewElement)domCandidate; |
| final Map<Pair<String, String>, String> attrsInCandidateMap = new HashMap<Pair<String, String>, String>(); |
| final List<XmlAttribute> attrsInCandidate = AndroidExtractStyleAction.getExtractableAttributes(candidate); |
| |
| if (attrsInCandidate.size() < myAttrMap.size()) { |
| return false; |
| } |
| |
| for (XmlAttribute attribute : attrsInCandidate) { |
| final String attrValue = attribute.getValue(); |
| |
| if (attrValue != null) { |
| attrsInCandidateMap.put(Pair.create(attribute.getNamespace(), attribute.getLocalName()), attrValue); |
| } |
| } |
| |
| for (Map.Entry<AndroidAttributeInfo, String> entry : myAttrMap.entrySet()) { |
| final String ns = entry.getKey().getNamespace(); |
| final String name = entry.getKey().getName(); |
| final String value = entry.getValue(); |
| final String valueInCandidate = attrsInCandidateMap.get(Pair.create(ns, name)); |
| |
| if (valueInCandidate == null || !valueInCandidate.equals(value)) { |
| return false; |
| } |
| } |
| |
| if (candidateView.getStyle().getStringValue() != null) { |
| if (myParentStyleNameAttrValue == null) { |
| return false; |
| } |
| final PsiElement styleNameAttrValueForTag = getStyleNameAttrValueForTag(candidateView); |
| |
| if (styleNameAttrValueForTag == null || |
| !myParentStyleNameAttrValue.equals(styleNameAttrValueForTag)) { |
| return false; |
| } |
| } |
| else if (myParentStyleNameAttrValue != null) { |
| return false; |
| } |
| return true; |
| } |
| |
| |
| public void setSearchOnlyInCurrentModule(boolean searchOnlyInCurrentModule) { |
| mySearchOnlyInCurrentModule = searchOnlyInCurrentModule; |
| } |
| |
| public void setFileToScan(VirtualFile fileToScan) { |
| myFileToScan = fileToScan; |
| } |
| |
| @NotNull |
| public Module getModule() { |
| return myModule; |
| } |
| |
| @NotNull |
| public String getStyleName() { |
| return myStyleName; |
| } |
| |
| public void configureScope(MyScope scope, @Nullable VirtualFile context) { |
| if (scope == MyScope.MODULE) { |
| setSearchOnlyInCurrentModule(true); |
| } |
| else if (scope == MyScope.FILE) { |
| setSearchOnlyInCurrentModule(true); |
| setFileToScan(context); |
| } |
| } |
| |
| enum MyScope { |
| PROJECT, MODULE, FILE |
| } |
| } |