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