| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * 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.android.tools.idea.rendering; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.ide.common.res2.DataBindingResourceType; |
| import com.android.ide.common.res2.ResourceFile; |
| import com.android.ide.common.res2.ResourceItem; |
| import com.android.ide.common.resources.configuration.FolderConfiguration; |
| import com.android.resources.FolderTypeRelationship; |
| import com.android.resources.ResourceFolderType; |
| import com.android.resources.ResourceType; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.tools.idea.configurations.ConfigurationManager; |
| import com.android.tools.idea.databinding.DataBindingUtil; |
| import com.android.tools.idea.model.ManifestInfo; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.google.common.collect.*; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.fileTypes.FileType; |
| import com.intellij.openapi.fileTypes.FileTypeManager; |
| import com.intellij.openapi.fileTypes.StdFileTypes; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.*; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.psi.xml.*; |
| import com.intellij.util.ArrayUtil; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.sdk.AndroidTargetData; |
| import org.jetbrains.annotations.Contract; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| |
| import static com.android.SdkConstants.*; |
| import static com.android.resources.ResourceFolderType.*; |
| import static com.android.tools.idea.rendering.PsiProjectListener.isRelevantFile; |
| import static com.android.tools.idea.rendering.PsiProjectListener.isRelevantFileType; |
| import static com.android.tools.idea.rendering.ResourceHelper.getFolderType; |
| import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix; |
| |
| /** |
| * Remaining work: |
| * <ul> |
| * <li>Find some way to have event updates in this resource folder directly update parent repositories |
| * (typically {@link ModuleResourceRepository}</li> |
| * <li>consider *initializing* this repository initially from IO files to not force full modelling of |
| * XML objects for all these tiny files (translations etc) ? Or find some way to persist the data in the index.</li> |
| * <li>Add defensive checks for non-read permission reads of resource values</li> |
| * <li>Idea: For {@link #rescan}; compare the removed items from the added items, and if they're the same, avoid |
| * creating a new generation.</li> |
| * <li>Register the psi project listener as a project service instead</li> |
| * </ul> |
| */ |
| public final class ResourceFolderRepository extends LocalResourceRepository { |
| private static final Logger LOG = Logger.getInstance(ResourceFolderRepository.class); |
| private final Module myModule; |
| private final AndroidFacet myFacet; |
| private final PsiListener myListener; |
| private final VirtualFile myResourceDir; |
| private final Map<ResourceType, ListMultimap<String, ResourceItem>> myItems = Maps.newEnumMap(ResourceType.class); |
| private final Map<PsiFile, PsiResourceFile> myResourceFiles = Maps.newHashMap(); |
| // qualifiedName -> PsiResourceFile |
| private Map<String, DataBindingInfo> myDataBindingResourceFiles = Maps.newHashMap(); |
| private long myDataBindingResourceFilesModificationCount = Long.MIN_VALUE; |
| private final Object SCAN_LOCK = new Object(); |
| private Set<PsiFile> myPendingScans; |
| |
| @VisibleForTesting |
| static int ourFullRescans; |
| |
| private ResourceFolderRepository(@NotNull AndroidFacet facet, @NotNull VirtualFile resourceDir) { |
| super(resourceDir.getName()); |
| myFacet = facet; |
| myModule = facet.getModule(); |
| myListener = new PsiListener(); |
| myResourceDir = resourceDir; |
| scan(); |
| } |
| |
| @NotNull |
| AndroidFacet getFacet() { |
| return myFacet; |
| } |
| |
| VirtualFile getResourceDir() { |
| return myResourceDir; |
| } |
| |
| /** NOTE: You should normally use {@link ResourceFolderRegistry#get} rather than this method. */ |
| @NotNull |
| static ResourceFolderRepository create(@NotNull final AndroidFacet facet, @NotNull VirtualFile dir) { |
| return new ResourceFolderRepository(facet, dir); |
| } |
| |
| private void scan() { |
| ApplicationManager.getApplication().runReadAction(new Runnable() { |
| @Override |
| public void run() { |
| PsiManager manager = PsiManager.getInstance(myFacet.getModule().getProject()); |
| if (myResourceDir.isValid()) { |
| PsiDirectory directory = manager.findDirectory(myResourceDir); |
| if (directory != null) { |
| scanResFolder(directory); |
| } |
| } |
| } |
| }); |
| } |
| |
| @Nullable |
| private PsiFile ensureValid(@NotNull PsiFile psiFile) { |
| if (psiFile.isValid()) { |
| return psiFile; |
| } else { |
| VirtualFile virtualFile = psiFile.getVirtualFile(); |
| if (virtualFile != null && virtualFile.exists()) { |
| Project project = myModule.getProject(); |
| if (!project.isDisposed()) { |
| return PsiManager.getInstance(project).findFile(virtualFile); |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| private void scanResFolder(@NotNull PsiDirectory res) { |
| for (PsiDirectory dir : res.getSubdirectories()) { |
| String name = dir.getName(); |
| ResourceFolderType folderType = ResourceFolderType.getFolderType(name); |
| if (folderType != null) { |
| String qualifiers = getQualifiers(name); |
| FolderConfiguration folderConfiguration = FolderConfiguration.getConfigForFolder(name); |
| if (folderConfiguration == null) { |
| continue; |
| } |
| if (folderType == VALUES) { |
| scanValueResFolder(dir, qualifiers, folderConfiguration); |
| } else { |
| scanFileResourceFolder(dir, folderType, qualifiers, folderConfiguration); |
| } |
| } |
| } |
| } |
| |
| private static String getQualifiers(String dirName) { |
| int index = dirName.indexOf('-'); |
| return index != -1 ? dirName.substring(index + 1) : ""; |
| } |
| |
| private void scanFileResourceFolder(@NotNull PsiDirectory directory, ResourceFolderType folderType, String qualifiers, |
| FolderConfiguration folderConfiguration) { |
| List<ResourceType> resourceTypes = FolderTypeRelationship.getRelatedResourceTypes(folderType); |
| assert resourceTypes.size() >= 1 : folderType; |
| ResourceType type = resourceTypes.get(0); |
| |
| boolean idGenerating = resourceTypes.size() > 1; |
| assert !idGenerating || resourceTypes.size() == 2 && resourceTypes.get(1) == ResourceType.ID; |
| |
| ListMultimap<String, ResourceItem> map = getMap(type, true); |
| |
| for (PsiFile file : directory.getFiles()) { |
| FileType fileType = file.getFileType(); |
| if (isRelevantFileType(fileType) || folderType == ResourceFolderType.RAW) { |
| scanFileResourceFile(qualifiers, folderType, folderConfiguration, type, idGenerating, map, file); |
| |
| } // TODO: Else warn about files that aren't expected to be found here? |
| } |
| } |
| |
| private void scanFileResourceFile(String qualifiers, |
| ResourceFolderType folderType, |
| FolderConfiguration folderConfiguration, |
| ResourceType type, |
| boolean idGenerating, |
| ListMultimap<String, ResourceItem> map, |
| PsiFile file) { |
| // XML or Image |
| String name = ResourceHelper.getResourceName(file); |
| ResourceItem item = new PsiResourceItem(name, type, null, file); |
| |
| if (idGenerating) { |
| List<ResourceItem> items = Lists.newArrayList(); |
| items.add(item); |
| map.put(name, item); |
| addIds(items, file); |
| |
| PsiResourceFile resourceFile = new PsiResourceFile(file, items, qualifiers, folderType, folderConfiguration); |
| scanDataBinding(resourceFile, getModificationCount()); |
| myResourceFiles.put(file, resourceFile); |
| } else { |
| PsiResourceFile resourceFile = new PsiResourceFile(file, item, qualifiers, folderType, folderConfiguration); |
| myResourceFiles.put(file, resourceFile); |
| map.put(name, item); |
| } |
| } |
| |
| @Nullable |
| @Override |
| public DataBindingInfo getDataBindingInfoForLayout(String layoutName) { |
| List<ResourceItem> resourceItems = getResourceItem(ResourceType.LAYOUT, layoutName); |
| if (resourceItems == null) { |
| return null; |
| } |
| for (ResourceItem item : resourceItems) { |
| final ResourceFile source = item.getSource(); |
| if (source instanceof PsiResourceFile && ((PsiResourceFile) source).getDataBindingInfo() != null) { |
| return ((PsiResourceFile) source).getDataBindingInfo(); |
| } |
| } |
| return null; |
| } |
| |
| @NotNull |
| @Override |
| public Map<String, DataBindingInfo> getDataBindingResourceFiles() { |
| long modificationCount = getModificationCount(); |
| if (myDataBindingResourceFilesModificationCount == modificationCount) { |
| return myDataBindingResourceFiles; |
| } |
| Map<String, DataBindingInfo> selected = Maps.newHashMap(); |
| for (PsiResourceFile file : myResourceFiles.values()) { |
| DataBindingInfo info = file.getDataBindingInfo(); |
| if (info != null) { |
| selected.put(info.getQualifiedName(), info); |
| } |
| } |
| myDataBindingResourceFiles = Collections.unmodifiableMap(selected); |
| myDataBindingResourceFilesModificationCount = modificationCount; |
| return myDataBindingResourceFiles; |
| } |
| |
| @Nullable |
| private static XmlTag getLayoutTag(PsiElement element) { |
| if (!(element instanceof XmlFile)) { |
| return null; |
| } |
| final XmlTag rootTag = ((XmlFile) element).getRootTag(); |
| if (rootTag != null && TAG_LAYOUT.equals(rootTag.getName())) { |
| return rootTag; |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static XmlTag getDataTag(XmlTag layoutTag) { |
| return layoutTag.findFirstSubTag(TAG_DATA); |
| } |
| |
| private static void scanDataBindingDataTag(PsiResourceFile resourceFile, @Nullable XmlTag dataTag, long modificationCount) { |
| DataBindingInfo info = resourceFile.getDataBindingInfo(); |
| assert info != null; |
| List<PsiDataBindingResourceItem> items = Lists.newArrayList(); |
| if (dataTag == null) { |
| info.replaceItems(items, modificationCount); |
| return; |
| } |
| Set<String> usedNames = Sets.newHashSet(); |
| for (XmlTag tag : dataTag.findSubTags(TAG_VARIABLE)) { |
| String nameValue = tag.getAttributeValue(ATTR_NAME); |
| if (nameValue == null) { |
| continue; |
| } |
| String name = StringUtil.unescapeXml(nameValue); |
| if (StringUtil.isNotEmpty(name)) { |
| if (usedNames.add(name)) { |
| PsiDataBindingResourceItem item = new PsiDataBindingResourceItem(name, DataBindingResourceType.VARIABLE, tag); |
| item.setSource(resourceFile); |
| items.add(item); |
| } |
| } |
| } |
| Set<String> usedAliases = Sets.newHashSet(); |
| for (XmlTag tag : dataTag.findSubTags(TAG_IMPORT)) { |
| String nameValue = tag.getAttributeValue(ATTR_TYPE); |
| if (nameValue == null) { |
| continue; |
| } |
| String name = StringUtil.unescapeXml(nameValue); |
| String aliasValue = tag.getAttributeValue(ATTR_ALIAS); |
| String alias = null; |
| if (aliasValue != null) { |
| alias = StringUtil.unescapeXml(aliasValue); |
| } |
| if (alias == null) { |
| int lastIndexOfDot = name.lastIndexOf('.'); |
| if (lastIndexOfDot >= 0) { |
| alias = name.substring(lastIndexOfDot + 1); |
| } |
| } |
| if (StringUtil.isNotEmpty(alias)) { |
| if (usedAliases.add(name)) { |
| PsiDataBindingResourceItem item = new PsiDataBindingResourceItem(name, DataBindingResourceType.IMPORT, tag); |
| item.setSource(resourceFile); |
| items.add(item); |
| } |
| } |
| } |
| |
| info.replaceItems(items, modificationCount); |
| } |
| |
| private void scanDataBinding(PsiResourceFile resourceFile, long modificationCount) { |
| if (resourceFile.getFolderType() != LAYOUT) { |
| resourceFile.setDataBindingInfo(null); |
| return; |
| } |
| XmlTag layout = getLayoutTag(resourceFile.getPsiFile()); |
| if (layout == null) { |
| resourceFile.setDataBindingInfo(null); |
| return; |
| } |
| XmlTag dataTag = getDataTag(layout); |
| String className; |
| String classPackage; |
| String modulePackage = ManifestInfo.get(myFacet.getModule(), false).getPackage(); |
| String classAttrValue = null; |
| if (dataTag != null) { |
| classAttrValue = dataTag.getAttributeValue(ATTR_CLASS); |
| if (classAttrValue != null) { |
| classAttrValue = StringUtil.unescapeXml(classAttrValue); |
| } |
| } |
| if (StringUtil.isEmpty(classAttrValue)) { |
| className = DataBindingUtil.convertToJavaClassName(resourceFile.getName()) + "Binding"; |
| classPackage = modulePackage + ".databinding"; |
| } else { |
| int firstDotIndex = classAttrValue.indexOf('.'); |
| |
| if (firstDotIndex < 0) { |
| classPackage = modulePackage + ".databinding"; |
| className = classAttrValue; |
| } else { |
| int lastDotIndex = classAttrValue.lastIndexOf('.'); |
| if (firstDotIndex == 0) { |
| classPackage = modulePackage + classAttrValue.substring(0, lastDotIndex); |
| } else { |
| classPackage = classAttrValue.substring(0, lastDotIndex); |
| } |
| className = classAttrValue.substring(lastDotIndex + 1); |
| } |
| } |
| if (resourceFile.getDataBindingInfo() == null) { |
| resourceFile.setDataBindingInfo(new DataBindingInfo(myFacet, resourceFile, className, classPackage)); |
| } else { |
| resourceFile.getDataBindingInfo().update(className, classPackage, modificationCount); |
| } |
| scanDataBindingDataTag(resourceFile, dataTag, modificationCount); |
| } |
| |
| @NonNull |
| @Override |
| protected Map<ResourceType, ListMultimap<String, ResourceItem>> getMap() { |
| return myItems; |
| } |
| |
| @Nullable |
| @Override |
| @Contract("_, true -> !null") |
| protected ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create) { |
| ListMultimap<String, ResourceItem> multimap = myItems.get(type); |
| if (multimap == null && create) { |
| multimap = ArrayListMultimap.create(); |
| myItems.put(type, multimap); |
| } |
| return multimap; |
| } |
| |
| @Override |
| public void clear() { |
| super.clear(); |
| myResourceFiles.clear(); |
| } |
| |
| private void addIds(List<ResourceItem> items, PsiFile file) { |
| addIds(items, file, file); |
| } |
| |
| private void addIds(List<ResourceItem> items, PsiElement element, PsiFile file) { |
| // "@+id/" names found before processing the view tag corresponding to the id. |
| Map<String, XmlTag> pendingResourceIds = Maps.newHashMap(); |
| Collection<XmlTag> xmlTags = PsiTreeUtil.findChildrenOfType(element, XmlTag.class); |
| if (element instanceof XmlTag) { |
| addId(items, file, (XmlTag)element, pendingResourceIds); |
| } |
| if (!xmlTags.isEmpty()) { |
| for (XmlTag tag : xmlTags) { |
| addId(items, file, tag, pendingResourceIds); |
| } |
| } |
| // Add any remaining ids. |
| if (!pendingResourceIds.isEmpty()) { |
| ListMultimap<String, ResourceItem> map = getMap(ResourceType.ID, true); |
| for (Map.Entry<String, XmlTag> entry : pendingResourceIds.entrySet()) { |
| String id = entry.getKey(); |
| map.put(id, new PsiResourceItem(id, ResourceType.ID, entry.getValue(), file)); |
| } |
| } |
| } |
| |
| private void addId(List<ResourceItem> items, PsiFile file, XmlTag tag, Map<String, XmlTag> pendingResourceIds) { |
| assert tag.isValid(); |
| for (XmlAttribute attribute : tag.getAttributes()) { |
| if (ANDROID_URI.equals(attribute.getNamespace())) { |
| // For all attributes in the android namespace, check if something has a value of the form "@+id/" |
| // If the attribute is not android:id, and an item for it hasn't been created yet, add it to |
| // the list of pending ids. |
| String value = attribute.getValue(); |
| if (value != null && value.startsWith(NEW_ID_PREFIX) && !ATTR_ID.equals(attribute.getLocalName())) { |
| ListMultimap<String, ResourceItem> map = myItems.get(ResourceType.ID); |
| String id = value.substring(NEW_ID_PREFIX.length()); |
| if (map != null && !map.containsKey(id) && !pendingResourceIds.containsKey(id)) { |
| pendingResourceIds.put(id, tag); |
| } |
| } |
| } |
| } |
| // Now process the android:id attribute. |
| String id = tag.getAttributeValue(ATTR_ID, ANDROID_URI); |
| if (id != null) { |
| if (id.startsWith(ID_PREFIX)) { |
| // If the id is not "@+id/", it may still have been declared as "@+id/" in a preceding view (eg. layout_above). |
| // So, we test if this is such a pending id. |
| id = id.substring(ID_PREFIX.length()); |
| if (!pendingResourceIds.containsKey(id)) { |
| return; |
| } |
| } else if (id.startsWith(NEW_ID_PREFIX)) { |
| id = id.substring(NEW_ID_PREFIX.length()); |
| } else { |
| return; |
| } |
| pendingResourceIds.remove(id); |
| PsiResourceItem item = new PsiResourceItem(id, ResourceType.ID, tag, file); |
| items.add(item); |
| |
| getMap(ResourceType.ID, true).put(id, item); |
| } |
| } |
| |
| private void scanValueResFolder(@NotNull PsiDirectory directory, String qualifiers, FolderConfiguration folderConfiguration) { |
| //noinspection ConstantConditions |
| assert directory.getName().startsWith(FD_RES_VALUES); |
| |
| for (PsiFile file : directory.getFiles()) { |
| scanValueFile(qualifiers, file, folderConfiguration); |
| } |
| } |
| |
| private boolean scanValueFile(String qualifiers, PsiFile file, FolderConfiguration folderConfiguration) { |
| boolean added = false; |
| FileType fileType = file.getFileType(); |
| if (fileType == StdFileTypes.XML) { |
| XmlFile xmlFile = (XmlFile)file; |
| assert xmlFile.isValid(); |
| XmlDocument document = xmlFile.getDocument(); |
| if (document != null) { |
| XmlTag root = document.getRootTag(); |
| if (root == null) { |
| return false; |
| } |
| if (!root.getName().equals(TAG_RESOURCES)) { |
| return false; |
| } |
| XmlTag[] subTags = root.getSubTags(); // Not recursive, right? |
| List<ResourceItem> items = Lists.newArrayListWithExpectedSize(subTags.length); |
| for (XmlTag tag : subTags) { |
| String name = tag.getAttributeValue(ATTR_NAME); |
| if (name != null) { |
| ResourceType type = getType(tag); |
| if (type != null) { |
| ListMultimap<String, ResourceItem> map = getMap(type, true); |
| ResourceItem item = new PsiResourceItem(name, type, tag, file); |
| map.put(name, item); |
| items.add(item); |
| added = true; |
| |
| if (type == ResourceType.DECLARE_STYLEABLE) { |
| // for declare styleables we also need to create attr items for its children |
| XmlTag[] attrs = tag.getSubTags(); |
| if (attrs.length > 0) { |
| map = myItems.get(ResourceType.ATTR); |
| if (map == null) { |
| map = ArrayListMultimap.create(); |
| myItems.put(ResourceType.ATTR, map); |
| } |
| |
| for (XmlTag child : attrs) { |
| String attrName = child.getAttributeValue(ATTR_NAME); |
| if (attrName != null && !attrName.startsWith(ANDROID_NS_NAME_PREFIX) |
| // Only add attr nodes for elements that specify a format or have flag/enum children; otherwise |
| // it's just a reference to an existing attr |
| && (child.getAttribute(ATTR_FORMAT) != null || child.getSubTags().length > 0)) { |
| ResourceItem attrItem = new PsiResourceItem(attrName, ResourceType.ATTR, child, file); |
| items.add(attrItem); |
| map.put(attrName, attrItem); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| PsiResourceFile resourceFile = new PsiResourceFile(file, items, qualifiers, ResourceFolderType.VALUES, folderConfiguration); |
| myResourceFiles.put(file, resourceFile); |
| } |
| } |
| |
| return added; |
| } |
| |
| /** |
| * Returns the type of the ResourceItem based on a node's attributes. |
| * @param node the node |
| * @return the ResourceType or null if it could not be inferred. |
| */ |
| @Nullable |
| private static ResourceType getType(XmlTag node) { |
| String nodeName = node.getLocalName(); |
| String typeString = null; |
| |
| if (TAG_ITEM.equals(nodeName)) { |
| String attribute = node.getAttributeValue(ATTR_TYPE); |
| if (attribute != null) { |
| typeString = attribute; |
| } |
| } else { |
| // the type is the name of the node. |
| typeString = nodeName; |
| } |
| |
| if (typeString != null) { |
| return ResourceType.getEnum(typeString); |
| } |
| |
| return null; |
| } |
| |
| private boolean isResourceFolder(@Nullable PsiElement parent) { |
| // Returns true if the given element represents a resource folder (e.g. res/values-en-rUS or layout-land, *not* the root res/ folder) |
| if (parent instanceof PsiDirectory) { |
| PsiDirectory directory = (PsiDirectory)parent; |
| PsiDirectory parentDirectory = directory.getParentDirectory(); |
| if (parentDirectory != null) { |
| VirtualFile dir = parentDirectory.getVirtualFile(); |
| return dir.equals(myResourceDir); |
| } |
| } |
| return false; |
| } |
| |
| private boolean isResourceFile(PsiFile psiFile) { |
| return isResourceFolder(psiFile.getParent()); |
| } |
| |
| @Override |
| public boolean isScanPending(@NonNull PsiFile psiFile) { |
| synchronized (SCAN_LOCK) { |
| return myPendingScans != null && myPendingScans.contains(psiFile); |
| } |
| } |
| |
| @VisibleForTesting |
| void rescan(@NonNull final PsiFile psiFile, final @NonNull ResourceFolderType folderType) { |
| synchronized(SCAN_LOCK) { |
| if (isScanPending(psiFile)) { |
| return; |
| } |
| |
| if (myPendingScans == null) { |
| myPendingScans = Sets.newHashSet(); |
| } |
| myPendingScans.add(psiFile); |
| } |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| boolean rescan; |
| synchronized (SCAN_LOCK) { |
| // Handled by {@link #sync()} after the {@link #rescan} call and before invokeLater ? |
| rescan = myPendingScans != null && myPendingScans.contains(psiFile); |
| } |
| if (rescan) { |
| rescanImmediately(psiFile, folderType); |
| synchronized (SCAN_LOCK) { |
| // myPendingScans can't be null here because the only method which clears it |
| // is sync() which also requires a write lock, and we've held the write lock |
| // since null checking it above |
| myPendingScans.remove(psiFile); |
| if (myPendingScans.isEmpty()) { |
| myPendingScans = null; |
| } |
| } |
| } |
| } |
| }); |
| } |
| }); |
| } |
| |
| @Override |
| public void sync() { |
| super.sync(); |
| |
| final List<PsiFile> files; |
| synchronized(SCAN_LOCK) { |
| if (myPendingScans == null || myPendingScans.isEmpty()) { |
| return; |
| } |
| files = new ArrayList<PsiFile>(myPendingScans); |
| } |
| |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| for (PsiFile file : files) { |
| if (file.isValid()) { |
| ResourceFolderType folderType = ResourceHelper.getFolderType(file); |
| if (folderType != null) { |
| rescanImmediately(file, folderType); |
| } |
| } |
| } |
| } |
| }); |
| |
| synchronized(SCAN_LOCK) { |
| myPendingScans = null; |
| } |
| } |
| |
| private void rescanImmediately(@NonNull final PsiFile psiFile, final @NonNull ResourceFolderType folderType) { |
| PsiFile file = psiFile; |
| if (folderType == VALUES) { |
| // For unit test tracking purposes only |
| //noinspection AssignmentToStaticFieldFromInstanceMethod |
| ourFullRescans++; |
| |
| // First delete out the previous items |
| PsiResourceFile resourceFile = myResourceFiles.get(file); |
| boolean removed = false; |
| if (resourceFile != null) { |
| for (ResourceItem item : resourceFile) { |
| removed |= removeItems(resourceFile, item.getType(), item.getName(), false); // Will throw away file |
| } |
| |
| myResourceFiles.remove(file); |
| } |
| |
| file = ensureValid(file); |
| boolean added = false; |
| if (file != null) { |
| // Add items for this file |
| PsiDirectory parent = file.getParent(); |
| assert parent != null; // since we have a folder type |
| String dirName = parent.getName(); |
| PsiDirectory fileParent = psiFile.getParent(); |
| if (fileParent != null) { |
| FolderConfiguration folderConfiguration = FolderConfiguration.getConfigForFolder(fileParent.getName()); |
| if (folderConfiguration != null) { |
| added = scanValueFile(getQualifiers(dirName), file, folderConfiguration); |
| } |
| } |
| } |
| |
| if (added || removed) { |
| // TODO: Consider doing a deeper diff of the changes to the resource items |
| // to determine if the removed and added items actually differ |
| myGeneration++; |
| invalidateItemCaches(); |
| } |
| } else { |
| PsiResourceFile resourceFile = myResourceFiles.get(file); |
| if (resourceFile != null) { |
| // Already seen this file; no need to do anything unless it's a layout or |
| // menu file; in that case we may need to update the id's |
| if (folderType == LAYOUT || folderType == MENU) { |
| // For unit test tracking purposes only |
| //noinspection AssignmentToStaticFieldFromInstanceMethod |
| ourFullRescans++; |
| |
| // We've already seen this resource, so no change in the ResourceItem for the |
| // file itself (e.g. @layout/foo from layout-land/foo.xml). However, we may have |
| // to update the id's: |
| Set<String> idsBefore = Sets.newHashSet(); |
| Set<String> idsAfter = Sets.newHashSet(); |
| ListMultimap<String, ResourceItem> map = myItems.get(ResourceType.ID); |
| if (map != null) { |
| List<ResourceItem> idItems = Lists.newArrayList(); |
| for (ResourceItem item : resourceFile) { |
| if (item.getType() == ResourceType.ID) { |
| idsBefore.add(item.getName()); |
| idItems.add(item); |
| } |
| } |
| for (String id : idsBefore) { |
| // Note that ResourceFile has a flat map (not a multimap) so it doesn't |
| // record all items (unlike the myItems map) so we need to remove the map |
| // items manually, can't just do map.remove(item.getName(), item) |
| List<ResourceItem> mapItems = map.get(id); |
| if (mapItems != null && !mapItems.isEmpty()) { |
| List<ResourceItem> toDelete = Lists.newArrayListWithExpectedSize(mapItems.size()); |
| for (ResourceItem mapItem : mapItems) { |
| if (mapItem.getSource() == resourceFile) { |
| toDelete.add(mapItem); |
| } |
| } |
| for (ResourceItem delete : toDelete) { |
| map.remove(delete.getName(), delete); |
| } |
| } |
| } |
| resourceFile.removeItems(idItems); |
| } |
| |
| // Add items for this file |
| List<ResourceItem> idItems = Lists.newArrayList(); |
| file = ensureValid(file); |
| if (file != null) { |
| addIds(idItems, file); |
| } |
| if (!idItems.isEmpty()) { |
| resourceFile.addItems(idItems); |
| for (ResourceItem item : idItems) { |
| idsAfter.add(item.getName()); |
| } |
| } |
| |
| if (!idsBefore.equals(idsAfter)) { |
| myGeneration++; |
| } |
| scanDataBinding(resourceFile, myGeneration); |
| // Identities may have changed even if the ids are the same, so update maps |
| invalidateItemCaches(ResourceType.ID); |
| } |
| } else { |
| // For unit test tracking purposes only |
| //noinspection AssignmentToStaticFieldFromInstanceMethod |
| ourFullRescans++; |
| |
| PsiDirectory parent = file.getParent(); |
| assert parent != null; // since we have a folder type |
| String dirName = parent.getName(); |
| |
| List<ResourceType> resourceTypes = FolderTypeRelationship.getRelatedResourceTypes(folderType); |
| assert resourceTypes.size() >= 1 : folderType; |
| ResourceType type = resourceTypes.get(0); |
| |
| boolean idGenerating = resourceTypes.size() > 1; |
| assert !idGenerating || resourceTypes.size() == 2 && resourceTypes.get(1) == ResourceType.ID; |
| |
| ListMultimap<String, ResourceItem> map = getMap(type, true); |
| |
| file = ensureValid(file); |
| if (file != null) { |
| PsiDirectory fileParent = psiFile.getParent(); |
| if (fileParent != null) { |
| FolderConfiguration folderConfiguration = FolderConfiguration.getConfigForFolder(fileParent.getName()); |
| if (folderConfiguration != null) { |
| scanFileResourceFile(getQualifiers(dirName), folderType, folderConfiguration, type, idGenerating, map, file); |
| } |
| } |
| myGeneration++; |
| invalidateItemCaches(); |
| } |
| } |
| } |
| } |
| |
| private boolean removeItems(PsiResourceFile resourceFile, ResourceType type, String name, boolean removeFromFile) { |
| boolean removed = false; |
| |
| // Remove the item of the given name and type from the given resource file. |
| // We CAN'T just remove items found in ResourceFile.getItems() because that map |
| // flattens everything down to a single item for a given name (it's using a flat |
| // map rather than a multimap) so instead we have to look up from the map instead |
| ListMultimap<String, ResourceItem> map = myItems.get(type); |
| if (map != null) { |
| List<ResourceItem> mapItems = map.get(name); |
| if (mapItems != null) { |
| ListIterator<ResourceItem> iterator = mapItems.listIterator(); |
| while (iterator.hasNext()) { |
| ResourceItem next = iterator.next(); |
| if (next.getSource() == resourceFile) { |
| iterator.remove(); |
| if (removeFromFile) { |
| resourceFile.removeItem(next); |
| } |
| removed = true; |
| } |
| } |
| } |
| } |
| |
| return removed; |
| } |
| |
| /** |
| * Called when a bitmap has been changed/deleted. In that case we need to clear out any caches for that |
| * image held by layout lib. |
| */ |
| private void bitmapUpdated() { |
| ConfigurationManager configurationManager = myFacet.getConfigurationManager(false); |
| if (configurationManager != null) { |
| IAndroidTarget target = configurationManager.getTarget(); |
| if (target != null) { |
| Module module = myFacet.getModule(); |
| AndroidTargetData targetData = AndroidTargetData.getTargetData(target, module); |
| if (targetData != null) { |
| targetData.clearLayoutBitmapCache(module); |
| } |
| } |
| } |
| } |
| |
| @NotNull |
| public PsiTreeChangeListener getPsiListener() { |
| return myListener; |
| } |
| |
| /** PSI listener which keeps the repository up to date */ |
| private final class PsiListener extends PsiTreeChangeAdapter { |
| private boolean myIgnoreChildrenChanged; |
| |
| @Override |
| public void childAdded(@NotNull PsiTreeChangeEvent event) { |
| PsiFile psiFile = event.getFile(); |
| if (psiFile == null) { |
| // Called when you've added a file |
| PsiElement child = event.getChild(); |
| if (child instanceof PsiFile) { |
| psiFile = (PsiFile)child; |
| if (isRelevantFile(psiFile)) { |
| addFile(psiFile); |
| } |
| } else if (child instanceof PsiDirectory) { |
| PsiDirectory directory = (PsiDirectory)child; |
| if (isResourceFolder(directory)) { |
| for (PsiFile file : directory.getFiles()) { |
| if (isRelevantFile(file)) { |
| addFile(file); |
| } |
| } |
| } |
| } |
| } else if (isRelevantFile(psiFile)) { |
| if (isScanPending(psiFile)) { |
| return; |
| } |
| // Some child was added within a file |
| ResourceFolderType folderType = getFolderType(psiFile); |
| if (folderType != null && isResourceFile(psiFile)) { |
| PsiElement child = event.getChild(); |
| PsiElement parent = event.getParent(); |
| if (folderType == ResourceFolderType.VALUES) { |
| if (child instanceof XmlTag) { |
| XmlTag tag = (XmlTag)child; |
| |
| if (isItemElement(tag)) { |
| PsiResourceFile resourceFile = myResourceFiles.get(psiFile); |
| if (resourceFile != null) { |
| String name = tag.getAttributeValue(ATTR_NAME); |
| if (name != null) { |
| ResourceType type = getType(tag); |
| if (type == ResourceType.DECLARE_STYLEABLE) { |
| // Can't handle declare styleable additions incrementally yet; need to update paired attr items |
| rescan(psiFile, folderType); |
| return; |
| } |
| if (type != null) { |
| ListMultimap<String, ResourceItem> map = getMap(type, true); |
| ResourceItem item = new PsiResourceItem(name, type, tag, psiFile); |
| map.put(name, item); |
| resourceFile.addItems(Collections.singletonList(item)); |
| myGeneration++; |
| invalidateItemCaches(type); |
| } |
| } |
| |
| return; |
| } |
| } |
| |
| // See if you just added a new item inside a <style> or <array> or <declare-styleable> etc |
| XmlTag parentTag = tag.getParentTag(); |
| if (parentTag != null && ResourceType.getEnum(parentTag.getName()) != null) { |
| // Yes just invalidate the corresponding style value |
| ResourceItem style = findValueResourceItem(parentTag, psiFile); |
| if (style instanceof PsiResourceItem) { |
| if (((PsiResourceItem)style).recomputeValue()) { |
| myGeneration++; |
| } |
| return; |
| } |
| } |
| |
| rescan(psiFile, folderType); |
| // Else: fall through and do full file rescan |
| } else if (parent instanceof XmlText) { |
| // If the edit is within an item tag |
| XmlText text = (XmlText)parent; |
| handleValueXmlTextEdit(text.getParentTag(), psiFile); |
| return; |
| } else if (child instanceof XmlText) { |
| // If the edit is within an item tag |
| handleValueXmlTextEdit(parent, psiFile); |
| return; |
| } else if (parent instanceof XmlComment || child instanceof XmlComment) { |
| // Can ignore comment edits or new comments |
| return; |
| } |
| rescan(psiFile, folderType); |
| } else if (folderType == LAYOUT || folderType == MENU) { |
| if (parent instanceof XmlComment || child instanceof XmlComment) { |
| return; |
| } |
| if (parent instanceof XmlText || |
| (child instanceof XmlText && child.getText().trim().isEmpty())) { |
| return; |
| } |
| |
| if (parent instanceof XmlElement && child instanceof XmlElement) { |
| if (child instanceof XmlTag) { |
| List<ResourceItem> ids = Lists.newArrayList(); |
| addIds(ids, child, psiFile); |
| if (!ids.isEmpty()) { |
| PsiResourceFile resourceFile = myResourceFiles.get(psiFile); |
| if (resourceFile != null) { |
| resourceFile.addItems(ids); |
| myGeneration++; |
| } |
| } |
| return; |
| } else if (child instanceof XmlAttributeValue) { |
| assert parent instanceof XmlAttribute : parent; |
| XmlAttribute attribute = (XmlAttribute)parent; |
| // warning for separate if branches suppressed because to do. |
| //noinspection IfStatementWithIdenticalBranches |
| if (ATTR_ID.equals(attribute.getLocalName()) && |
| ANDROID_URI.equals(attribute.getNamespace())) { |
| // TODO: Update it incrementally |
| rescan(psiFile, folderType); |
| } else if (ArrayUtil.contains(attribute.getLocalName(), ATTRS_DATA_BINDING) |
| && ArrayUtil.contains(attribute.getParent().getLocalName(), TAGS_DATA_BINDING)) { |
| rescan(psiFile, folderType); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| myIgnoreChildrenChanged = true; |
| } |
| |
| @Override |
| public void childRemoved(@NotNull PsiTreeChangeEvent event) { |
| PsiFile psiFile = event.getFile(); |
| if (psiFile == null) { |
| // Called when you've removed a file |
| PsiElement child = event.getChild(); |
| if (child instanceof PsiFile) { |
| psiFile = (PsiFile)child; |
| if (isRelevantFile(psiFile)) { |
| removeFile(psiFile); |
| } |
| } else if (child instanceof PsiDirectory) { |
| // We can't iterate the children here because the dir is already empty. |
| // Instead, try to locate the files |
| String dirName = ((PsiDirectory)child).getName(); |
| ResourceFolderType folderType = ResourceFolderType.getFolderType(dirName); |
| |
| if (folderType != null) { |
| // Make sure it's really a resource folder. We can't look at the directory |
| // itself since the file no longer exists, but make sure the parent directory is |
| // a resource directory root |
| PsiDirectory parentDirectory = ((PsiDirectory)child).getParent(); |
| if (parentDirectory != null) { |
| VirtualFile dir = parentDirectory.getVirtualFile(); |
| if (!myFacet.getLocalResourceManager().isResourceDir(dir)) { |
| return; |
| } |
| } else { |
| return; |
| } |
| int index = dirName.indexOf('-'); |
| String qualifiers; |
| if (index == -1) { |
| qualifiers = ""; |
| } else { |
| qualifiers = dirName.substring(index + 1); |
| } |
| |
| // Copy file map so we can delete while iterating |
| Collection<PsiResourceFile> resourceFiles = new ArrayList<PsiResourceFile>(myResourceFiles.values()); |
| for (PsiResourceFile file : resourceFiles) { |
| if (folderType == file.getFolderType() && qualifiers.equals(file.getQualifiers())) { |
| removeFile(file); |
| } |
| } |
| } |
| } |
| } else if (isRelevantFile(psiFile)) { |
| if (isScanPending(psiFile)) { |
| return; |
| } |
| // Some child was removed within a file |
| ResourceFolderType folderType = getFolderType(psiFile); |
| if (folderType != null && isResourceFile(psiFile)) { |
| PsiElement child = event.getChild(); |
| PsiElement parent = event.getParent(); |
| |
| if (folderType == ResourceFolderType.VALUES) { |
| if (child instanceof XmlTag) { |
| XmlTag tag = (XmlTag)child; |
| |
| // See if you just removed an item inside a <style> or <array> or <declare-styleable> etc |
| if (parent instanceof XmlTag) { |
| XmlTag parentTag = (XmlTag)parent; |
| if (ResourceType.getEnum(parentTag.getName()) != null) { |
| // Yes just invalidate the corresponding style value |
| ResourceItem style = findValueResourceItem(parentTag, psiFile); |
| if (style instanceof PsiResourceItem) { |
| if (((PsiResourceItem)style).recomputeValue()) { |
| myGeneration++; |
| } |
| |
| if (style.getType() == ResourceType.ATTR) { |
| parentTag = parentTag.getParentTag(); |
| if (parentTag != null && parentTag.getName().equals(ResourceType.DECLARE_STYLEABLE.getName())) { |
| ResourceItem declareStyleable = findValueResourceItem(parentTag, psiFile); |
| if (declareStyleable instanceof PsiResourceItem) { |
| if (((PsiResourceItem)declareStyleable).recomputeValue()) { |
| myGeneration++; |
| } |
| } |
| } |
| } |
| return; |
| } |
| } |
| } |
| |
| if (isItemElement(tag)) { |
| PsiResourceFile resourceFile = myResourceFiles.get(psiFile); |
| if (resourceFile != null) { |
| String name; |
| if (!tag.isValid()) { |
| ResourceItem item = findValueResourceItem(tag, psiFile); |
| if (item != null) { |
| name = item.getName(); |
| } else { |
| // Can't find the name of the deleted tag; just do a full rescan |
| rescan(psiFile, folderType); |
| return; |
| } |
| } else { |
| name = tag.getAttributeValue(ATTR_NAME); |
| } |
| if (name != null) { |
| ResourceType type = getType(tag); |
| if (type != null) { |
| ListMultimap<String, ResourceItem> map = myItems.get(type); |
| if (map == null) { |
| return; |
| } |
| if (removeItems(resourceFile, type, name, true)) { |
| myGeneration++; |
| invalidateItemCaches(type); |
| } |
| } |
| } |
| |
| return; |
| } |
| } |
| |
| rescan(psiFile, folderType); |
| } else if (parent instanceof XmlText) { |
| // If the edit is within an item tag |
| XmlText text = (XmlText)parent; |
| handleValueXmlTextEdit(text.getParentTag(), psiFile); |
| } else if (child instanceof XmlText) { |
| handleValueXmlTextEdit(parent, psiFile); |
| } else if (parent instanceof XmlComment || child instanceof XmlComment) { |
| // Can ignore comment edits or removed comments |
| return; |
| } else { |
| // Some other change: do full file rescan |
| rescan(psiFile, folderType); |
| } |
| } else if (folderType == LAYOUT || folderType == MENU) { |
| // TODO: Handle removals of id's (values an attributes) incrementally |
| rescan(psiFile, folderType); |
| } |
| } |
| } |
| |
| myIgnoreChildrenChanged = true; |
| } |
| |
| private void removeFile(@Nullable PsiResourceFile resourceFile) { |
| if (resourceFile == null) { |
| // No resources for this file |
| return; |
| } |
| for (Map.Entry<PsiFile, PsiResourceFile> entry : myResourceFiles.entrySet()) { |
| PsiResourceFile file = entry.getValue(); |
| if (resourceFile == file) { |
| PsiFile psiFile = entry.getKey(); |
| myResourceFiles.remove(psiFile); |
| break; |
| } |
| } |
| |
| myGeneration++; |
| invalidateItemCaches(); |
| |
| ResourceFolderType folderType = resourceFile.getFolderType(); |
| if (folderType == VALUES || folderType == LAYOUT || folderType == MENU) { |
| removeItemsFromFile(resourceFile); |
| } else if (folderType != null) { |
| // Remove the file item |
| List<ResourceType> resourceTypes = FolderTypeRelationship.getRelatedResourceTypes(folderType); |
| for (ResourceType type : resourceTypes) { |
| if (type != ResourceType.ID) { |
| String name = LintUtils.getBaseName(resourceFile.getName()); |
| removeItems(resourceFile, type, name, false); // no need since we're discarding the file |
| } |
| } |
| } // else: not a resource folder |
| } |
| |
| private void removeFile(PsiFile psiFile) { |
| assert !psiFile.isValid() || isRelevantFile(psiFile); |
| |
| PsiResourceFile resourceFile = myResourceFiles.get(psiFile); |
| if (resourceFile == null) { |
| // No resources for this file |
| return; |
| } |
| myResourceFiles.remove(psiFile); |
| myGeneration++; |
| invalidateItemCaches(); |
| |
| ResourceFolderType folderType = getFolderType(psiFile); |
| if (folderType == VALUES || folderType == LAYOUT || folderType == MENU) { |
| removeItemsFromFile(resourceFile); |
| } else if (folderType != null) { |
| if (folderType == DRAWABLE) { |
| FileType fileType = psiFile.getFileType(); |
| if (fileType.isBinary() && fileType == FileTypeManager.getInstance().getFileTypeByExtension(EXT_PNG)) { |
| bitmapUpdated(); |
| } |
| } |
| |
| // Remove the file item |
| List<ResourceType> resourceTypes = FolderTypeRelationship.getRelatedResourceTypes(folderType); |
| for (ResourceType type : resourceTypes) { |
| if (type != ResourceType.ID) { |
| String name = ResourceHelper.getResourceName(psiFile); |
| removeItems(resourceFile, type, name, false); // no need since we're discarding the file |
| } |
| } |
| } // else: not a resource folder |
| } |
| |
| private void addFile(PsiFile psiFile) { |
| assert isRelevantFile(psiFile); |
| |
| // Same handling as rescan, where the initial deletion is a no-op |
| ResourceFolderType folderType = getFolderType(psiFile); |
| if (folderType != null && isResourceFile(psiFile)) { |
| rescanImmediately(psiFile, folderType); |
| } |
| } |
| |
| @Override |
| public void childReplaced(@NotNull PsiTreeChangeEvent event) { |
| PsiFile psiFile = event.getFile(); |
| if (psiFile != null) { |
| if (isScanPending(psiFile)) { |
| return; |
| } |
| // This method is called when you edit within a file |
| if (isRelevantFile(psiFile)) { |
| // First determine if the edit is non-consequential. |
| // That's the case if the XML edited is not a resource file (e.g. the manifest file), |
| // or if it's within a file that is not a value file or an id-generating file (layouts and menus), |
| // such as editing the content of a drawable XML file. |
| ResourceFolderType folderType = getFolderType(psiFile); |
| if (folderType == LAYOUT || folderType == MENU) { |
| // The only way the edit affected the set of resources was if the user added or removed an |
| // id attribute. Since these can be added redundantly we can't automatically remove the old |
| // value if you renamed one, so we'll need a full file scan. |
| // However, we only need to do this scan if the change appears to be related to ids; this can |
| // only happen if the attribute value is changed. |
| PsiElement parent = event.getParent(); |
| PsiElement child = event.getChild(); |
| if (parent instanceof XmlText || child instanceof XmlText || |
| parent instanceof XmlComment || child instanceof XmlComment) { |
| return; |
| } |
| if (parent instanceof XmlElement && child instanceof XmlElement) { |
| if (event.getOldChild() == event.getNewChild()) { |
| // We're not getting accurate PSI information: we have to do a full file scan |
| rescan(psiFile, folderType); |
| return; |
| } |
| if (child instanceof XmlAttributeValue) { |
| assert parent instanceof XmlAttribute : parent; |
| @SuppressWarnings("CastConflictsWithInstanceof") // IDE bug? Cast is valid. |
| XmlAttribute attribute = (XmlAttribute)parent; |
| if (ATTR_ID.equals(attribute.getLocalName()) && |
| ANDROID_URI.equals(attribute.getNamespace())) { |
| // for each id attribute! |
| PsiResourceFile resourceFile = myResourceFiles.get(psiFile); |
| if (resourceFile != null) { |
| XmlTag xmlTag = attribute.getParent(); |
| PsiElement oldChild = event.getOldChild(); |
| PsiElement newChild = event.getNewChild(); |
| if (oldChild instanceof XmlAttributeValue && newChild instanceof XmlAttributeValue) { |
| XmlAttributeValue oldValue = (XmlAttributeValue)oldChild; |
| XmlAttributeValue newValue = (XmlAttributeValue)newChild; |
| String oldName = stripIdPrefix(oldValue.getValue()); |
| String newName = stripIdPrefix(newValue.getValue()); |
| if (oldName.equals(newName)) { |
| // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc) |
| return; |
| } |
| ResourceItem item = findResourceItem(ResourceType.ID, psiFile, oldName, xmlTag); |
| if (item != null) { |
| ListMultimap<String, ResourceItem> map = myItems.get(item.getType()); |
| if (map != null) { |
| // Found the relevant item: delete it and create a new one in a new location |
| map.remove(oldName, item); |
| ResourceItem newItem = new PsiResourceItem(newName, ResourceType.ID, xmlTag, psiFile); |
| map.put(newName, newItem); |
| resourceFile.replace(item, newItem); |
| myGeneration++; |
| invalidateItemCaches(ResourceType.ID); |
| return; |
| } |
| } |
| } |
| } |
| |
| rescan(psiFile, folderType); |
| } |
| } else if (parent instanceof XmlAttributeValue) { |
| assert parent.getParent() instanceof XmlAttribute : parent; |
| XmlAttribute attribute = (XmlAttribute)parent.getParent(); |
| if (ATTR_ID.equals(attribute.getLocalName()) && |
| ANDROID_URI.equals(attribute.getNamespace())) { |
| // for each id attribute! |
| PsiResourceFile resourceFile = myResourceFiles.get(psiFile); |
| if (resourceFile != null) { |
| XmlTag xmlTag = attribute.getParent(); |
| PsiElement oldChild = event.getOldChild(); |
| PsiElement newChild = event.getNewChild(); |
| String oldName = stripIdPrefix(oldChild.getText()); |
| String newName = stripIdPrefix(newChild.getText()); |
| if (oldName.equals(newName)) { |
| // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc) |
| return; |
| } |
| ResourceItem item = findResourceItem(ResourceType.ID, psiFile, oldName, xmlTag); |
| if (item != null) { |
| ListMultimap<String, ResourceItem> map = myItems.get(item.getType()); |
| if (map != null) { |
| // Found the relevant item: delete it and create a new one in a new location |
| map.remove(oldName, item); |
| ResourceItem newItem = new PsiResourceItem(newName, ResourceType.ID, xmlTag, psiFile); |
| map.put(newName, newItem); |
| resourceFile.replace(item, newItem); |
| myGeneration++; |
| invalidateItemCaches(ResourceType.ID); |
| return; |
| } |
| } |
| } |
| |
| rescan(psiFile, folderType); |
| } else if (ArrayUtil.contains(attribute.getLocalName(), ATTRS_DATA_BINDING) |
| && ArrayUtil.contains(attribute.getParent().getLocalName(), TAGS_DATA_BINDING)) { |
| PsiResourceFile resourceFile = myResourceFiles.get(psiFile); |
| if (resourceFile != null) { |
| myGeneration++; |
| scanDataBinding(resourceFile, myGeneration); |
| } |
| } |
| } |
| |
| return; |
| } |
| |
| // TODO: Handle adding/removing elements in layouts incrementally |
| |
| rescan(psiFile, folderType); |
| } else if (folderType == VALUES) { |
| PsiElement parent = event.getParent(); |
| if (parent instanceof XmlElement) { |
| // Editing within an XML file |
| // An edit in a comment can be ignored |
| // An edit in a text inside an element can be used to invalidate the ResourceValue of an element |
| // (need to search upwards since strings can have HTML content) |
| // An edit between elements can be ignored |
| // An edit to an attribute name (not the attribute value for the attribute named "name"...) can |
| // sometimes be ignored (if you edit type or name, consider what to do) |
| // An edit of an attribute value can affect the name of type so update item |
| // An edit of other parts; for example typing in a new <string> item character by character. |
| // etc. |
| |
| // See if you just removed an item inside a <style> or <array> or <declare-styleable> etc |
| if (parent instanceof XmlTag) { |
| XmlTag parentTag = (XmlTag)parent; |
| if (ResourceType.getEnum(parentTag.getName()) != null) { |
| // Yes just invalidate the corresponding style value |
| ResourceItem style = findValueResourceItem(parentTag, psiFile); |
| if (style instanceof PsiResourceItem) { |
| if (((PsiResourceItem)style).recomputeValue()) { |
| myGeneration++; |
| } |
| return; |
| } |
| } |
| |
| if (parentTag.getName().equals(TAG_RESOURCES) |
| && event.getOldChild() instanceof XmlText |
| && event.getNewChild() instanceof XmlText) { |
| return; |
| } |
| } |
| |
| if (parent instanceof XmlText) { |
| XmlText text = (XmlText)parent; |
| handleValueXmlTextEdit(text.getParentTag(), psiFile); |
| return; |
| } else if (parent instanceof XmlComment) { |
| // Nothing to do |
| return; |
| } |
| |
| if (parent instanceof XmlAttributeValue) { |
| PsiElement attribute = parent.getParent(); |
| if (attribute instanceof XmlProcessingInstruction) { |
| // Don't care about edits in the processing instructions, e.g. editing the encoding attribute in |
| // <?xml version="1.0" encoding="utf-8"?> |
| return; |
| } |
| PsiElement tag = attribute.getParent(); |
| assert attribute instanceof XmlAttribute : attribute; |
| XmlAttribute xmlAttribute = (XmlAttribute)attribute; |
| assert tag instanceof XmlTag : tag; |
| XmlTag xmlTag = (XmlTag)tag; |
| String attributeName = xmlAttribute.getName(); |
| // We could also special case handling of editing the type attribute, and the parent attribute, |
| // but editing these is rare enough that we can just stick with the fallback full file scan for those |
| // scenarios. |
| if (isItemElement(xmlTag) && attributeName.equals(ATTR_NAME)) { |
| // Edited the name of the item: replace it |
| ResourceType type = getType(xmlTag); |
| if (type != null) { |
| String oldName = event.getOldChild().getText(); |
| String newName = event.getNewChild().getText(); |
| if (oldName.equals(newName)) { |
| // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc) |
| return; |
| } |
| ResourceItem item = findResourceItem(type, psiFile, oldName, xmlTag); |
| if (item != null) { |
| ListMultimap<String, ResourceItem> map = myItems.get(item.getType()); |
| if (map != null) { |
| // Found the relevant item: delete it and create a new one in a new location |
| map.remove(oldName, item); |
| ResourceItem newItem = new PsiResourceItem(newName, type, xmlTag, psiFile); |
| map.put(newName, newItem); |
| PsiResourceFile resourceFile = myResourceFiles.get(psiFile); |
| if (resourceFile != null) { |
| resourceFile.replace(item, newItem); |
| } |
| else { |
| assert false : item; |
| } |
| myGeneration++; |
| invalidateItemCaches(type); |
| |
| // Invalidate surrounding declare styleable if any |
| if (type == ResourceType.ATTR) { |
| XmlTag parentTag = xmlTag.getParentTag(); |
| if (parentTag != null && parentTag.getName().equals(ResourceType.DECLARE_STYLEABLE.getName())) { |
| ResourceItem style = findValueResourceItem(parentTag, psiFile); |
| if (style instanceof PsiResourceItem) { |
| ((PsiResourceItem)style).recomputeValue(); |
| } |
| } |
| } |
| |
| return; |
| } |
| } |
| } else { |
| XmlTag parentTag = xmlTag.getParentTag(); |
| if (parentTag != null && ResourceType.getEnum(parentTag.getName()) != null) { |
| // <style>, or <plurals>, or <array>, or <string-array>, ... |
| // Edited the attribute value of an item that is wrapped in a <style> tag: invalidate parent cached value |
| ResourceItem style = findValueResourceItem(parentTag, psiFile); |
| if (style instanceof PsiResourceItem) { |
| if (((PsiResourceItem)style).recomputeValue()) { |
| myGeneration++; |
| } |
| return; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Fall through: We were not able to directly manipulate the repository to accommodate |
| // the edit, so re-scan the whole value file instead |
| rescan(psiFile, folderType); |
| |
| } // else: can ignore this edit |
| } |
| } else { |
| PsiElement parent = event.getParent(); |
| if (isResourceFolder(parent)) { |
| PsiElement oldChild = event.getOldChild(); |
| PsiElement newChild = event.getNewChild(); |
| if (oldChild instanceof PsiFile) { |
| PsiFile oldFile = (PsiFile)oldChild; |
| if (isRelevantFile(oldFile)) { |
| removeFile(oldFile); |
| } |
| } |
| if (newChild instanceof PsiFile) { |
| PsiFile newFile = (PsiFile)newChild; |
| if (isRelevantFile(newFile)) { |
| addFile(newFile); |
| } |
| } |
| } |
| } |
| |
| myIgnoreChildrenChanged = true; |
| } |
| |
| private void handleValueXmlTextEdit(@Nullable PsiElement parent, @NotNull PsiFile psiFile) { |
| if (!(parent instanceof XmlTag)) { |
| // Edited text outside the root element |
| return; |
| } |
| XmlTag parentTag = (XmlTag)parent; |
| String parentTagName = parentTag.getName(); |
| if (parentTagName.equals(TAG_RESOURCES)) { |
| // Editing whitespace between top level elements; ignore |
| return; |
| } |
| |
| if (parentTagName.equals(TAG_ITEM)) { |
| XmlTag style = parentTag.getParentTag(); |
| if (style != null && ResourceType.getEnum(style.getName()) != null) { |
| // <style>, or <plurals>, or <array>, or <string-array>, ... |
| // Edited the text value of an item that is wrapped in a <style> tag: invalidate |
| ResourceItem item = findValueResourceItem(style, psiFile); |
| if (item instanceof PsiResourceItem) { |
| boolean cleared = ((PsiResourceItem)item).recomputeValue(); |
| if (cleared) { // Only bump revision if this is a value which has already been observed! |
| myGeneration++; |
| } |
| } |
| return; |
| } |
| } |
| |
| // Find surrounding item |
| while (parentTag != null) { |
| if (isItemElement(parentTag)) { |
| ResourceItem item = findValueResourceItem(parentTag, psiFile); |
| if (item instanceof PsiResourceItem) { |
| // Edited XML value |
| boolean cleared = ((PsiResourceItem)item).recomputeValue(); |
| if (cleared) { // Only bump revision if this is a value which has already been observed! |
| myGeneration++; |
| } |
| } |
| break; |
| } |
| parentTag = parentTag.getParentTag(); |
| } |
| |
| // Fully handled; other whitespace changes do not affect resources |
| } |
| |
| @Override |
| public void childMoved(@NotNull PsiTreeChangeEvent event) { |
| PsiElement child = event.getChild(); |
| PsiFile psiFile = event.getFile(); |
| //noinspection StatementWithEmptyBody |
| if (psiFile == null) { |
| // This is called when you move a file from one folder to another |
| if (child instanceof PsiFile) { |
| psiFile = (PsiFile)child; |
| if (!isRelevantFile(psiFile)) { |
| return; |
| } |
| |
| // If you are renaming files, determine whether we can do a simple replacement |
| // (e.g. swap out ResourceFile instances), or whether it changes the type |
| // (e.g. moving foo.xml from layout/ to animator/), or whether it adds or removes |
| // the type (e.g. moving from invalid to valid resource directories), or whether |
| // it just affects the qualifiers (special case of swapping resource file instances). |
| String name = psiFile.getName(); |
| |
| PsiElement oldParent = event.getOldParent(); |
| PsiDirectory oldParentDir; |
| if (oldParent instanceof PsiDirectory) { |
| oldParentDir = (PsiDirectory)oldParent; |
| } else { |
| // Can't find old location: treat this as a file add |
| addFile(psiFile); |
| return; |
| } |
| |
| String oldDirName = oldParentDir.getName(); |
| ResourceFolderType oldFolderType = ResourceFolderType.getFolderType(oldDirName); |
| ResourceFolderType newFolderType = getFolderType(psiFile); |
| |
| boolean wasResourceFolder = oldFolderType != null && isResourceFolder(oldParentDir); |
| boolean isResourceFolder = newFolderType != null && isResourceFile(psiFile); |
| |
| if (wasResourceFolder == isResourceFolder) { |
| if (!isResourceFolder) { |
| // Moved a non-resource file around: nothing to do |
| return; |
| } |
| |
| // Moved a resource file from one resource folder to another: we need to update |
| // the ResourceFile entries for this file. We may also need to update the types. |
| PsiResourceFile resourceFile = findResourceFile(oldDirName, name); |
| if (resourceFile != null) { |
| if (oldFolderType != newFolderType) { |
| // In some cases we can do this cheaply, e.g. if you move from layout to menu |
| // we can just look up and change @layout/foo to @menu/foo, but if you move |
| // to or from values folder it gets trickier, so for now just treat this as a delete |
| // followed by an add |
| removeFile(resourceFile); |
| addFile(psiFile); |
| } else { |
| myResourceFiles.remove(resourceFile.getPsiFile()); |
| myResourceFiles.put(psiFile, resourceFile); |
| PsiDirectory newParent = psiFile.getParent(); |
| assert newParent != null; // Since newFolderType != null |
| String newDirName = newParent.getName(); |
| resourceFile.setPsiFile(psiFile, getQualifiers(newDirName)); |
| myGeneration++; // qualifiers may have changed: can affect configuration matching |
| // We need to recompute resource values too, since some of these can point to |
| // the old file (e.g. a drawable resource could have a DensityBasedResourceValue |
| // pointing to the old file |
| for (ResourceItem item : resourceFile) { // usually just 1 |
| if (item instanceof PsiResourceItem) { |
| ((PsiResourceItem)item).recomputeValue(); |
| } |
| } |
| invalidateItemCaches(); |
| } |
| } else { |
| // Couldn't find previous file; just add new file |
| addFile(psiFile); |
| } |
| } else if (isResourceFolder) { |
| // Moved file into resource folder: treat it as a file add |
| addFile(psiFile); |
| } else { |
| //noinspection ConstantConditions |
| assert wasResourceFolder; |
| |
| // Moved file out of resource folders: treat it as a file deletion. |
| // The only trick here is that we don't actually have the PsiFile anymore. |
| // Work around this by searching our PsiFile to ResourceFile map for a match. |
| String dirName = oldParentDir.getName(); |
| PsiResourceFile resourceFile = findResourceFile(dirName, name); |
| if (resourceFile != null) { |
| removeFile(resourceFile); |
| } |
| } |
| } |
| } else { |
| // Change inside a file |
| // Ignore: moving elements around doesn't affect the resources |
| } |
| |
| myIgnoreChildrenChanged = true; |
| } |
| |
| @Override |
| public final void beforeChildrenChange(@NotNull PsiTreeChangeEvent event) { |
| myIgnoreChildrenChanged = false; |
| } |
| |
| @Override |
| public void childrenChanged(@NotNull PsiTreeChangeEvent event) { |
| // Called after children have changed. There are typically individual childMoved, childAdded etc |
| // calls that we hook into for more specific details. However, there are some events we don't |
| // catch using those methods, and for that we have the below handling. |
| if (myIgnoreChildrenChanged) { |
| // We've already processed this change as one or more individual childMoved, childAdded, childRemoved etc calls |
| // However, we sometimes get some surprising (=bogus) events where the parent and the child |
| // are the same, and in those cases there may be other child events we need to process |
| // so fall through and process the whole file |
| if (event.getParent() != event.getChild()) { |
| return; |
| } |
| } |
| else if (event.getNewChild() == null && event.getOldChild() == null && event.getOldParent() == null && event.getNewParent() == null |
| && event.getParent() instanceof PsiFile) { |
| return; |
| } |
| |
| PsiFile psiFile = event.getFile(); |
| if (psiFile != null && isRelevantFile(psiFile)) { |
| VirtualFile file = psiFile.getVirtualFile(); |
| if (file != null) { |
| ResourceFolderType folderType = getFolderType(psiFile); |
| if (folderType != null && isResourceFile(psiFile)) { |
| // TODO: If I get an XmlText change and the parent is the resources tag or it's a layout, nothing to do. |
| rescan(psiFile, folderType); |
| } |
| } |
| } else { |
| Throwable throwable = new Throwable(); |
| throwable.fillInStackTrace(); |
| LOG.debug("Received unexpected childrenChanged event for inter-file operations", throwable); |
| } |
| } |
| |
| // There are cases where a file is renamed, and I don't get a pre-notification. Use this flag |
| // to detect those scenarios, and in that case, do proper cleanup. |
| // (Note: There are also cases where *only* beforePropertyChange is called, not propertyChange. |
| // One example is the unit test for the raw folder, where we're renaming a file, and we get |
| // the beforePropertyChange notification, followed by childReplaced on the PsiDirectory, and |
| // nothing else. |
| private boolean mySeenPrePropertyChange; |
| |
| @Override |
| public final void beforePropertyChange(@NotNull PsiTreeChangeEvent event) { |
| if (PsiTreeChangeEvent.PROP_FILE_NAME == event.getPropertyName()) { |
| // This is called when you rename a file (before the file has been renamed) |
| PsiElement child = event.getChild(); |
| if (child instanceof PsiFile) { |
| PsiFile psiFile = (PsiFile)child; |
| if (isRelevantFile(psiFile) && isResourceFolder(event.getParent())) { |
| removeFile(psiFile); |
| } |
| } |
| // The new name will be added in the post hook (propertyChanged rather than beforePropertyChange) |
| } |
| |
| mySeenPrePropertyChange = true; |
| } |
| |
| @Override |
| public void propertyChanged(@NotNull PsiTreeChangeEvent event) { |
| if (PsiTreeChangeEvent.PROP_FILE_NAME == event.getPropertyName() && isResourceFolder(event.getParent())) { |
| // This is called when you rename a file (after the file has been renamed) |
| PsiElement child = event.getElement(); |
| if (child instanceof PsiFile) { |
| PsiFile psiFile = (PsiFile)child; |
| if (isRelevantFile(psiFile) && isResourceFolder(event.getParent())) { |
| if (!mySeenPrePropertyChange) { |
| Object oldValue = event.getOldValue(); |
| if (oldValue instanceof String) { |
| PsiDirectory parent = psiFile.getParent(); |
| String oldName = (String)oldValue; |
| if (parent != null && parent.findFile(oldName) == null) { |
| removeFile(findResourceFile(parent.getName(), oldName)); |
| } |
| } |
| } |
| |
| addFile(psiFile); |
| } |
| } |
| } |
| |
| // TODO: Do we need to handle PROP_DIRECTORY_NAME for users renaming any of the resource folders? |
| // and what about PROP_FILE_TYPES -- can users change the type of an XML File to something else? |
| |
| mySeenPrePropertyChange = false; |
| } |
| } |
| |
| @Nullable |
| private PsiResourceFile findResourceFile(String dirName, String fileName) { |
| int index = dirName.indexOf('-'); |
| String qualifiers; |
| String folderTypeName; |
| if (index == -1) { |
| qualifiers = ""; |
| folderTypeName = dirName; |
| } else { |
| qualifiers = dirName.substring(index + 1); |
| folderTypeName = dirName.substring(0, index); |
| } |
| ResourceFolderType folderType = ResourceFolderType.getTypeByName(folderTypeName); |
| |
| for (PsiResourceFile file : myResourceFiles.values()) { |
| String name = file.getName(); |
| if (folderType == file.getFolderType() && fileName.equals(name) && qualifiers.equals(file.getQualifiers())) { |
| return file; |
| } |
| } |
| |
| return null; |
| } |
| |
| private void removeItemsFromFile(PsiResourceFile resourceFile) { |
| for (ResourceItem item : resourceFile) { |
| removeItems(resourceFile, item.getType(), item.getName(), false); // no need since we're discarding the file |
| } |
| } |
| |
| private static boolean isItemElement(XmlTag xmlTag) { |
| String tag = xmlTag.getName(); |
| if (tag.equals(TAG_RESOURCES)) { |
| return false; |
| } |
| return tag.equals(TAG_ITEM) || ResourceType.getEnum(tag) != null; |
| } |
| |
| @Nullable |
| private ResourceItem findValueResourceItem(XmlTag tag, PsiFile file) { |
| if (!tag.isValid()) { |
| PsiResourceFile resourceFile = myResourceFiles.get(file); |
| if (resourceFile != null) { |
| for (ResourceItem item : resourceFile) { |
| PsiResourceItem pri = (PsiResourceItem)item; |
| XmlTag xmlTag = pri.getTag(); |
| if (xmlTag == tag) { |
| return item; |
| } |
| } |
| } |
| return null; |
| } |
| String name = tag.getAttributeValue(ATTR_NAME); |
| return name != null ? findValueResourceItem(tag, file, name) : null; |
| } |
| |
| @Nullable |
| private ResourceItem findValueResourceItem(XmlTag tag, PsiFile file, String name) { |
| ResourceType type = getType(tag); |
| return findResourceItem(type, file, name, tag); |
| } |
| |
| @Nullable |
| private ResourceItem findResourceItem(@Nullable ResourceType type, @Nullable PsiFile file, @Nullable String name, @Nullable XmlTag tag) { |
| if (type != null && name != null) { |
| ListMultimap<String, ResourceItem> map = myItems.get(type); |
| if (map != null) { |
| List<ResourceItem> items = map.get(name); |
| assert items != null; |
| if (tag != null) { |
| for (ResourceItem item : items) { |
| assert item instanceof PsiResourceItem; |
| PsiResourceItem psiItem = (PsiResourceItem)item; |
| if (psiItem.getTag() == tag) { |
| return item; |
| } |
| } |
| } |
| |
| for (ResourceItem item : items) { |
| assert item instanceof PsiResourceItem; |
| PsiResourceItem psiItem = (PsiResourceItem)item; |
| PsiFile virtualFile = psiItem.getPsiFile(); |
| if (virtualFile == file) { |
| return item; |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| // For debugging only |
| @Override |
| public String toString() { |
| return getClass().getSimpleName() + " for " + myResourceDir + ": @" + Integer.toHexString(System.identityHashCode(this)); |
| } |
| } |