/*
* Copyright 2010-2016 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 org.jetbrains.kotlin.idea.codeInsight;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.descriptors.ClassKind;
import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils;
import org.jetbrains.kotlin.lexer.KtTokens;
import org.jetbrains.kotlin.psi.*;
import org.jetbrains.kotlin.psi.psiUtil.KtPsiUtilKt;
import org.jetbrains.kotlin.resolve.BindingContext;
import org.jetbrains.kotlin.resolve.scopes.receivers.ClassQualifier;
import org.jetbrains.kotlin.resolve.scopes.receivers.Qualifier;
import org.jetbrains.kotlin.types.KotlinType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jetbrains.kotlin.builtins.KotlinBuiltIns.*;
public class CodeInsightUtils {
@Nullable
public static PsiElement findElement(
@NotNull PsiFile file,
int startOffset,
int endOffset,
@NotNull CodeInsightUtils.ElementKind elementKind
) {
Class<? extends KtElement> elementClass;
switch (elementKind) {
case EXPRESSION: elementClass = KtExpression.class;
break;
case TYPE_ELEMENT: elementClass = KtTypeElement.class;
break;
case TYPE_CONSTRUCTOR: elementClass = KtSimpleNameExpression.class;
break;
default: throw new IllegalArgumentException(elementKind.name());
}
PsiElement element = findElementOfClassAtRange(file, startOffset, endOffset, elementClass);
if (elementKind == ElementKind.TYPE_ELEMENT) return element;
if (elementKind == ElementKind.TYPE_CONSTRUCTOR) {
return element != null && KtPsiUtilKt.isTypeConstructorReference(element) ? element : null;
}
if (element instanceof KtScriptInitializer) {
element = ((KtScriptInitializer) element).getBody();
}
if (element == null) return null;
// TODO: Support binary operations in "Introduce..." refactorings
if (element instanceof KtOperationReferenceExpression
&& ((KtOperationReferenceExpression) element).getReferencedNameElementType() != KtTokens.IDENTIFIER
&& element.getParent() instanceof KtBinaryExpression) {
return null;
}
// For cases like 'this@outerClass', don't return the label part
if (KtPsiUtil.isLabelIdentifierExpression(element)) {
element = PsiTreeUtil.getParentOfType(element, KtExpression.class);
}
if (element instanceof KtBlockExpression) {
List<KtExpression> statements = ((KtBlockExpression) element).getStatements();
if (statements.size() == 1) {
KtExpression statement = statements.get(0);
if (statement.getText().equals(element.getText())) {
return statement;
}
}
}
KtExpression expression = (KtExpression) element;
BindingContext context = ResolutionUtils.analyze(expression);
Qualifier qualifier = context.get(BindingContext.QUALIFIER, expression);
if (qualifier != null) {
if (!(qualifier instanceof ClassQualifier)) return null;
if (((ClassQualifier) qualifier).getDescriptor().getKind() != ClassKind.OBJECT) return null;
}
return expression;
}
public enum ElementKind {
EXPRESSION,
TYPE_ELEMENT,
TYPE_CONSTRUCTOR
}
@NotNull
public static PsiElement[] findElements(@NotNull PsiFile file, int startOffset, int endOffset, @NotNull ElementKind kind) {
PsiElement element1 = getElementAtOffsetIgnoreWhitespaceBefore(file, startOffset);
PsiElement element2 = getElementAtOffsetIgnoreWhitespaceAfter(file, endOffset);
if (element1 == null || element2 == null) return PsiElement.EMPTY_ARRAY;
startOffset = element1.getTextRange().getStartOffset();
endOffset = element2.getTextRange().getEndOffset();
if (startOffset >= endOffset) return PsiElement.EMPTY_ARRAY;
PsiElement parent = PsiTreeUtil.findCommonParent(element1, element2);
if (parent == null) return PsiElement.EMPTY_ARRAY;
while (true) {
if (parent instanceof KtBlockExpression) break;
if (parent == null || parent instanceof KtFile) return PsiElement.EMPTY_ARRAY;
parent = parent.getParent();
}
element1 = getTopmostParentInside(element1, parent);
if (startOffset != element1.getTextRange().getStartOffset()) return PsiElement.EMPTY_ARRAY;
element2 = getTopmostParentInside(element2, parent);
if (endOffset != element2.getTextRange().getEndOffset()) return PsiElement.EMPTY_ARRAY;
List<PsiElement> array = new ArrayList<PsiElement>();
PsiElement stopElement = element2.getNextSibling();
for (PsiElement currentElement = element1; currentElement != stopElement; currentElement = currentElement.getNextSibling()) {
if (!(currentElement instanceof PsiWhiteSpace)) {
array.add(currentElement);
}
}
for (PsiElement element : array) {
boolean correctType = kind == ElementKind.EXPRESSION && element instanceof KtExpression
|| kind == ElementKind.TYPE_ELEMENT && element instanceof KtTypeElement
|| kind == ElementKind.TYPE_CONSTRUCTOR && KtPsiUtilKt.isTypeConstructorReference(element);
if (!(correctType
|| element.getNode().getElementType() == KtTokens.SEMICOLON
|| element instanceof PsiWhiteSpace
|| element instanceof PsiComment)) {
return PsiElement.EMPTY_ARRAY;
}
}
return PsiUtilCore.toPsiElementArray(array);
}
@Nullable
public static <T extends PsiElement> T findElementOfClassAtRange(@NotNull PsiFile file, int startOffset, int endOffset, Class<T> aClass) {
// When selected range is this@Fo<select>o</select> we'd like to return `@Foo`
// But it's PSI looks like: (AT IDENTIFIER):JetLabel
// So if we search parent starting exactly at IDENTIFIER then we find nothing
// Solution is to retrieve label if we are on AT or IDENTIFIER
PsiElement element1 = getParentLabelOrElement(getElementAtOffsetIgnoreWhitespaceBefore(file, startOffset));
PsiElement element2 = getParentLabelOrElement(getElementAtOffsetIgnoreWhitespaceAfter(file, endOffset));
if (element1 == null || element2 == null) return null;
startOffset = element1.getTextRange().getStartOffset();
endOffset = element2.getTextRange().getEndOffset();
T newElement = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, endOffset, aClass);
if (newElement == null ||
newElement.getTextRange().getStartOffset() != startOffset ||
newElement.getTextRange().getEndOffset() != endOffset) {
return null;
}
return newElement;
}
private static PsiElement getParentLabelOrElement(@Nullable PsiElement element) {
if (element != null && element.getParent() instanceof KtLabelReferenceExpression) {
return element.getParent();
}
return element;
}
@NotNull
public static List<PsiElement> findElementsOfClassInRange(@NotNull PsiFile file, int startOffset, int endOffset, Class<? extends PsiElement> ... classes) {
PsiElement element1 = getElementAtOffsetIgnoreWhitespaceBefore(file, startOffset);
PsiElement element2 = getElementAtOffsetIgnoreWhitespaceAfter(file, endOffset);
if (element1 == null || element2 == null) return Collections.emptyList();
startOffset = element1.getTextRange().getStartOffset();
endOffset = element2.getTextRange().getEndOffset();
PsiElement parent = PsiTreeUtil.findCommonParent(element1, element2);
if (parent == null) return Collections.emptyList();
element1 = getTopmostParentInside(element1, parent);
if (startOffset != element1.getTextRange().getStartOffset()) return Collections.emptyList();
element2 = getTopmostParentInside(element2, parent);
if (endOffset != element2.getTextRange().getEndOffset()) return Collections.emptyList();
PsiElement stopElement = element2.getNextSibling();
List<PsiElement> result = new ArrayList<PsiElement>();
for (PsiElement currentElement = element1; currentElement != stopElement && currentElement != null; currentElement = currentElement.getNextSibling()) {
for (Class aClass : classes) {
if (aClass.isInstance(currentElement)) {
result.add(currentElement);
}
result.addAll(PsiTreeUtil.findChildrenOfType(currentElement, aClass));
}
}
return result;
}
@NotNull
private static PsiElement getTopmostParentInside(@NotNull PsiElement element, @NotNull PsiElement parent) {
if (!parent.equals(element)) {
while (!parent.equals(element.getParent())) {
element = element.getParent();
}
}
return element;
}
@Nullable
public static PsiElement getElementAtOffsetIgnoreWhitespaceBefore(@NotNull PsiFile file, int offset) {
PsiElement element = file.findElementAt(offset);
if (element instanceof PsiWhiteSpace) {
return file.findElementAt(element.getTextRange().getEndOffset());
}
return element;
}
@Nullable
public static PsiElement getElementAtOffsetIgnoreWhitespaceAfter(@NotNull PsiFile file, int offset) {
PsiElement element = file.findElementAt(offset - 1);
if (element instanceof PsiWhiteSpace) {
return file.findElementAt(element.getTextRange().getStartOffset() - 1);
}
return element;
}
@Nullable
public static String defaultInitializer(KotlinType type) {
if (type.isMarkedNullable()) {
return "null";
}
else if (isInt(type) || isLong(type) || isShort(type) || isByte(type)) {
return "0";
}
else if (isFloat(type)) {
return "0.0f";
}
else if (isDouble(type)) {
return "0.0";
}
else if (isChar(type)) {
return "'\\u0000'";
}
else if (isBoolean(type)) {
return "false";
}
else if (isUnit(type)) {
return "Unit";
}
else if (isString(type)) {
return "\"\"";
}
return null;
}
public static void showErrorHint(
@NotNull Project project, @NotNull Editor editor,
@NotNull String message, @NotNull String title,
@Nullable String helpId
) {
if (ApplicationManager.getApplication().isUnitTestMode()) throw new RuntimeException(message);
CommonRefactoringUtil.showErrorHint(project, editor, message, title, helpId);
}
private CodeInsightUtils() {
}
@Nullable
public static Integer getStartLineOffset(@NotNull PsiFile file, int line) {
Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
if (document == null) return null;
if (line >= document.getLineCount()) {
return null;
}
int lineStartOffset = document.getLineStartOffset(line);
return CharArrayUtil.shiftForward(document.getCharsSequence(), lineStartOffset, " \t");
}
@Nullable
public static Integer getEndLineOffset(@NotNull PsiFile file, int line) {
Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
if (document == null) return null;
if (line >= document.getLineCount()) {
return null;
}
int lineStartOffset = document.getLineEndOffset(line);
return CharArrayUtil.shiftBackward(document.getCharsSequence(), lineStartOffset, " \t");
}
@Nullable
public static PsiElement getTopmostElementAtOffset(@NotNull PsiElement element, int offset) {
do {
PsiElement parent = element.getParent();
if (parent == null || (parent.getTextOffset() < offset) || parent instanceof KtBlockExpression) {
break;
}
element = parent;
}
while(true);
return element;
}
@Nullable
public static <T> T getTopmostElementAtOffset(@NotNull PsiElement element, int offset, @NotNull Class<T> klass) {
T lastElementOfType = null;
if (klass.isInstance(element)) {
lastElementOfType = (T) element;
}
do {
PsiElement parent = element.getParent();
if (parent == null || (parent.getTextOffset() < offset) || parent instanceof KtBlockExpression) {
break;
}
if (klass.isInstance(parent)) {
lastElementOfType = (T) parent;
}
element = parent;
}
while(true);
return lastElementOfType;
}
}