public void childReplaced()

in android/src/com/android/tools/idea/res/ResourceFolderRepository.java [1431:1809]


    public void childReplaced(@NotNull PsiTreeChangeEvent event) {
      ResourceUpdateTracer.log(() -> getSimpleId(this) + ".childReplaced " + pathForLogging(event.getFile()));
      try {
        PsiFile psiFile = event.getFile();
        if (psiFile != null) {
          VirtualFile virtualFile = psiFile.getVirtualFile();
          // If the file is currently being scanned, schedule a new scan to avoid a race condition
          // between the incremental update and the running scan.
          if (rescheduleScanIfRunning(virtualFile)) {
            return;
          }

          // This method is called when you edit within a file.
          if (isRelevantFile(virtualFile)) {
            // 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 = ResourceFilesUtil.getFolderType(virtualFile);
            if (folderType != null && FolderTypeRelationship.isIdGeneratingFolderType(folderType) &&
                psiFile.getFileType() == XmlFileType.INSTANCE) {
              // 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();  // Same as event.getNewChild() for a childReplaced event.
              if (parent instanceof XmlText || child instanceof XmlText || parent instanceof XmlComment) {
                return;
              }
              PsiElement oldChild = event.getOldChild();
              if (child instanceof XmlComment && oldChild instanceof XmlComment) {
                // Checking for child to be a comment is not sufficient, because the old child may
                // have been an XmlElement with an ID that's been removed. This can happen is the
                // user selects an XML tag and uses an action to comment out the whole thing.
                // Only if both the old and new child are comments can a scan be skipped.
                return;
              }
              if (parent instanceof XmlElement && child instanceof XmlElement) {
                if (child instanceof XmlComment || oldChild instanceof XmlComment) {
                  // We know from the check above that not both of these children are comments; so
                  // the child has either been commented or uncommented. Since the non-comment
                  // element may include IDs, schedule a rescan.
                  scheduleScan(virtualFile, folderType);
                  return;
                }
                if (event.getOldChild() == event.getNewChild()) {
                  // We're not getting accurate PSI information: we have to do a full file scan.
                  scheduleScan(virtualFile, folderType);
                  return;
                }
                if (child instanceof XmlAttributeValue) {
                  assert parent instanceof XmlAttribute : parent;
                  XmlAttribute attribute = (XmlAttribute)parent;

                  PsiElement newChild = event.getNewChild();
                  if (oldChild instanceof XmlAttributeValue && newChild instanceof XmlAttributeValue) {
                    String oldText = ((XmlAttributeValue)oldChild).getValue().trim();
                    String newText = ((XmlAttributeValue)newChild).getValue().trim();
                    if (oldText.startsWith(NEW_ID_PREFIX) || newText.startsWith(NEW_ID_PREFIX)) {
                      ResourceItemSource<?> resourceFile = mySources.get(psiFile.getVirtualFile());
                      if (!(resourceFile instanceof PsiResourceFile)) {
                        scheduleScan(virtualFile, folderType);
                        return;
                      }

                      ResourceUrl oldResourceUrl = ResourceUrl.parse(oldText);
                      ResourceUrl newResourceUrl = ResourceUrl.parse(newText);

                      // Make sure to compare name as well as urlType, e.g. if both have @+id or not.
                      if (Objects.equals(oldResourceUrl, newResourceUrl)) {
                        // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc).
                        return;
                      }

                      XmlTag xmlTag = attribute.getParent();
                      scheduleUpdate(() -> {
                        if (!xmlTag.isValid()) {
                          scan(psiFile, folderType);
                          return;
                        }
                        Map<ResourceType, ListMultimap<String, ResourceItem>> result = new HashMap<>();
                        ArrayList<PsiResourceItem> items = new ArrayList<>();
                        addIds(xmlTag, items, result);
                        synchronized (ITEM_MAP_LOCK) {
                          PsiResourceFile psiResourceFile = (PsiResourceFile)resourceFile;
                          removeItemsForTag(psiResourceFile, xmlTag, ResourceType.ID);
                          for (PsiResourceItem item : items) {
                            psiResourceFile.addItem(item);
                          }
                          commitToRepositoryWithoutLock(result);
                          setModificationCount(ourModificationCounter.incrementAndGet());
                        }
                      });

                      return;
                    }
                  }
                }
                else if (parent instanceof XmlAttributeValue) {
                  PsiElement grandParent = parent.getParent();
                  if (grandParent 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;
                  }
                  assert grandParent instanceof XmlAttribute : parent;
                  XmlAttribute attribute = (XmlAttribute)grandParent;
                  XmlTag xmlTag = attribute.getParent();
                  String oldText = StringUtil.notNullize(event.getOldChild().getText()).trim();
                  String newText = StringUtil.notNullize(event.getNewChild().getText()).trim();
                  ResourceUpdateTracer.log(() -> getSimpleId(this) + ".childReplaced " + pathForLogging(event.getFile()) +
                                                 " oldText: \"" + oldText + "\" newText: \"" + newText + "\"");
                  if (oldText.startsWith(NEW_ID_PREFIX) || newText.startsWith(NEW_ID_PREFIX)) {
                    ResourceItemSource<?> resourceFile = mySources.get(psiFile.getVirtualFile());
                    if (!(resourceFile instanceof PsiResourceFile)) {
                      scheduleScan(virtualFile, folderType);
                      return;
                    }

                    ResourceUrl oldResourceUrl = ResourceUrl.parse(oldText);
                    ResourceUrl newResourceUrl = ResourceUrl.parse(newText);

                    // Make sure to compare name as well as urlType, e.g. if both have @+id or not.
                    if (Objects.equals(oldResourceUrl, newResourceUrl)) {
                      // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc).
                      return;
                    }

                    scheduleUpdate(() -> {
                      if (!xmlTag.isValid()) {
                        scan(psiFile, folderType);
                        return;
                      }
                      Map<ResourceType, ListMultimap<String, ResourceItem>> result = new HashMap<>();
                      ArrayList<PsiResourceItem> items = new ArrayList<>();
                      addIds(xmlTag, items, result);
                      synchronized (ITEM_MAP_LOCK) {
                        PsiResourceFile psiResourceFile = (PsiResourceFile)resourceFile;
                        removeItemsForTag(psiResourceFile, xmlTag, ResourceType.ID);
                        commitToRepository(result);
                        for (PsiResourceItem item : items) {
                          psiResourceFile.addItem(item);
                        }
                        setModificationCount(ourModificationCounter.incrementAndGet());
                        invalidateParentCaches(ResourceFolderRepository.this, ResourceType.ID);
                      }
                    });

                    return;
                  }
                }
                // This is an XML change within an ID generating folder to something that it's not an ID. While we do not need
                // to generate the ID, we need to notify that something relevant has changed.
                // One example of this change would be an edit to a drawable.
                setModificationCount(ourModificationCounter.incrementAndGet());
                return;
              }

              // TODO: Handle adding/removing elements in layouts incrementally.

              scheduleScan(virtualFile, folderType);
            }
            else if (folderType == VALUES) {
              // This is a folder that *may* contain XML files. Check if this is a relevant XML edit.
              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.

                if (parent instanceof XmlComment) {
                  // Nothing to do
                  return;
                }

                // 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 (getResourceTypeForResourceTag(parentTag) != null) {
                    if (convertToPsiIfNeeded(psiFile, folderType)) {
                      return;
                    }
                    // Yes just invalidate the corresponding cached value.
                    ResourceItem resourceItem = findValueResourceItem(parentTag, psiFile);
                    if (resourceItem instanceof PsiResourceItem) {
                      if (((PsiResourceItem)resourceItem).recomputeValue()) {
                        setModificationCount(ourModificationCounter.incrementAndGet());
                      }
                      ResourceUpdateTracer.log(() -> getSimpleId(this) + ".childReplaced " + pathForLogging(event.getFile()) +
                                                     " recomputed: " + resourceItem);
                      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;
                }

                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 = getResourceTypeForResourceTag(xmlTag);
                    if (type != null) {
                      String oldName = event.getOldChild().getText();
                      String newName = event.getNewChild().getText();
                      ResourceUpdateTracer.log(() -> getSimpleId(this) + ".childReplaced " + pathForLogging(event.getFile()) +
                                                     " oldName: \"" + oldName + "\" newName: \"" + newName + "\"");
                      if (oldName.equals(newName)) {
                        // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc).
                        return;
                      }
                      // findResourceItem depends on PSI in some cases, so we need to bail and rescan if not PSI.
                      if (convertToPsiIfNeeded(psiFile, folderType)) {
                        return;
                      }

                      scheduleUpdate(() -> {
                        if (!xmlTag.isValid()) {
                          scan(psiFile, folderType);
                          return;
                        }
                        ResourceItem item = findResourceItem(type, psiFile, oldName, xmlTag);
                        if (item == null && isValidValueResourceName(oldName)) {
                          scan(psiFile, folderType);
                          return;
                        }

                        synchronized (ITEM_MAP_LOCK) {
                          ListMultimap<String, ResourceItem> items = myResourceTable.get(type);
                          if (items == null) {
                            scan(psiFile, folderType);
                            return;
                          }

                          if (item != null) {
                            // Found the relevant item: delete it and create a new one in a new location.
                            items.remove(oldName, item);
                          }

                          if (isValidValueResourceName(newName)) {
                            PsiResourceItem newItem = PsiResourceItem.forXmlTag(newName, type, ResourceFolderRepository.this, xmlTag);
                            items.put(newName, newItem);
                            ResourceItemSource<?> resourceFile = mySources.get(psiFile.getVirtualFile());
                            if (resourceFile != null) {
                              PsiResourceFile psiResourceFile = (PsiResourceFile)resourceFile;
                              if (item != null) {
                                psiResourceFile.removeItem((PsiResourceItem)item);
                              }
                              psiResourceFile.addItem(newItem);
                            }
                            else {
                              assert false : item;
                            }
                          }
                          setModificationCount(ourModificationCounter.incrementAndGet());
                          invalidateParentCaches(ResourceFolderRepository.this, type);
                        }

                        // Invalidate surrounding declare styleable if any.
                        if (type == ResourceType.ATTR) {
                          XmlTag parentTag = xmlTag.getParentTag();
                          if (parentTag != null && getResourceTypeForResourceTag(parentTag) == ResourceType.STYLEABLE) {
                            ResourceItem style = findValueResourceItem(parentTag, psiFile);
                            if (style instanceof PsiResourceItem) {
                              ((PsiResourceItem)style).recomputeValue();
                            }
                            ResourceUpdateTracer.log(() -> getSimpleId(this) + ".childReplaced " + pathForLogging(event.getFile()) +
                                                           " recomputed: " + style);
                          }
                        }
                      });

                      return;
                    }
                    else {
                      XmlTag parentTag = xmlTag.getParentTag();
                      if (parentTag != null && getResourceTypeForResourceTag(parentTag) != 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.
                        if (convertToPsiIfNeeded(psiFile, folderType)) {
                          return;
                        }
                        ResourceItem resourceItem = findValueResourceItem(parentTag, psiFile);
                        if (resourceItem instanceof PsiResourceItem) {
                          if (((PsiResourceItem)resourceItem).recomputeValue()) {
                            setModificationCount(ourModificationCounter.incrementAndGet());
                          }
                          ResourceUpdateTracer.log(() -> getSimpleId(this) + ".childReplaced " + pathForLogging(event.getFile()) +
                                                         " recomputed: " + resourceItem);
                          return;
                        }
                      }
                    }
                  }
                }
              }

              // Fall through: We were not able to directly manipulate the repository to accommodate
              // the edit, so re-scan the whole value file instead.
              scheduleScan(virtualFile, folderType);
            }
            else if (folderType == COLOR) {
              PsiElement parent = event.getParent();
              if (parent instanceof XmlElement) {
                if (parent instanceof XmlComment) {
                  return; // Nothing to do.
                }

                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;
                  }
                }

                setModificationCount(ourModificationCounter.incrementAndGet());
                return;
              }
            }
            else if (folderType == FONT) {
              clearFontCache(psiFile.getVirtualFile());
            }
            else if (folderType != null) {
              PsiElement parent = event.getParent();

              if (parent instanceof XmlElement) {
                if (parent instanceof XmlComment) {
                  return; // Nothing to do.
                }

                // A change to an XML file that does not require adding/removing resources.
                // This could be a change to the contents of an XML file in the raw folder.
                setModificationCount(ourModificationCounter.incrementAndGet());
              }
            } // else: can ignore this edit.
          }
        }

        myIgnoreChildrenChanged = true;
      }
      finally {
        ResourceUpdateTracer.log(() -> getSimpleId(this) + ".childReplaced " + pathForLogging(event.getFile()) + " end");
      }
    }