From dd4e91f4935f62b610a2c53f9fa49d4d0c8a991f Mon Sep 17 00:00:00 2001 From: Piotr Tomiak Date: Thu, 28 May 2020 11:46:18 +0200 Subject: [PATCH] WEB-32876 WEB-36410 Support formatting of multiple injections within single XmlText element. GitOrigin-RevId: 217283985c2e6d210f04c4f1e9f8d950181ee81e --- .../common/InjectedLanguageBlockBuilder.java | 91 ++++++------ xml/impl/intellij.xml.impl.iml | 1 + .../psi/formatter/xml/AbstractXmlBlock.java | 31 ++-- .../xml/XmlInjectedLanguageBlockBuilder.java | 5 +- .../psi/formatter/xml/XmlTagBlock.java | 134 ++++++++++++++++-- .../template/formatter/TemplateXmlBlock.java | 8 +- .../formatter/TemplateXmlTagBlock.java | 8 +- 7 files changed, 211 insertions(+), 67 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/psi/formatter/common/InjectedLanguageBlockBuilder.java b/platform/lang-impl/src/com/intellij/psi/formatter/common/InjectedLanguageBlockBuilder.java index e8bc2b7e5688..4f8aabf812bb 100644 --- a/platform/lang-impl/src/com/intellij/psi/formatter/common/InjectedLanguageBlockBuilder.java +++ b/platform/lang-impl/src/com/intellij/psi/formatter/common/InjectedLanguageBlockBuilder.java @@ -55,24 +55,20 @@ public abstract class InjectedLanguageBlockBuilder { public abstract Block createBlockAfterInjection(ASTNode node, Wrap wrap, Alignment alignment, Indent indent, TextRange range); public boolean addInjectedBlocks(List result, final ASTNode injectionHost, Wrap wrap, Alignment alignment, Indent indent) { - final PsiFile[] injectedFile = new PsiFile[1]; - final Ref injectedRangeInsideHost = new Ref<>(); - final Ref prefixLength = new Ref<>(); - final Ref suffixLength = new Ref<>(); - final Ref injectionHostToUse = new Ref<>(injectionHost); + final Ref lastInjectionEndOffset = new Ref<>(0); final PsiLanguageInjectionHost.InjectedPsiVisitor injectedPsiVisitor = (injectedPsi, places) -> { if (places.size() != 1) { return; } final PsiLanguageInjectionHost.Shred shred = places.get(0); - TextRange textRange = shred.getRangeInsideHost(); + TextRange injectionRange = shred.getRangeInsideHost(); PsiLanguageInjectionHost shredHost = shred.getHost(); if (shredHost == null) { return; } ASTNode node = shredHost.getNode(); - if (node == null || !injectionHost.getTextRange().contains(textRange.shiftRight(node.getStartOffset()))) { + if (node == null || !injectionHost.getTextRange().contains(injectionRange.shiftRight(node.getStartOffset()))) { return; } if (node != injectionHost) { @@ -81,7 +77,7 @@ public abstract class InjectedLanguageBlockBuilder { for (ASTNode n = injectionHost.getTreeParent(), prev = injectionHost; n != null; prev = n, n = n.getTreeParent()) { shift += n.getStartOffset() - prev.getStartOffset(); if (n == node) { - textRange = textRange.shiftRight(shift); + injectionRange = injectionRange.shiftRight(shift); canProcess = true; break; } @@ -92,47 +88,60 @@ public abstract class InjectedLanguageBlockBuilder { } String childText; - if (injectionHost.getTextLength() == textRange.getEndOffset() && textRange.getStartOffset() == 0 || - canProcessFragment((childText = injectionHost.getText()).substring(0, textRange.getStartOffset()), injectionHost) && - canProcessFragment(childText.substring(textRange.getEndOffset()), injectionHost)) { - injectedFile[0] = injectedPsi; - injectedRangeInsideHost.set(textRange); - prefixLength.set(shred.getPrefix().length()); - suffixLength.set(shred.getSuffix().length()); + if (injectionHost.getTextLength() == injectionRange.getEndOffset() && injectionRange.getStartOffset() == 0 || + canProcessFragment((childText = injectionHost.getText()).substring(0, injectionRange.getStartOffset()), injectionHost) && + canProcessFragment(childText.substring(injectionRange.getEndOffset()), injectionHost)) { + + // inject language block + + final Language childLanguage = injectedPsi.getLanguage(); + final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(childLanguage, injectionHost.getPsi()); + + if (builder != null) { + final int startOffset = injectionRange.getStartOffset(); + final int endOffset = injectionRange.getEndOffset(); + TextRange range = injectionHost.getTextRange(); + final int prefixLength = shred.getPrefix().length(); + final int suffixLength = shred.getSuffix().length(); + + int childOffset = range.getStartOffset(); + if (lastInjectionEndOffset.get() < startOffset) { + result.add(createBlock(injectionHost, wrap, alignment, indent, new TextRange(lastInjectionEndOffset.get(), startOffset))); + } + + addInjectedLanguageBlockWrapper(result, injectedPsi.getNode(), indent, childOffset + startOffset, + new TextRange(prefixLength, injectedPsi.getTextLength() - suffixLength)); + + lastInjectionEndOffset.set(endOffset); } + } }; final PsiElement injectionHostPsi = injectionHost.getPsi(); PsiFile containingFile = injectionHostPsi.getContainingFile(); - InjectedLanguageManager.getInstance(containingFile.getProject()).enumerateEx(injectionHostPsi, containingFile, true, injectedPsiVisitor); + InjectedLanguageManager.getInstance(containingFile.getProject()) + .enumerateEx(injectionHostPsi, containingFile, true, injectedPsiVisitor); - if (injectedFile[0] != null) { - final Language childLanguage = injectedFile[0].getLanguage(); - final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(childLanguage, injectionHostPsi); - - if (builder != null) { - final int startOffset = injectedRangeInsideHost.get().getStartOffset(); - final int endOffset = injectedRangeInsideHost.get().getEndOffset(); - TextRange range = injectionHostToUse.get().getTextRange(); - - int childOffset = range.getStartOffset(); - if (startOffset != 0) { - final ASTNode leaf = injectionHostToUse.get().findLeafElementAt(startOffset - 1); - result.add(createBlockBeforeInjection(leaf, wrap, alignment, indent, new TextRange(childOffset, childOffset + startOffset))); - } - - addInjectedLanguageBlockWrapper(result, injectedFile[0].getNode(), indent, childOffset + startOffset, - new TextRange(prefixLength.get(), injectedFile[0].getTextLength() - suffixLength.get())); - - if (endOffset != injectionHostToUse.get().getTextLength()) { - final ASTNode leaf = injectionHostToUse.get().findLeafElementAt(endOffset); - result.add(createBlockAfterInjection(leaf, wrap, alignment, indent, new TextRange(childOffset + endOffset, range.getEndOffset()))); - } - return true; + if (lastInjectionEndOffset.get() > 0) { + if (lastInjectionEndOffset.get() < injectionHost.getTextLength()) { + result.add(createBlock(injectionHost, wrap, alignment, indent, + new TextRange(lastInjectionEndOffset.get(), injectionHost.getTextLength()))); } + return true; } return false; } + private Block createBlock(ASTNode injectionHost, Wrap wrap, Alignment alignment, Indent indent, TextRange range) { + if (range.getStartOffset() == 0) { + final ASTNode leaf = injectionHost.findLeafElementAt(range.getEndOffset() - 1); + return createBlockBeforeInjection( + leaf, wrap, alignment, indent, range.shiftRight(injectionHost.getStartOffset())); + } + final ASTNode leaf = injectionHost.findLeafElementAt(range.getStartOffset()); + return createBlockAfterInjection( + leaf, wrap, alignment, indent, range.shiftRight(injectionHost.getStartOffset())); + } + public void addInjectedLanguageBlockWrapper(final List result, final ASTNode injectedNode, final Indent indent, int offset, @Nullable TextRange range) { @@ -141,11 +150,11 @@ public abstract class InjectedLanguageBlockBuilder { // if (range != null) { if (range.getLength() == 0) return; - if(StringUtil.isEmptyOrSpaces(range.substring(injectedNode.getText()))) { + if (StringUtil.isEmptyOrSpaces(range.substring(injectedNode.getText()))) { return; } } - + final PsiElement childPsi = injectedNode.getPsi(); final Language childLanguage = childPsi.getLanguage(); final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(childLanguage, childPsi); diff --git a/xml/impl/intellij.xml.impl.iml b/xml/impl/intellij.xml.impl.iml index 94959436ad00..f8220219ca78 100644 --- a/xml/impl/intellij.xml.impl.iml +++ b/xml/impl/intellij.xml.impl.iml @@ -36,6 +36,7 @@ + diff --git a/xml/impl/src/com/intellij/psi/formatter/xml/AbstractXmlBlock.java b/xml/impl/src/com/intellij/psi/formatter/xml/AbstractXmlBlock.java index 1c277dcb399a..8fd887e622f0 100644 --- a/xml/impl/src/com/intellij/psi/formatter/xml/AbstractXmlBlock.java +++ b/xml/impl/src/com/intellij/psi/formatter/xml/AbstractXmlBlock.java @@ -22,6 +22,7 @@ import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; import com.intellij.psi.xml.*; import com.intellij.util.containers.JBIterable; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -63,7 +64,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock { * * @return True if the space must be preserved (xml:space='preserve'), false if the attribute * contains 'default'. If the attribute is not defined, return the current value. - */ + */ private static boolean shouldPreserveSpace(ASTNode node, boolean defaultValue) { if (node.getPsi() instanceof XmlTag) { XmlTag tag = (XmlTag)node.getPsi(); @@ -82,7 +83,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock { } return defaultValue; } - + public boolean isPreserveSpace() { return myPreserveSpace; } @@ -207,7 +208,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock { else if (!isBuildIndentsOnly()) { myInjectedBlockBuilder.addInjectedLanguageBlockWrapper(result, child, indent, 0, null); } - + return child; } @@ -279,13 +280,25 @@ public abstract class AbstractXmlBlock extends AbstractBlock { ); } else { - result.add(createSimpleChild(child, indent, wrap, alignment)); + result.add(createSimpleChild(child, indent, wrap, alignment, null)); } } - + /** @deprecated use and override {@code createSimpleChild } overload with {@code range } provided */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "2020.3") protected XmlBlock createSimpleChild(final ASTNode child, final Indent indent, final Wrap wrap, final Alignment alignment) { - return new XmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, null, isPreserveSpace()); + return null; + } + + protected @NotNull XmlBlock createSimpleChild(@NotNull ASTNode child, @Nullable Indent indent, + @Nullable Wrap wrap, @Nullable Alignment alignment, @Nullable TextRange range) { + XmlBlock blockFromDeprecatedCall = createSimpleChild(child, indent, wrap, alignment); + if (blockFromDeprecatedCall != null) { + return blockFromDeprecatedCall; + } + return new XmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, range, isPreserveSpace()); } protected XmlTagBlock createTagBlock(final ASTNode child, final Indent indent, final Wrap wrap, final Alignment alignment) { @@ -294,7 +307,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock { @Nullable protected XmlTag findXmlTagAt(final ASTNode child, final int startOffset) { - return null; + return null; } @Nullable @@ -351,7 +364,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock { } public abstract boolean insertLineBreakBeforeTag(); - + public int getBlankLinesBeforeTag() { return insertLineBreakBeforeTag() ? 1 : 0; } @@ -416,7 +429,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock { protected boolean buildInjectedPsiBlocks(List result, final ASTNode child, Wrap wrap, Alignment alignment, Indent indent) { if (isBuildIndentsOnly()) return false; - + if (myInjectedBlockBuilder.addInjectedBlocks(result, child, wrap, alignment, indent)) { return true; } diff --git a/xml/impl/src/com/intellij/psi/formatter/xml/XmlInjectedLanguageBlockBuilder.java b/xml/impl/src/com/intellij/psi/formatter/xml/XmlInjectedLanguageBlockBuilder.java index 2829dbc58d0a..2372544f859d 100644 --- a/xml/impl/src/com/intellij/psi/formatter/xml/XmlInjectedLanguageBlockBuilder.java +++ b/xml/impl/src/com/intellij/psi/formatter/xml/XmlInjectedLanguageBlockBuilder.java @@ -66,14 +66,11 @@ public class XmlInjectedLanguageBlockBuilder extends InjectedLanguageBlockBuilde public boolean canProcessFragment(String text, final ASTNode injectionHost) { IElementType type = injectionHost.getElementType(); if (type == XmlElementType.XML_TEXT) { - text = text.trim(); - text = text.replace("", ""); + return true; } else if (type == XmlElementType.XML_COMMENT) { // of conditional comments injection return true; } - return text.isEmpty(); } diff --git a/xml/impl/src/com/intellij/psi/formatter/xml/XmlTagBlock.java b/xml/impl/src/com/intellij/psi/formatter/xml/XmlTagBlock.java index 1282d560a180..23352588bfa0 100644 --- a/xml/impl/src/com/intellij/psi/formatter/xml/XmlTagBlock.java +++ b/xml/impl/src/com/intellij/psi/formatter/xml/XmlTagBlock.java @@ -17,17 +17,23 @@ package com.intellij.psi.formatter.xml; import com.intellij.formatting.*; import com.intellij.lang.ASTNode; -import com.intellij.psi.PsiElement; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.tree.IElementType; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTokenType; +import com.intellij.util.SmartList; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import static com.intellij.util.ObjectUtils.notNull; +import static com.intellij.util.containers.ContainerUtil.addIfNotNull; + public class XmlTagBlock extends AbstractXmlBlock{ private final Indent myIndent; @@ -162,17 +168,21 @@ public class XmlTagBlock extends AbstractXmlBlock{ @Override @Nullable - protected - ASTNode processChild(List result, final ASTNode child, final Wrap wrap, final Alignment alignment, final Indent indent) { + protected ASTNode processChild(List result, final ASTNode child, final Wrap wrap, final Alignment alignment, final Indent indent) { IElementType type = child.getElementType(); if (type == XmlElementType.XML_TEXT) { - final PsiElement parent = child.getPsi().getParent(); - - if (parent instanceof XmlTag && ((XmlTag)parent).getSubTags().length == 0) { - if (buildInjectedPsiBlocks(result, child, wrap, alignment, indent)) return child; + List injections = new SmartList<>(); + if (buildInjectedPsiBlocks(injections, child, wrap, alignment, indent)) { + List regular = new SmartList<>(); + createXmlTextBlocks(regular, child, wrap, alignment); + splitRegularBlocksWithInjected(child, result, injections, regular); + return child; } - return createXmlTextBlocks(result, child, wrap, alignment); - } else if (type == XmlElementType.XML_COMMENT) { + else { + return createXmlTextBlocks(result, child, wrap, alignment); + } + } + else if (type == XmlElementType.XML_COMMENT) { if (buildInjectedPsiBlocks(result, child, wrap, alignment, indent)) return child; return super.processChild(result, child, wrap, alignment, indent); } @@ -181,6 +191,112 @@ public class XmlTagBlock extends AbstractXmlBlock{ } } + private void splitRegularBlocksWithInjected(@NotNull ASTNode injectionHost, @NotNull List result, + @NotNull List withInjections, @NotNull List regularBlocks) { + int i = 0; + int j = 0; + int injectionHostOffset = injectionHost.getStartOffset(); + // Since there are possible injections without formatter blocks (i.e. whitespace only block), + // we need to detect such scenarios and correctly split as if there was an injection. + List injectedRanges = new ArrayList<>(withInjections.size()); + Int2ObjectMap injectedBlocksMap = new Int2ObjectOpenHashMap<>(); + boolean lastInjected = true; + int lastOffset = 0; + for (Block block: withInjections) { + TextRange range = block.getTextRange(); + if (block instanceof AnotherLanguageBlockWrapper) { + injectedRanges.add(range); + injectedBlocksMap.put(range.getStartOffset(), block); + lastInjected = true; + } else { + if (!lastInjected) { + // add range for empty injection + int offset = range.getStartOffset(); + injectedRanges.add(new TextRange(lastOffset, offset)); + } + lastOffset = range.getEndOffset(); + lastInjected = false; + } + } + // Perform actual splitting + int injectedRangesCount = injectedRanges.size(); + while (i < regularBlocks.size()) { + Block reg = regularBlocks.get(i); + if (j < injectedRangesCount) { + TextRange injRange = injectedRanges.get(j); + TextRange regRange = reg.getTextRange(); + if (regRange.getEndOffset() <= injRange.getStartOffset()) { + // Regular block does not intersect with injected - add + result.add(reg); + i++; + } + else if (injRange.getStartOffset() <= regRange.getStartOffset() + && regRange.getEndOffset() <= injRange.getEndOffset()) { + // Regular block completely within injected - skip + i++; + } + else { + if (regRange.getStartOffset() < injRange.getStartOffset()) { + // Regular block ends within or after an injected - split + ASTNode node = notNull(injectionHost.findLeafElementAt(injRange.getStartOffset() - 1 - injectionHostOffset), injectionHost); + result.add(createSimpleChild(node, reg.getIndent(), reg.getWrap(), reg.getAlignment(), new TextRange( + regRange.getStartOffset(), injRange.getStartOffset()))); + if (regRange.getEndOffset() <= injRange.getEndOffset()) { + // Block ends within injected - move to the next block + i++; + continue; + } + // Case of a regular block spanning over multiple injected ones + } + // Add injected block to the result + addIfNotNull(result, injectedBlocksMap.get(injRange.getStartOffset())); + j++; + if (regRange.getStartOffset() < injRange.getEndOffset()) { + // we have a regular block starting within or before last added injected block - split + int lastInjection = injRange.getEndOffset(); + while (j < injectedRangesCount) { + // check if single block does not span over next injected block + TextRange nextRange = injectedRanges.get(j); + if (nextRange.getStartOffset() < regRange.getEndOffset()) { + // out regular block ends within or after the next injected block - add a split if it's not empty + if (lastInjection < nextRange.getStartOffset()) { + ASTNode node = notNull(injectionHost.findLeafElementAt(lastInjection - injectionHostOffset), injectionHost); + result.add(createSimpleChild(node, reg.getIndent(), reg.getWrap(), reg.getAlignment(), + new TextRange(lastInjection, nextRange.getStartOffset()))); + } + lastInjection = nextRange.getEndOffset(); + if (lastInjection <= regRange.getEndOffset()) { + // Add current injected block and repeat if regular block ends after it + addIfNotNull(result, injectedBlocksMap.get(nextRange.getStartOffset())); + j++; + continue; + } + } + break; + } + // We might have some leftover of regular block after last added injected one, + // which does not end within or after the next injected block - add a split + if (lastInjection < regRange.getEndOffset()) { + ASTNode node = notNull(injectionHost.findLeafElementAt(lastInjection - injectionHostOffset), injectionHost); + result.add(createSimpleChild(node, reg.getIndent(), reg.getWrap(), reg.getAlignment(), + new TextRange(lastInjection, regRange.getEndOffset()))); + } + i++; + } + } + } + else { + // No more injected blocks to process, just add the regular ones + result.add(reg); + i++; + } + } + // Add any leftover injected blocks + while (j < injectedRangesCount) { + addIfNotNull(result, injectedBlocksMap.get(injectedRanges.get(j++).getStartOffset())); + } + } + protected Indent getChildrenIndent() { return myXmlFormattingPolicy.indentChildrenOf(getTag()) ? Indent.getNormalIndent() diff --git a/xml/impl/src/com/intellij/xml/template/formatter/TemplateXmlBlock.java b/xml/impl/src/com/intellij/xml/template/formatter/TemplateXmlBlock.java index 4b2537952b35..088a4529b737 100644 --- a/xml/impl/src/com/intellij/xml/template/formatter/TemplateXmlBlock.java +++ b/xml/impl/src/com/intellij/xml/template/formatter/TemplateXmlBlock.java @@ -23,10 +23,13 @@ import com.intellij.psi.formatter.xml.*; import com.intellij.psi.templateLanguages.OuterLanguageElement; import com.intellij.psi.xml.XmlElementType; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import static com.intellij.util.ObjectUtils.notNull; + public class TemplateXmlBlock extends XmlBlock implements IndentInheritingBlock { private final AbstractXmlTemplateFormattingModelBuilder myBuilder; private Indent myIndent; @@ -45,8 +48,9 @@ public class TemplateXmlBlock extends XmlBlock implements IndentInheritingBlock } @Override - protected XmlBlock createSimpleChild(ASTNode child, Indent indent, Wrap wrap, Alignment alignment) { - return myBuilder.createXmlBlock(child, wrap, alignment, myXmlFormattingPolicy,indent, child.getTextRange()); + protected @NotNull XmlBlock createSimpleChild(@NotNull ASTNode child, @Nullable Indent indent, + @Nullable Wrap wrap, @Nullable Alignment alignment, @Nullable TextRange range) { + return myBuilder.createXmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, notNull(range, child.getTextRange())); } @Override diff --git a/xml/impl/src/com/intellij/xml/template/formatter/TemplateXmlTagBlock.java b/xml/impl/src/com/intellij/xml/template/formatter/TemplateXmlTagBlock.java index 955bbe713391..4654a5e37040 100644 --- a/xml/impl/src/com/intellij/xml/template/formatter/TemplateXmlTagBlock.java +++ b/xml/impl/src/com/intellij/xml/template/formatter/TemplateXmlTagBlock.java @@ -20,10 +20,13 @@ import com.intellij.formatting.Block; import com.intellij.formatting.Indent; import com.intellij.formatting.Wrap; import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.formatter.xml.XmlBlock; import com.intellij.psi.formatter.xml.XmlFormattingPolicy; import com.intellij.psi.formatter.xml.XmlTagBlock; import com.intellij.xml.util.HtmlUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -60,8 +63,9 @@ public class TemplateXmlTagBlock extends XmlTagBlock implements IndentInheriting @Override - protected XmlBlock createSimpleChild(ASTNode child, Indent indent, Wrap wrap, Alignment alignment) { - return myBuilder.createXmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, null); + protected @NotNull XmlBlock createSimpleChild(@NotNull ASTNode child, @Nullable Indent indent, + @Nullable Wrap wrap, @Nullable Alignment alignment, @Nullable TextRange range) { + return myBuilder.createXmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, range); } @Override