blob: 3409d3f93fc80e040edbbe49e7d29950a780041f [file] [log] [blame]
/*
* Copyright 2000-2014 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.folding.impl;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.StringTokenizer;
/**
* Performs {@code 'PSI element <-> signature'} mappings on the basis of unique path of {@link PsiNamedElement PSI named elements}
* to the PSI root.
* <p/>
* Thread-safe.
*
* @author Denis Zhdanov
* @since 11/7/11 11:58 AM
*/
public class PsiNamesElementSignatureProvider extends AbstractElementSignatureProvider {
private static final String TYPE_MARKER = "n";
private static final String TOP_LEVEL_CHILD_MARKER = "!!top";
private static final String DOC_COMMENT_MARKER = "!!doc";
private static final String CODE_BLOCK_MARKER = "!!block";
@Override
protected PsiElement restoreBySignatureTokens(@NotNull PsiFile file,
@NotNull PsiElement parent,
@NotNull final String type,
@NotNull StringTokenizer tokenizer,
@Nullable StringBuilder processingInfoStorage)
{
if (!TYPE_MARKER.equals(type)) {
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format(
"Stopping '%s' provider because given signature doesn't have expected type - can work with '%s' but got '%s'%n",
getClass().getName(), TYPE_MARKER, type
));
}
return null;
}
String elementMarker = tokenizer.nextToken();
if (TOP_LEVEL_CHILD_MARKER.equals(elementMarker)) {
PsiElement result = null;
for (PsiElement child = file.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof PsiWhiteSpace) {
continue;
}
if (result == null) {
result = child;
}
else {
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format(
"Stopping '%s' provider because it has top level marker but more than one non white-space child: %s%n",
getClass().getName(), Arrays.toString(file.getChildren())
));
}
// More than one top-level non-white space children. Can't match.
return null;
}
}
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format(
"Finished processing of '%s' provider because all of its top-level children have been processed: %s%n",
getClass().getName(), Arrays.toString(file.getChildren())
));
}
return result;
}
else if (DOC_COMMENT_MARKER.equals(elementMarker)) {
PsiElement candidate = parent.getFirstChild();
return candidate instanceof PsiComment ? candidate : null;
}
else if (CODE_BLOCK_MARKER.equals(elementMarker)) {
int index = 0;
if (tokenizer.hasMoreTokens()) {
String indexStr = tokenizer.nextToken();
try {
index = Integer.parseInt(indexStr);
}
catch (NumberFormatException e) {
if (processingInfoStorage != null) {
processingInfoStorage.append("Invalid block index: ").append(indexStr).append("\n");
}
}
}
for (PsiElement child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
if (isBlockElement(child)) {
if (--index < 0) {
return child;
}
}
}
return null;
}
if (!tokenizer.hasMoreTokens()) {
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Stopping '%s' provider because it has no more data to process%n", getClass().getName()));
}
return null;
}
try {
int index = Integer.parseInt(tokenizer.nextToken());
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Looking for the child with a name '%s' # %d at the element '%s'%n",
elementMarker, index, parent));
}
return restoreElementInternal(parent, unescape(elementMarker), index, PsiNamedElement.class);
}
catch (NumberFormatException e) {
return null;
}
}
@Override
public String getSignature(@NotNull final PsiElement element) {
StringBuilder buffer = null;
int length;
for (PsiElement current = element; current != null && !(current instanceof PsiFile); current = current.getParent()) {
length = buffer == null ? 0 : buffer.length();
StringBuilder b = getSignature(current, buffer);
if (b == null && buffer != null && current.getParent() instanceof PsiFile && canResolveTopLevelChild(current)) {
buffer.append(TYPE_MARKER).append(ELEMENT_TOKENS_SEPARATOR).append(TOP_LEVEL_CHILD_MARKER).append(ELEMENTS_SEPARATOR);
break;
}
buffer = b;
if (buffer == null || length >= buffer.length()) {
return null;
}
buffer.append(ELEMENTS_SEPARATOR);
}
if (buffer == null) {
return null;
}
buffer.setLength(buffer.length() - 1);
return buffer.toString();
}
/**
* Allows to answer if it's possible to use {@link #TOP_LEVEL_CHILD_MARKER} for the given element.
*
* @param element element to check
* @return <code>true</code> if {@link #TOP_LEVEL_CHILD_MARKER} can be used for the given element; <code>false</code> otherwise
*/
private static boolean canResolveTopLevelChild(@NotNull PsiElement element) {
final PsiElement parent = element.getParent();
if (parent == null) {
return false;
}
for (PsiElement child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof PsiWhiteSpace) {
continue;
}
if (child != element) {
return false;
}
}
return true;
}
/**
* Tries to produce signature for the exact given PSI element.
*
* @param element target element
* @param buffer buffer to store the signature in
* @return buffer that contains signature of the given element if it was produced;
* <code>null</code> as an indication that signature for the given element was not produced
*/
@SuppressWarnings("unchecked")
@Nullable
private static StringBuilder getSignature(@NotNull PsiElement element, @Nullable StringBuilder buffer) {
if (element instanceof PsiNamedElement) {
PsiNamedElement named = (PsiNamedElement)element;
final String name = named.getName();
if (StringUtil.isEmpty(name)) {
return null;
}
int index = getChildIndex(named, element.getParent(), name, PsiNamedElement.class);
StringBuilder bufferToUse = buffer;
if (bufferToUse == null) {
bufferToUse = new StringBuilder();
}
bufferToUse.append(TYPE_MARKER).append(ELEMENT_TOKENS_SEPARATOR).append(escape(name))
.append(ELEMENT_TOKENS_SEPARATOR).append(index);
return bufferToUse;
}
else if (element instanceof PsiComment) {
PsiElement parent = element.getParent();
boolean nestedComment = false;
if (parent instanceof PsiComment && parent.getTextRange().equals(element.getTextRange())) {
parent = parent.getParent();
nestedComment = true;
}
if (parent instanceof PsiNamedElement && (nestedComment || parent.getFirstChild() == element)) {
// Consider doc comment element that is the first child of named element to be a doc comment.
StringBuilder bufferToUse = buffer;
if (bufferToUse == null) {
bufferToUse = new StringBuilder();
}
bufferToUse.append(TYPE_MARKER).append(ELEMENT_TOKENS_SEPARATOR).append(DOC_COMMENT_MARKER);
return bufferToUse;
}
}
PsiElement parent = element.getParent();
if (parent instanceof PsiNamedElement && !(parent instanceof PsiFile)) {
if (isBlockElement(element)) {
int index = getBlockElementIndex(element);
StringBuilder bufferToUse = buffer;
if (bufferToUse == null) {
bufferToUse = new StringBuilder();
}
bufferToUse.append(TYPE_MARKER).append(ELEMENT_TOKENS_SEPARATOR).append(CODE_BLOCK_MARKER);
if (index > 0) {
bufferToUse.append(ELEMENT_TOKENS_SEPARATOR).append(index);
}
return bufferToUse;
}
}
return null;
}
private static boolean isBlockElement(@NotNull PsiElement element) {
PsiElement firstChild = element.getFirstChild();
PsiElement lastChild = element.getLastChild();
return firstChild != null && "{".equals(firstChild.getText()) && lastChild != null && "}".equals(lastChild.getText());
}
private static int getBlockElementIndex(@NotNull PsiElement element) {
int i = 0;
for (PsiElement sibling : element.getParent().getChildren()) {
if (element.equals(sibling)) {
return i;
}
if (isBlockElement(sibling)) {
i++;
}
}
throw new RuntimeException("Malformed PSI");
}
}