diff --git a/idea/src/org/jetbrains/kotlin/idea/filters/InlineFunctionHyperLinkInfo.kt b/idea/src/org/jetbrains/kotlin/idea/filters/InlineFunctionHyperLinkInfo.kt new file mode 100644 index 00000000000..314b3bd872a --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/filters/InlineFunctionHyperLinkInfo.kt @@ -0,0 +1,104 @@ +/* + * 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.filters + +import com.intellij.execution.filters.FileHyperlinkInfo +import com.intellij.execution.filters.HyperlinkInfoBase +import com.intellij.execution.filters.OpenFileHyperlinkInfo +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBList +import java.awt.Component +import javax.swing.JList +import javax.swing.ListCellRenderer + +class InlineFunctionHyperLinkInfo( + private val project: Project, + private val inlineInfo: List +) : HyperlinkInfoBase(), FileHyperlinkInfo { + + override fun navigate(project: Project, hyperlinkLocationPoint: RelativePoint?) { + if (inlineInfo.isEmpty()) return + + if (inlineInfo.size == 1) { + OpenFileHyperlinkInfo(project, inlineInfo.first().file, inlineInfo.first().line).navigate(project) + } + else { + val list = JBList(inlineInfo) + list.cellRenderer = InlineInfoCellRenderer() + val popup = JBPopupFactory.getInstance().createListPopupBuilder(list) + .setTitle("Navigate to") + .setItemChoosenCallback { + val fileInfo = list.selectedValue as InlineInfo + OpenFileHyperlinkInfo(project, fileInfo.file, fileInfo.line).navigate(project) + } + .createPopup() + + if (hyperlinkLocationPoint != null) { + popup.show(hyperlinkLocationPoint) + } + else { + popup.showInFocusCenter() + } + } + } + + override fun getDescriptor(): OpenFileDescriptor? { + val file = inlineInfo.firstOrNull() + return file?.let { OpenFileDescriptor(project, file.file, file.line, 0) } + } + + sealed class InlineInfo(val prefix: String, val file: VirtualFile, val line: Int) { + class CallSiteInfo(file: VirtualFile, line: Int): InlineInfo("inline function call site", file, line) + class InlineFunctionBodyInfo(file: VirtualFile, line: Int): InlineInfo("inline function body", file, line) + } + + private class InlineInfoCellRenderer : SimpleColoredComponent(), ListCellRenderer { + init { + isOpaque = true + } + + override fun getListCellRendererComponent( + list: JList?, + value: InlineInfo?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + + clear() + + if (value != null) { + append(value.prefix) + } + + if (isSelected) { + background = list?.selectionBackground + foreground = list?.selectionForeground + } + else { + background = list?.background + foreground = list?.foreground + } + + return this + } + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt b/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt index 2c26660fc81..eff75a391b3 100644 --- a/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt +++ b/idea/src/org/jetbrains/kotlin/idea/filters/KotlinExceptionFilter.kt @@ -27,6 +27,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope import org.jetbrains.kotlin.codegen.inline.FileMapping import org.jetbrains.kotlin.codegen.inline.InlineCodegenUtil +import org.jetbrains.kotlin.codegen.inline.SMAP import org.jetbrains.kotlin.codegen.inline.SMAPParser import org.jetbrains.kotlin.idea.refactoring.getLineCount import org.jetbrains.kotlin.idea.refactoring.toPsiFile @@ -36,6 +37,7 @@ import org.jetbrains.kotlin.load.kotlin.JvmVirtualFileFinder import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.tail +import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.kotlin.utils.addToStdlib.check import org.jetbrains.org.objectweb.asm.ClassReader @@ -67,11 +69,15 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter val virtualFile = file.virtualFile ?: return null - return virtualFileForInlineCall(jvmClassName, virtualFile, lineNumber + 1, project) ?: - OpenFileHyperlinkInfo(project, virtualFile, lineNumber) + val hyperlinkInfoForInline = getHyperlinkInfoIfInline(jvmClassName, virtualFile, lineNumber + 1, project) + if (hyperlinkInfoForInline != null) { + return hyperlinkInfoForInline + } + + return OpenFileHyperlinkInfo(project, virtualFile, lineNumber) } - private fun virtualFileForInlineCall(jvmName: JvmClassName, file: VirtualFile, lineNumber: Int, project: Project): OpenFileHyperlinkInfo? { + private fun getHyperlinkInfoIfInline(jvmName: JvmClassName, file: VirtualFile, lineNumber: Int, project: Project): InlineFunctionHyperLinkInfo? { val fqNameWithInners = jvmName.fqNameForClassNameWithoutDollars.tail(jvmName.packageFqName) if (ProjectRootsUtil.isLibrarySourceFile(project, file)) { @@ -111,10 +117,30 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter return null } - private fun readDebugInfoForInlineFun(bytes: ByteArray, line: Int, project: Project): OpenFileHyperlinkInfo? { - val debugInfo = readDebugInfo(bytes) ?: return null + private fun readDebugInfoForInlineFun(bytes: ByteArray, line: Int, project: Project): InlineFunctionHyperLinkInfo? { + val smapData = readDebugInfo(bytes) ?: return null - val mappings = SMAPParser.parse(debugInfo) + val inlineInfo = arrayListOf() + + val (inlineFunctionBodyFile, inlineFunctionBodyLine) = parseStrata(smapData.kotlin1, line, project) ?: return null + inlineInfo.add(InlineFunctionHyperLinkInfo.InlineInfo.InlineFunctionBodyInfo( + inlineFunctionBodyFile.virtualFile, + inlineFunctionBodyLine)) + + val kotlin2 = parseStrata(smapData.kotlin2, line, project) + if (kotlin2 != null) { + inlineInfo.add(InlineFunctionHyperLinkInfo.InlineInfo.CallSiteInfo( + kotlin2.first.virtualFile, + kotlin2.second)) + } + + return InlineFunctionHyperLinkInfo(project, inlineInfo) + } + + private fun parseStrata(strata: String?, line: Int, project: Project): Pair? { + if (strata == null) return null + + val mappings = SMAPParser.parse(strata) val mappingInfo = mappings.fileMappings.firstOrNull { it.getIntervalIfContains(line) != null @@ -122,10 +148,11 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter val newJvmName = JvmClassName.byInternalName(mappingInfo.path) val newSourceFile = DebuggerUtils.findSourceFileForClassIncludeLibrarySources(project, searchScope, newJvmName, mappingInfo.name) ?: return null - return OpenFileHyperlinkInfo(project, newSourceFile.virtualFile, mappingInfo.getIntervalIfContains(line)!!.map(line) - 1) + + return newSourceFile to mappingInfo.getIntervalIfContains(line)!!.mapDestToSource(line) - 1 } - private fun readDebugInfo(bytes: ByteArray): String? { + private fun readDebugInfo(bytes: ByteArray): SmapData? { val cr = ClassReader(bytes); var debugInfo: String? = null cr.accept(object : ClassVisitor(InlineCodegenUtil.API) { @@ -133,7 +160,27 @@ class KotlinExceptionFilter(private val searchScope: GlobalSearchScope) : Filter debugInfo = debug } }, ClassReader.SKIP_FRAMES and ClassReader.SKIP_CODE) - return debugInfo + return debugInfo?.let { SmapData(it) } + } + + private class SmapData(debugInfo: String) { + var kotlin1: String? = null + private set + var kotlin2: String? = null + private set + + init { + val intervals = debugInfo.split(SMAP.END).filter { it.isNotBlank() } + when(intervals.count()) { + 1 -> { + kotlin1 = intervals[0] + SMAP.END + } + else -> { + kotlin1 = intervals[0] + SMAP.END + kotlin2 = intervals[1] + SMAP.END + } + } + } } private fun FileMapping.getIntervalIfContains(destLine: Int) = lineMappings.firstOrNull { it.contains(destLine) }