/* * Copyright 2000-2017 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(); if (element == null) { if (processingInfoStorage != null) { processingInfoStorage.append("Reached top of PSI tree"); } return null; } 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(); } }