blob: 5a9defd95d0c165c325a078c138a4b4b92494290 [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.lang.Language;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.FileViewProvider;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.StringTokenizer;
/**
* Performs {@code 'PSI element <-> signature'} mappings on the basis of the target PSI element's offsets.
* <p/>
* Thread-safe.
*
* @author Denis Zhdanov
* @since 11/7/11 11:59 AM
*/
public class OffsetsElementSignatureProvider extends AbstractElementSignatureProvider {
private static final String TYPE_MARKER = "e";
@Override
protected PsiElement restoreBySignatureTokens(@NotNull PsiFile file,
@NotNull PsiElement parent,
@NotNull 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;
}
int start;
int end;
try {
start = Integer.parseInt(tokenizer.nextToken());
end = Integer.parseInt(tokenizer.nextToken());
}
catch (NumberFormatException e) {
return null;
}
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Parsed target offsets - [%d; %d)%n", start, end));
}
int index = 0;
if (tokenizer.hasMoreTokens()) {
try {
index = Integer.parseInt(tokenizer.nextToken());
}
catch (NumberFormatException e) {
// Do nothing
}
}
Language language = file.getLanguage();
if (tokenizer.hasMoreTokens()) {
String languageId = tokenizer.nextToken();
Language languageFromSignature = Language.findLanguageByID(languageId);
if (languageFromSignature == null) {
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Couldn't find language for id %s", languageId));
}
}
else {
language = languageFromSignature;
}
}
FileViewProvider viewProvider = file.getViewProvider();
PsiElement element = viewProvider.findElementAt(start, language);
if (element == null) {
return null;
}
PsiElement result = findElement(start, end, index, element, processingInfoStorage);
if (result != null) {
return result;
}
else if (processingInfoStorage != null) {
processingInfoStorage.append(String.format(
"Failed to find an element by the given offsets for language %s. Started by the element '%s' (%s)",
language, element, element.getText()
));
}
PsiFile psiFile = viewProvider.getPsi(language);
if (psiFile == null) {
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Couldn't find PSI for language %s", language.toString()));
}
return null;
}
final PsiElement injectedStartElement = InjectedLanguageUtil.findElementAtNoCommit(psiFile, start);
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format(
"Trying to find injected element starting from the '%s'%s%n",
injectedStartElement, injectedStartElement == null ? "" : String.format("(%s)", injectedStartElement.getText())
));
}
if (injectedStartElement != null && injectedStartElement != element) {
return findElement(start, end, index, injectedStartElement, processingInfoStorage);
}
return null;
}
@Nullable
private PsiElement findElement(int start, int end, int index, @NotNull PsiElement element, @Nullable StringBuilder processingInfoStorage) {
TextRange range = element.getTextRange();
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Starting processing from element '%s'. It's range is %s%n", element, range));
}
while (range != null && range.getStartOffset() == start && range.getEndOffset() < end) {
element = element.getParent();
range = element.getTextRange();
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Expanding element to '%s' and range to '%s'%n", element, range));
}
}
if (range == null || range.getStartOffset() != start || range.getEndOffset() != end) {
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format(
"Stopping %s because target element's range differs from the target one. Element: '%s', it's range: %s%n",
getClass(), element, range));
}
return null;
}
// There is a possible case that we have a hierarchy of PSI elements that target the same document range. We need to find
// out the right one then.
int indexFromRoot = 0;
for (PsiElement e = element.getParent(); e != null && range.equals(e.getTextRange()); e = e.getParent()) {
indexFromRoot++;
}
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Target element index is %d. Current index from root is %d%n", index, indexFromRoot));
}
if (index > indexFromRoot) {
int steps = index - indexFromRoot;
PsiElement result = element;
for (PsiElement e = result.getFirstChild(); steps > 0 && e != null && range.equals(e.getTextRange()); steps--, e = e.getFirstChild()) {
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Clarifying target element to '%s', its range is %s%n", result, result.getTextRange()));
}
result = e;
}
return result;
}
int steps = indexFromRoot - index;
PsiElement result = element;
while (--steps >= 0) {
result = result.getParent();
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Reducing target element to '%s', its range is %s%n", result, result.getTextRange()));
}
}
if (processingInfoStorage != null) {
processingInfoStorage.append(String.format("Returning element '%s', its range is %s%n", result, result.getTextRange()));
}
return result;
}
@Override
public String getSignature(@NotNull PsiElement element) {
TextRange range = element.getTextRange();
if (range.isEmpty()) {
return null;
}
StringBuilder buffer = new StringBuilder();
buffer.append(TYPE_MARKER).append("#");
buffer.append(range.getStartOffset());
buffer.append(ELEMENT_TOKENS_SEPARATOR);
buffer.append(range.getEndOffset());
// There is a possible case that given PSI element has a parent or child that targets the same range. So, we remember
// not only target range offsets but 'hierarchy index' as well.
int index = 0;
for (PsiElement e = element.getParent(); e != null && range.equals(e.getTextRange()); e = e.getParent()) {
index++;
}
buffer.append(ELEMENT_TOKENS_SEPARATOR).append(index);
PsiFile containingFile = element.getContainingFile();
if (containingFile != null && containingFile.getViewProvider().getLanguages().size() > 1) {
buffer.append(ELEMENT_TOKENS_SEPARATOR).append(containingFile.getLanguage().getID());
}
return buffer.toString();
}
}