IDEA-61055 XML: moving tags up/down wrecks well-formedness

This commit is contained in:
Dmitry Avdeev
2012-09-06 16:10:34 +04:00
parent 925f4e12f1
commit 675529ce18
10 changed files with 322 additions and 223 deletions
@@ -1,212 +1,249 @@
/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* 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.intellij.codeInsight.editorActions.moveUpDown;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.html.HtmlTag;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlText;
import org.jetbrains.annotations.NotNull;
class XmlMover extends LineMover {
//private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.actions.moveUpDown.XmlMover");
public boolean checkAvailable(@NotNull final Editor editor, @NotNull final PsiFile file, @NotNull final MoveInfo info, final boolean down) {
if (!(file instanceof XmlFile)) {
return false;
}
boolean available = super.checkAvailable(editor, file, info, down);
if (!available) return false;
// updated moved range end to cover multiline tag start
final Document document = editor.getDocument();
int movedLineStart = document.getLineStartOffset(info.toMove.startLine);
final int movedLineEnd = document.getLineEndOffset(info.toMove.endLine - 1);
PsiElement movedEndElement = file.findElementAt(movedLineEnd);
if (movedEndElement instanceof PsiWhiteSpace) movedEndElement = PsiTreeUtil.prevLeaf(movedEndElement);
PsiElement movedStartElement = file.findElementAt(movedLineStart);
if (movedStartElement instanceof PsiWhiteSpace) movedStartElement = PsiTreeUtil.nextLeaf(movedStartElement);
if (movedEndElement == null || movedStartElement == null) return false;
final PsiNamedElement namedParentAtEnd = PsiTreeUtil.getParentOfType(movedEndElement, PsiNamedElement.class);
final PsiNamedElement namedParentAtStart = PsiTreeUtil.getParentOfType(movedStartElement, PsiNamedElement.class);
final XmlText text = PsiTreeUtil.getParentOfType(movedStartElement, XmlText.class);
final XmlText text2 = PsiTreeUtil.getParentOfType(movedEndElement, XmlText.class);
// Let's do not care about injections for this mover
if ( ( text != null && InjectedLanguageManager.getInstance(text.getProject()).getInjectedPsiFiles(text) != null) ||
( text2 != null && InjectedLanguageManager.getInstance(text2.getProject()).getInjectedPsiFiles(text2) != null)) {
return false;
}
XmlTag nearestTag = PsiTreeUtil.getParentOfType(movedStartElement, XmlTag.class);
if (nearestTag != null &&
( "script".equals(nearestTag.getLocalName()) ||
(nearestTag instanceof HtmlTag && "script".equalsIgnoreCase(nearestTag.getLocalName()))
)
) {
return false;
}
PsiNamedElement movedParent = null;
if (namedParentAtEnd == namedParentAtStart) movedParent = namedParentAtEnd;
else if (namedParentAtEnd instanceof XmlAttribute && namedParentAtStart instanceof XmlTag && namedParentAtEnd.getParent() == namedParentAtStart) {
movedParent = namedParentAtStart;
} else if (namedParentAtStart instanceof XmlAttribute && namedParentAtEnd instanceof XmlTag && namedParentAtStart.getParent() == namedParentAtEnd) {
movedParent = namedParentAtEnd;
}
if (movedParent == null) {
return false;
}
final TextRange textRange = movedParent.getTextRange();
if (movedParent instanceof XmlTag) {
final XmlTag tag = (XmlTag)movedParent;
final TextRange valueRange = tag.getValue().getTextRange();
final int valueStart = valueRange.getStartOffset();
if (movedLineStart < valueStart && valueStart + 1 < document.getTextLength()) {
movedLineStart = updateMovedRegionEnd(document, movedLineStart, valueStart + 1, info, down);
}
if (movedLineStart < valueStart) {
movedLineStart = updatedMovedRegionStart(document, movedLineStart, tag.getTextRange().getStartOffset(), info, down);
}
} else if (movedParent instanceof XmlAttribute) {
final int endOffset = textRange.getEndOffset() + 1;
if (endOffset < document.getTextLength()) movedLineStart = updateMovedRegionEnd(document, movedLineStart, endOffset, info, down);
movedLineStart = updatedMovedRegionStart(document, movedLineStart, textRange.getStartOffset(), info, down);
}
final TextRange moveDestinationRange = new TextRange(
document.getLineStartOffset(info.toMove2.startLine),
document.getLineStartOffset(info.toMove2.endLine)
);
if (movedParent instanceof XmlAttribute) {
final XmlTag parent = ((XmlAttribute)movedParent).getParent();
if (parent != null) {
final TextRange valueRange = parent.getValue().getTextRange();
// Do not move attributes out of tags
if ( (down && moveDestinationRange.getEndOffset() >= valueRange.getStartOffset()) ||
(!down && moveDestinationRange.getStartOffset() <= parent.getTextRange().getStartOffset())
) {
info.toMove2 = null;
}
}
}
if (down) {
PsiElement updatedElement = file.findElementAt(moveDestinationRange.getEndOffset());
if (updatedElement instanceof PsiWhiteSpace) updatedElement = PsiTreeUtil.prevLeaf(updatedElement);
if (updatedElement != null) {
final PsiNamedElement namedParent = PsiTreeUtil.getParentOfType(updatedElement, movedParent.getClass());
if (namedParent instanceof XmlTag) {
final XmlTag tag = (XmlTag)namedParent;
final int offset = tag.isEmpty() ? tag.getTextRange().getStartOffset() : tag.getValue().getTextRange().getStartOffset();
updatedMovedIntoEnd(document, info, offset);
} else if (namedParent instanceof XmlAttribute) {
updatedMovedIntoEnd(document, info, namedParent.getTextRange().getEndOffset());
}
}
} else {
PsiElement updatedElement = file.findElementAt(moveDestinationRange.getStartOffset());
if (updatedElement instanceof PsiWhiteSpace) updatedElement = PsiTreeUtil.nextLeaf(updatedElement);
if (updatedElement != null) {
final PsiNamedElement namedParent = PsiTreeUtil.getParentOfType(updatedElement, movedParent.getClass());
if (namedParent instanceof XmlTag) {
final XmlTag tag = (XmlTag)namedParent;
final TextRange tagValueRange = tag.getValue().getTextRange();
// We need to update destination range to jump over tag start
final XmlTag[] subtags = tag.getSubTags();
if ((tagValueRange.contains(movedLineStart) && subtags.length > 0 && subtags[0] == movedParent) ||
( tagValueRange.getLength() == 0 && tag.getTextRange().intersects(moveDestinationRange))
) {
final int line = document.getLineNumber(tag.getTextRange().getStartOffset());
final LineRange toMove2 = info.toMove2;
info.toMove2 = new LineRange(Math.min(line, toMove2.startLine), toMove2.endLine);
}
} else if (namedParent instanceof XmlAttribute) {
final int line = document.getLineNumber(namedParent.getTextRange().getStartOffset());
final LineRange toMove2 = info.toMove2;
info.toMove2 = new LineRange(Math.min(line, toMove2.startLine), toMove2.endLine);
}
}
}
return true;
}
private void updatedMovedIntoEnd(final Document document, @NotNull final MoveInfo info, final int offset) {
if (offset + 1 < document.getTextLength()) {
final int line = document.getLineNumber(offset + 1);
final LineRange toMove2 = info.toMove2;
if (toMove2 == null) return;
info.toMove2 = new LineRange(toMove2.startLine, Math.min(Math.max(line, toMove2.endLine), document.getLineCount() - 1));
}
}
private int updatedMovedRegionStart(final Document document, int movedLineStart, final int offset, @NotNull final MoveInfo info, final boolean down) {
final int line = document.getLineNumber(offset);
final LineRange toMove = info.toMove;
int delta = toMove.startLine - line;
info.toMove = new LineRange(Math.min(line, toMove.startLine), toMove.endLine);
// update moved range
if (delta > 0 && !down) {
final LineRange toMove2 = info.toMove2;
info.toMove2 = new LineRange(toMove2.startLine - delta, toMove2.endLine - delta);
movedLineStart = document.getLineStartOffset(toMove.startLine);
}
return movedLineStart;
}
private int updateMovedRegionEnd(final Document document, int movedLineStart, final int valueStart, @NotNull final MoveInfo info, final boolean down) {
final int line = document.getLineNumber(valueStart);
final LineRange toMove = info.toMove;
int delta = line - toMove.endLine;
info.toMove = new LineRange(toMove.startLine, Math.max(line, toMove.endLine));
// update moved range
if (delta > 0 && down) {
final LineRange toMove2 = info.toMove2;
info.toMove2 = new LineRange(toMove2.startLine + delta, Math.min(toMove2.endLine + delta, document.getLineCount() - 1));
movedLineStart = document.getLineStartOffset(toMove.startLine);
}
return movedLineStart;
}
/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* 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.intellij.codeInsight.editorActions.moveUpDown;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.html.HtmlTag;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlText;
import org.jetbrains.annotations.NotNull;
class XmlMover extends LineMover {
//private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.actions.moveUpDown.XmlMover");
public boolean checkAvailable(@NotNull final Editor editor, @NotNull final PsiFile file, @NotNull final MoveInfo info, final boolean down) {
if (!(file instanceof XmlFile)) {
return false;
}
if (!super.checkAvailable(editor, file, info, down)) return false;
// updated moved range end to cover multiline tag start
final Document document = editor.getDocument();
int movedLineStart = document.getLineStartOffset(info.toMove.startLine);
final int movedLineEnd = document.getLineEndOffset(info.toMove.endLine - 1);
PsiElement movedEndElement = file.findElementAt(movedLineEnd);
if (movedEndElement instanceof PsiWhiteSpace) movedEndElement = PsiTreeUtil.prevLeaf(movedEndElement);
PsiElement movedStartElement = file.findElementAt(movedLineStart);
if (movedStartElement instanceof PsiWhiteSpace) movedStartElement = PsiTreeUtil.nextLeaf(movedStartElement);
if (movedEndElement == null || movedStartElement == null) return false;
final PsiNamedElement namedParentAtEnd = PsiTreeUtil.getParentOfType(movedEndElement, PsiNamedElement.class);
final PsiNamedElement namedParentAtStart = PsiTreeUtil.getParentOfType(movedStartElement, PsiNamedElement.class);
if (checkInjections(movedEndElement, movedStartElement)) return false;
XmlTag nearestTag = PsiTreeUtil.getParentOfType(movedStartElement, XmlTag.class);
if (nearestTag != null &&
( "script".equals(nearestTag.getLocalName()) ||
(nearestTag instanceof HtmlTag && "script".equalsIgnoreCase(nearestTag.getLocalName()))
)
) {
return false;
}
PsiNamedElement movedParent = null;
if (namedParentAtEnd == namedParentAtStart) movedParent = namedParentAtEnd;
else if (namedParentAtEnd instanceof XmlAttribute && namedParentAtStart instanceof XmlTag && namedParentAtEnd.getParent() == namedParentAtStart) {
movedParent = namedParentAtStart;
} else if (namedParentAtStart instanceof XmlAttribute && namedParentAtEnd instanceof XmlTag && namedParentAtStart.getParent() == namedParentAtEnd) {
movedParent = namedParentAtEnd;
}
if (movedParent == null) {
return false;
}
final TextRange textRange = movedParent.getTextRange();
if (movedParent instanceof XmlTag) {
final XmlTag tag = (XmlTag)movedParent;
final TextRange valueRange = tag.getValue().getTextRange();
final int valueStart = valueRange.getStartOffset();
if (movedLineStart < valueStart && valueStart + 1 < document.getTextLength()) {
movedLineStart = updateMovedRegionEnd(document, movedLineStart, valueStart + 1, info, down);
}
if (movedLineStart < valueStart) {
movedLineStart = updateMovedRegionStart(document, movedLineStart, tag.getTextRange().getStartOffset(), info, down);
}
} else if (movedParent instanceof XmlAttribute) {
final int endOffset = textRange.getEndOffset() + 1;
if (endOffset < document.getTextLength()) movedLineStart = updateMovedRegionEnd(document, movedLineStart, endOffset, info, down);
movedLineStart = updateMovedRegionStart(document, movedLineStart, textRange.getStartOffset(), info, down);
}
final TextRange moveDestinationRange = new TextRange(
document.getLineStartOffset(info.toMove2.startLine),
document.getLineStartOffset(info.toMove2.endLine) - 1
);
if (movedParent instanceof XmlAttribute) {
final XmlTag parent = ((XmlAttribute)movedParent).getParent();
if (parent != null) {
final TextRange valueRange = parent.getValue().getTextRange();
// Do not move attributes out of tags
if ( (down && moveDestinationRange.getEndOffset() >= valueRange.getStartOffset()) ||
(!down && moveDestinationRange.getStartOffset() <= parent.getTextRange().getStartOffset())
) {
info.toMove2 = null;
}
}
}
if (down) {
PsiElement updatedElement = file.findElementAt(moveDestinationRange.getEndOffset());
if (updatedElement instanceof PsiWhiteSpace) updatedElement = PsiTreeUtil.prevLeaf(updatedElement);
if (updatedElement != null) {
final PsiNamedElement namedParent = PsiTreeUtil.getParentOfType(updatedElement, movedParent.getClass());
if (namedParent instanceof XmlTag) {
if (checkParents(info, movedParent, namedParent, file, document)) return true;
final XmlTag tag = (XmlTag)namedParent;
final int offset = tag.isEmpty() ? tag.getTextRange().getStartOffset() : tag.getValue().getTextRange().getStartOffset();
updatedMovedIntoEnd(document, info, offset);
if (tag.isEmpty()) {
info.toMove2 = new LineRange(namedParent);
}
} else if (namedParent instanceof XmlAttribute) {
updatedMovedIntoEnd(document, info, namedParent.getTextRange().getEndOffset());
}
}
} else {
PsiElement updatedElement = file.findElementAt(moveDestinationRange.getStartOffset());
if (updatedElement instanceof PsiWhiteSpace) updatedElement = PsiTreeUtil.nextLeaf(updatedElement);
if (updatedElement != null) {
final PsiNamedElement namedParent = PsiTreeUtil.getParentOfType(updatedElement, movedParent.getClass());
if (namedParent instanceof XmlTag) {
final XmlTag tag = (XmlTag)namedParent;
final TextRange tagValueRange = tag.getValue().getTextRange();
// We need to update destination range to jump over tag start
final XmlTag[] subtags = tag.getSubTags();
if ((tagValueRange.contains(movedLineStart) && subtags.length > 0 && subtags[0] == movedParent) ||
( tagValueRange.getLength() == 0 && tag.getTextRange().intersects(moveDestinationRange))
) {
final int line = document.getLineNumber(tag.getTextRange().getStartOffset());
final LineRange toMove2 = info.toMove2;
info.toMove2 = new LineRange(Math.min(line, toMove2.startLine), toMove2.endLine);
}
checkParents(info, movedParent, namedParent, file, document);
} else if (namedParent instanceof XmlAttribute) {
final int line = document.getLineNumber(namedParent.getTextRange().getStartOffset());
final LineRange toMove2 = info.toMove2;
info.toMove2 = new LineRange(Math.min(line, toMove2.startLine), toMove2.endLine);
}
}
}
return true;
}
private static boolean checkParents(MoveInfo info,
PsiNamedElement movedParent,
PsiNamedElement targetParent,
PsiFile file,
Document document) {
// don't jump into your own children!
if (targetParent.getParent() == movedParent) {
info.toMove2 = null;
return true;
}
if (movedParent.getParent() == targetParent) {
// info.toMove = new LineRange();
// well, target parent may be not detected right
int offset = document.getLineStartOffset(info.toMove2.startLine);
//PsiElement element = file.findElementAt(offset);
//info.toMove2 = null;
//return true;
}
return false;
}
private static boolean checkInjections(PsiElement movedEndElement, PsiElement movedStartElement) {
final XmlText text = PsiTreeUtil.getParentOfType(movedStartElement, XmlText.class);
final XmlText text2 = PsiTreeUtil.getParentOfType(movedEndElement, XmlText.class);
// Let's do not care about injections for this mover
if ( ( text != null && InjectedLanguageManager.getInstance(text.getProject()).getInjectedPsiFiles(text) != null) ||
( text2 != null && InjectedLanguageManager.getInstance(text2.getProject()).getInjectedPsiFiles(text2) != null)) {
return true;
}
return false;
}
private void updatedMovedIntoEnd(final Document document, @NotNull final MoveInfo info, final int offset) {
if (offset + 1 < document.getTextLength()) {
final int line = document.getLineNumber(offset + 1);
final LineRange toMove2 = info.toMove2;
if (toMove2 == null) return;
info.toMove2 = new LineRange(toMove2.startLine, Math.min(Math.max(line, toMove2.endLine), document.getLineCount() - 1));
}
}
private int updateMovedRegionStart(final Document document,
int movedLineStart,
final int offset,
@NotNull final MoveInfo info,
final boolean down) {
final int line = document.getLineNumber(offset);
final LineRange toMove = info.toMove;
int delta = toMove.startLine - line;
info.toMove = new LineRange(Math.min(line, toMove.startLine), toMove.endLine);
// update moved range
if (delta > 0 && !down) {
final LineRange toMove2 = info.toMove2;
info.toMove2 = new LineRange(toMove2.startLine - delta, toMove2.endLine - delta);
movedLineStart = document.getLineStartOffset(toMove.startLine);
}
return movedLineStart;
}
private int updateMovedRegionEnd(final Document document, int movedLineStart, final int valueStart, @NotNull final MoveInfo info, final boolean down) {
final int line = document.getLineNumber(valueStart);
final LineRange toMove = info.toMove;
int delta = line - toMove.endLine;
info.toMove = new LineRange(toMove.startLine, Math.max(line, toMove.endLine));
// update moved range
if (delta > 0 && down) {
final LineRange toMove2 = info.toMove2;
info.toMove2 = new LineRange(toMove2.startLine + delta, Math.min(toMove2.endLine + delta, document.getLineCount() - 1));
movedLineStart = document.getLineStartOffset(toMove.startLine);
}
return movedLineStart;
}
}
@@ -25,30 +25,32 @@ public class XmlMoverTest extends CodeInsightTestCase {
public void testTag10() throws Exception { doTest("xml"); }
public void testTag11() throws Exception { doTest("xml"); }
public void testMultiLineTag() throws Exception { doTest("xml"); }
public void testRootTag() throws Exception { doTest("xml"); }
public void testRootTag1() throws Exception { doTest("xml"); }
// public void testSingleTag() throws Exception { doTest("xml"); }
public void test1() throws Exception { doTest("html"); }
private void doTest(String ext) throws Exception {
final String baseName = getBasePath() + '/' + getTestName(true);
final String fileName = baseName + "."+ext;
try {
@NonNls String afterFileName = baseName + "_afterUp." + ext;
EditorActionHandler handler = new MoveStatementUpAction().getHandler();
performAction(fileName, handler, afterFileName);
@NonNls String afterFileName = baseName + "_afterUp." + ext;
EditorActionHandler handler = new MoveStatementUpAction().getHandler();
performAction(fileName, handler, afterFileName);
afterFileName = baseName + "_afterDown." + ext;
handler = new MoveStatementDownAction().getHandler();
performAction(fileName, handler, afterFileName);
}
finally {
//CodeStyleSettingsManager.getInstance(myProject).dropTemporarySettings();
}
afterFileName = baseName + "_afterDown." + ext;
handler = new MoveStatementDownAction().getHandler();
performAction(fileName, handler, afterFileName);
}
private void performAction(final String fileName, final EditorActionHandler handler, final String afterFileName) throws Exception {
configureByFile(fileName);
final boolean enabled = handler.isEnabled(myEditor, null);
assertEquals(new File(getTestDataPath(), afterFileName).exists(), enabled);
assertEquals("not enabled for " + afterFileName, new File(getTestDataPath(), afterFileName).exists(), enabled);
if (enabled) {
handler.execute(myEditor, null);
checkResultByFile(afterFileName);
@@ -0,0 +1,8 @@
<test>
<element
attribute="test1"/>
<element
<caret> attribute="test2"/>
<element
attribute="test3"/>
</test>
@@ -0,0 +1,8 @@
<test>
<element
attribute="test1"/>
<element
attribute="test3"/>
<element
attribute="test2"/>
</test>
@@ -0,0 +1,8 @@
<test>
<element
attribute="test2"/>
<element
attribute="test1"/>
<element
attribute="test3"/>
</test>
+6
View File
@@ -0,0 +1,6 @@
<te<caret>st>
<element
attribute="test1"/>
<element
attribute="test2"/>
</test>
+6
View File
@@ -0,0 +1,6 @@
<test>
<element
attribute="test1"/>
<element
attribute="test2"/>
</te<caret>st>
+8
View File
@@ -0,0 +1,8 @@
<root>
<te<caret>st>
<element
attribute="test1"/>
<element
attribute="test2"/>
</test>
</root>
@@ -0,0 +1,8 @@
<root>
<te<caret>st>
<element
attribute="test1"/>
<element
attribute="test2"/>
</test>
</root>
@@ -0,0 +1,8 @@
<root>
<te<caret>st>
<element
attribute="test1"/>
<element
attribute="test2"/>
</test>
</root>