blob: a59da3c70e69a9a33db2d95a2f3da56a05cd5026 [file] [log] [blame]
/*
* 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;
private final boolean myNonVirtualOnly;
/**
* Creates new <code>PsiAwareLineWrapPositionStrategy</code> object.
*
* @param nonVirtualOnly defines if current PSI-aware logic should be exploited only for 'real wrap' position requests
* @param enabledTypes target element/token types where line wrapping is allowed
*/
public PsiAwareLineWrapPositionStrategy(boolean nonVirtualOnly, @NotNull IElementType ... enabledTypes) {
myEnabledTypes = TokenSet.create(enabledTypes);
myNonVirtualOnly = nonVirtualOnly;
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,
boolean virtual) {
if (virtual && myNonVirtualOnly) {
LineWrapPositionStrategy implementation = LanguageLineWrapPositionStrategy.INSTANCE.getDefaultImplementation();
return implementation.calculateWrapPosition(
document, project, startOffset, endOffset, maxPreferredOffset, allowToBeyondMaxPreferredOffset, virtual
);
}
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 = Math.max(textRange.getStartOffset(), startOffset);
int end = Math.min(textRange.getEndOffset(), endOffset);
int result = doCalculateWrapPosition(document, project, start, end, end, false, virtual);
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, boolean)} but ensures
* that given offsets target {@link #PsiAwareLineWrapPositionStrategy(boolean, 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
* @param virtual identifies if current request is for virtual wrap (soft wrap) position
* @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, boolean virtual
);
/**
* 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();
if (textRange == null) {
return false;
}
for (PsiElement parent = element; parent != null && textRange.equals(parent.getTextRange()); 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;
}
}