IDEA-69506 Code Wrapping Creates Errors In XML

1. Defined basic class for PSI-aware line wrapping strategy;
2. Created markup-specific line wrapping strategy;
3. Corresponding test is added;
This commit is contained in:
Denis Zhdanov
2011-05-12 16:23:49 +04:00
parent 4dc5defef0
commit fd3f5e58bb
12 changed files with 279 additions and 6 deletions
@@ -53,4 +53,10 @@ public class LanguageLineWrapPositionStrategy extends LanguageExtension<LineWrap
}
return result;
}
@NotNull
@Override
public LineWrapPositionStrategy getDefaultImplementation() {
return super.getDefaultImplementation();
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2000-2011 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.openapi.editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* {@link LineWrapPositionStrategy} implementation that uses
* {@link LanguageLineWrapPositionStrategy#getDefaultImplementation() default line wrap strategy} but restricts its scope
* by {@link #PsiAwareDefaultLineWrapPositionStrategy(IElementType...) target tokens/elements}.
*
* @author Denis Zhdanov
* @since 5/12/11 12:50 PM
*/
public class PsiAwareDefaultLineWrapPositionStrategy extends PsiAwareLineWrapPositionStrategy {
public PsiAwareDefaultLineWrapPositionStrategy(@NotNull IElementType ... enabledTypes) {
super(enabledTypes);
}
@Override
protected int doCalculateWrapPosition(@NotNull Document document,
@Nullable Project project,
int startOffset,
int endOffset,
int maxPreferredOffset,
boolean allowToBeyondMaxPreferredOffset)
{
LineWrapPositionStrategy implementation = LanguageLineWrapPositionStrategy.INSTANCE.getDefaultImplementation();
return implementation.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset,
allowToBeyondMaxPreferredOffset);
}
}
@@ -0,0 +1,170 @@
/*
* Copyright 2000-2011 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.openapi.editor;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Base super-class for {@link LineWrapPositionStrategy} implementations that want to restrict wrap positions
* only for particular elements/tokens (e.g. we may want to avoid line wrap in the middle of xml tag name etc).
*
* @author Denis Zhdanov
* @since 5/12/11 12:30 PM
*/
public abstract class PsiAwareLineWrapPositionStrategy implements LineWrapPositionStrategy {
private static final Logger LOG = Logger.getInstance("#" + PsiAwareLineWrapPositionStrategy.class.getName());
private final TokenSet myEnabledTypes;
/**
* Creates new <code>PsiAwareLineWrapPositionStrategy</code> object.
*
* @param enabledTypes target element/token types where line wrapping is allowed
*/
public PsiAwareLineWrapPositionStrategy(@NotNull IElementType ... enabledTypes) {
myEnabledTypes = TokenSet.create(enabledTypes);
if (enabledTypes.length <= 0) {
LOG.warn(String.format("%s instance is created with empty token/element types. That will lead to inability to perform line wrap",
getClass().getName()));
}
}
@Override
public int calculateWrapPosition(@NotNull Document document,
@Nullable Project project,
int startOffset,
int endOffset,
int maxPreferredOffset,
boolean allowToBeyondMaxPreferredOffset) {
if (project == null) {
return -1;
}
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
if (documentManager == null) {
return -1;
}
PsiFile psiFile = documentManager.getPsiFile(document);
if (psiFile == null) {
return -1;
}
PsiElement element = psiFile.findElementAt(maxPreferredOffset);
if (element == null) {
return -1;
}
for (; element != null && element.getTextRange().getEndOffset() > startOffset; element = getPrevious(element)) {
if (allowToWrapInside(element)) {
TextRange textRange = element.getTextRange();
int start = textRange.getStartOffset();
int end = textRange.getEndOffset();
int result = doCalculateWrapPosition(document, project, start, end, end, false);
if (result >= 0) {
return result;
}
// Assume that it's possible to wrap on token boundary (makes sense at least for the tokens that occupy one symbol only).
if (end <= maxPreferredOffset) {
return end;
}
if (start > startOffset) {
return start;
}
}
}
return -1;
}
/**
* Serves for the same purposes as {@link #calculateWrapPosition(Document, Project, int, int, int, boolean)} but ensures that given
* offsets target {@link #PsiAwareLineWrapPositionStrategy(IElementType...) enabled token/element types}.
*
* @param document target document which text is being processed
* @param project target project
* @param startOffset start offset to use with the given text holder (inclusive)
* @param endOffset end offset to use with the given text holder (exclusive)
* @param maxPreferredOffset this method is expected to do its best to return offset that belongs to
* <code>(startOffset; maxPreferredOffset]</code> interval. However, it's allowed
* to return value from <code>(maxPreferredOffset; endOffset]</code> interval
* unless <code>'allowToBeyondMaxPreferredOffset'</code> if <code>'false'</code>
* @param allowToBeyondMaxPreferredOffset indicates if it's allowed to return value from
* <code>(maxPreferredOffset; endOffset]</code> interval in case of inability to
* find appropriate offset from <code>(startOffset; maxPreferredOffset]</code> interval
* @return offset from <code>(startOffset; endOffset]</code> interval where
* target line should be wrapped OR <code>-1</code> if no wrapping should be performed
*/
protected abstract int doCalculateWrapPosition(
@NotNull Document document, @Nullable Project project, int startOffset, int endOffset, int maxPreferredOffset,
boolean allowToBeyondMaxPreferredOffset
);
/**
* Allows to check if line wrap at the text range defined by the given element is allowed.
*
* @param element element that defines target text range
* @return <code>true</code> if wrapping at the text range defined by the given element is allowed;
* <code>false</code> otherwise
*/
private boolean allowToWrapInside(@NotNull PsiElement element) {
TextRange textRange = element.getTextRange();
for (PsiElement parent = element; parent != null && parent.getTextRange().equals(textRange); parent = parent.getParent()) {
ASTNode parentNode = parent.getNode();
if (parentNode != null && myEnabledTypes.contains(parentNode.getElementType())) {
return true;
}
}
return false;
}
@Nullable
private static PsiElement getPrevious(@NotNull PsiElement element) {
PsiElement result = element.getPrevSibling();
if (result != null) {
return result;
}
PsiElement parent = element.getParent();
if (parent == null) {
return null;
}
PsiElement parentSibling = null;
for (; parent != null && parentSibling == null; parent = parent.getParent()) {
parentSibling = parent.getPrevSibling();
}
if (parentSibling == null) {
return null;
}
result = parentSibling.getLastChild();
return result == null ? parentSibling : result;
}
}
@@ -150,7 +150,7 @@ public class AutoHardWrapHandler {
new VisualPosition(caretModel.getVisualPosition().line, margin - FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS)
));
int wrapOffset = strategy.calculateWrapPosition(document, startOffset, endOffset, maxPreferredOffset, true);
int wrapOffset = strategy.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset, true);
if (wrapOffset < 0) {
return;
}
@@ -370,7 +370,8 @@ public class CodeFormatterFacade {
// We know that current line exceeds right margin if control flow reaches this place, so, wrap it.
int wrapOffset = strategy.calculateWrapPosition(
document, Math.max(startLineOffset, startOffsetToUse), Math.min(endLineOffset, endOffsetToUse), preferredWrapPosition, false
document, editor.getProject(), Math.max(startLineOffset, startOffsetToUse), Math.min(endLineOffset, endOffsetToUse),
preferredWrapPosition, false
);
if (wrapOffset < 0) {
continue;
@@ -15,8 +15,10 @@
*/
package com.intellij.openapi.editor;
import com.intellij.openapi.project.Project;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
@@ -42,6 +44,7 @@ public class GenericLineWrapPositionStrategy implements LineWrapPositionStrategy
@Override
public int calculateWrapPosition(@NotNull Document document,
@Nullable Project project,
int startOffset,
int endOffset,
int maxPreferredOffset,
@@ -15,7 +15,9 @@
*/
package com.intellij.openapi.editor;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Defines contract for the strategy that calculates the best place to apply wrap to particular line (sequence of characters).
@@ -33,6 +35,7 @@ public interface LineWrapPositionStrategy {
* Allows to calculate the most appropriate position to wrap target line.
*
* @param document target document which text is being processed
* @param project target project
* @param startOffset start offset to use with the given text holder (inclusive)
* @param endOffset end offset to use with the given text holder (exclusive)
* @param maxPreferredOffset this method is expected to do its best to return offset that belongs to
@@ -46,7 +49,7 @@ public interface LineWrapPositionStrategy {
* target line should be wrapped OR <code>-1</code> if no wrapping should be performed
*/
int calculateWrapPosition(
@NotNull Document document, int startOffset, int endOffset, int maxPreferredOffset,
@NotNull Document document, @Nullable Project project, int startOffset, int endOffset, int maxPreferredOffset,
boolean allowToBeyondMaxPreferredOffset
);
}
@@ -15,6 +15,7 @@
*/
package com.intellij.openapi.editor;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jmock.Expectations;
import org.jmock.Mockery;
@@ -36,6 +37,7 @@ public class DefaultLineWrapPositionStrategyTest {
private Mockery myMockery;
private DefaultLineWrapPositionStrategy myStrategy;
private Project myProject;
@Before
public void setUp() {
@@ -44,6 +46,7 @@ public class DefaultLineWrapPositionStrategyTest {
myMockery = new JUnit4Mockery() {{
setImposteriser(ClassImposteriser.INSTANCE);
}};
myProject = myMockery.mock(Project.class);
}
@Test
@@ -82,7 +85,7 @@ public class DefaultLineWrapPositionStrategyTest {
final Context context = new Context(document);
context.init();
int actual = myStrategy.calculateWrapPosition(
createMockDocument(context.document), 0, context.document.length(), context.edgeIndex, allowToBeyondMaxPreferredOffset
createMockDocument(context.document), myProject, 0, context.document.length(), context.edgeIndex, allowToBeyondMaxPreferredOffset
);
assertSame(context.wrapIndex, actual);
}
@@ -519,7 +519,7 @@ public class SoftWrapApplianceManager implements SoftWrapFoldingListener, Docume
myLineWrapPositionStrategy = LanguageLineWrapPositionStrategy.INSTANCE.forEditor(myEditor);
}
softWrapOffset = myLineWrapPositionStrategy.calculateWrapPosition(document, minOffset, maxOffset, preferredOffset, true);
softWrapOffset = myLineWrapPositionStrategy.calculateWrapPosition(document, myEditor.getProject(), minOffset, maxOffset, preferredOffset, true);
}
if (softWrapOffset >= lineData.endLineOffset || softWrapOffset < 0) {
@@ -150,6 +150,9 @@
<lang.formatter language="XML" implementationClass="com.intellij.lang.xml.XmlFormattingModelBuilder"/>
<lang.formatter language="HTML" implementationClass="com.intellij.lang.html.HtmlFormattingModelBuilder"/>
<lang.formatter language="XHTML" implementationClass="com.intellij.lang.xhtml.XhtmlFormattingModelBuilder"/>
<lang.lineWrapStrategy language="XML" implementationClass="com.intellij.psi.formatter.MarkupLineWrapPositionStrategy"/>
<lang.lineWrapStrategy language="HTML" implementationClass="com.intellij.psi.formatter.MarkupLineWrapPositionStrategy"/>
<lang.lineWrapStrategy language="XHTML" implementationClass="com.intellij.psi.formatter.MarkupLineWrapPositionStrategy"/>
<lang.documentationProvider language="XML" implementationClass="com.intellij.xml.util.documentation.XmlDocumentationProvider"
order="last"/>
@@ -23,7 +23,7 @@ package com.intellij.testFramework;
*/
public enum TestFileType {
JAVA("java"), SQL("sql"), TEXT("txt");
JAVA("java"), SQL("sql"), TEXT("txt"), XML("xml"), HTML("html"), XHTML("xhtml");
private final String myExtension;
@@ -0,0 +1,35 @@
/*
* Copyright 2000-2011 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.psi.formatter;
import com.intellij.openapi.editor.LineWrapPositionStrategy;
import com.intellij.openapi.editor.PsiAwareDefaultLineWrapPositionStrategy;
import com.intellij.psi.TokenType;
import com.intellij.psi.xml.XmlElementType;
import com.intellij.psi.xml.XmlTokenType;
/**
* {@link LineWrapPositionStrategy} for markup languages like XML, HTML etc.
*
* @author Denis Zhdanov
* @since 5/11/11 7:42 PM
*/
public class MarkupLineWrapPositionStrategy extends PsiAwareDefaultLineWrapPositionStrategy {
public MarkupLineWrapPositionStrategy() {
super(XmlElementType.XML_TEXT, TokenType.WHITE_SPACE);
}
}