Support alternative source popup for Kotlin files (KT-21958)

#KT-21958 Fixed
This commit is contained in:
Nikolay Krasko
2017-12-21 19:42:17 +03:00
parent 449fee74e8
commit 976a158ce1
3 changed files with 187 additions and 1 deletions
@@ -0,0 +1,170 @@
/*
* Copyright 2010-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 org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.ide.util.ModuleRendererFactory
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotifications
import com.intellij.ui.components.JBList
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
import org.jetbrains.kotlin.idea.stubindex.PackageIndexUtil.findFilesWithExactPackage
import org.jetbrains.kotlin.psi.KtFile
class KotlinAlternativeSourceNotificationProvider(private val myProject: Project) : EditorNotifications.Provider<EditorNotificationPanel>() {
override fun getKey(): Key<EditorNotificationPanel> {
return KEY
}
override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel? {
if (!DebuggerSettings.getInstance().SHOW_ALTERNATIVE_SOURCE) {
return null
}
val session = XDebuggerManager.getInstance(myProject).currentSession
if (session == null) {
FILE_PROCESSED_KEY.set(file, null)
return null
}
val position = session.currentPosition
if (file != position?.file) {
FILE_PROCESSED_KEY.set(file, null)
return null
}
if (DumbService.getInstance(myProject).isDumb) return null
val ktFile = PsiManager.getInstance(myProject).findFile(file) as? KtFile ?: return null
val packageFqName = ktFile.packageFqName
val fileName = ktFile.name
val alternativeKtFiles = findFilesWithExactPackage(packageFqName, GlobalSearchScope.allScope(myProject), myProject).filterTo(HashSet()) {
it.name == fileName
}
FILE_PROCESSED_KEY.set(file, true)
if (alternativeKtFiles.size <= 1) {
return null
}
val currentFirstAlternatives: Collection<KtFile> = listOf(ktFile) + alternativeKtFiles.filter { it != ktFile }
val frame = session.currentStackFrame
val locationDeclName: String? = when (frame) {
is JavaStackFrame -> {
val location = frame.descriptor.location
location?.declaringType()?.name()
}
else -> null
}
return AlternativeSourceNotificationPanel(currentFirstAlternatives, myProject, file, locationDeclName)
}
private class AlternativeSourceNotificationPanel(
alternatives: Collection<KtFile>,
project: Project,
file: VirtualFile,
locationDeclName: String?
) : EditorNotificationPanel() {
private class ComboBoxFileElement(val ktFile: KtFile) {
private val label: String by lazy(LazyThreadSafetyMode.NONE) {
val factory = ModuleRendererFactory.findInstance(ktFile)
val moduleRenderer = factory.moduleRenderer
moduleRenderer.getListCellRendererComponent(ourDummyList, ktFile, 1, false, false)
moduleRenderer.text ?: ""
}
override fun toString(): String = label
companion object {
private val ourDummyList = JBList<KtFile>()
}
}
init {
setText("Alternative source available for file ${file.name}")
val items = alternatives.map { ComboBoxFileElement(it) }
myLinksPanel.add(
ComboBox<ComboBoxFileElement>(items.toTypedArray()).apply {
addActionListener {
val context = DebuggerManagerEx.getInstanceEx(project).context
val session = context.debuggerSession
val ktFile = (selectedItem as ComboBoxFileElement).ktFile
val vFile = ktFile.containingFile.virtualFile
when {
session != null && vFile != null ->
session.process.managerThread.schedule(object : DebuggerCommandImpl() {
override fun action() {
if (!StringUtil.isEmpty(locationDeclName)) {
DebuggerUtilsEx.setAlternativeSourceUrl(locationDeclName, vFile.url, project)
}
DebuggerUIUtil.invokeLater {
FileEditorManager.getInstance(project).closeFile(file)
session.refresh(true)
}
}
})
else -> {
FileEditorManager.getInstance(project).closeFile(file)
ktFile.navigate(true)
}
}
}
})
createActionLabel("Disable") {
DebuggerSettings.getInstance().SHOW_ALTERNATIVE_SOURCE = false
FILE_PROCESSED_KEY.set(file, null)
val fileEditorManager = FileEditorManager.getInstance(project)
val editor = fileEditorManager.getSelectedEditor(file)
if (editor != null) {
fileEditorManager.removeTopComponent(editor, this)
}
}
}
}
companion object {
private val KEY = Key.create<EditorNotificationPanel>("KotlinAlternativeSource")
// FIXME: Share AlternativeSourceNotificationProvider.FILE_PROCESSED_KEY
@Suppress("UNCHECKED_CAST", "DEPRECATION")
private val FILE_PROCESSED_KEY = Key.findKeyByName("AlternativeSourceCheckDone") as Key<Boolean>
}
}
@@ -26,6 +26,7 @@ import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.requests.ClassPrepareRequestor
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.compiled.ClsFileImpl
import com.intellij.psi.search.GlobalSearchScope
@@ -87,7 +88,10 @@ class KotlinPositionManager(private val myDebugProcess: DebugProcess) : MultiReq
if (!DebuggerUtils.isKotlinSourceFile(fileName)) throw NoDataException.INSTANCE
val psiFile = getPsiFileByLocation(location)
val psiFile = getPsiFileByLocation(location)?.let {
replaceWithAlternativeSource(it, location)
}
if (psiFile == null) {
val isKotlinStrataAvailable = location.declaringType().containsKotlinStrata()
if (isKotlinStrataAvailable) {
@@ -148,6 +152,17 @@ class KotlinPositionManager(private val myDebugProcess: DebugProcess) : MultiReq
class KotlinReentrantSourcePosition(delegate: SourcePosition) : DelegateSourcePosition(delegate)
private fun replaceWithAlternativeSource(psiFile: PsiFile, location: Location): PsiFile {
fun findAlternativeSource(): PsiFile? {
val qName = location.declaringType().name()
val alternativeFileUrl = DebuggerUtilsEx.getAlternativeSourceUrl(qName, myDebugProcess.project) ?: return null
val alternativePsiFile = VirtualFileManager.getInstance().findFileByUrl(alternativeFileUrl) ?: return null
return psiFile.manager.findFile(alternativePsiFile)
}
return findAlternativeSource() ?: psiFile
}
// Returns a property or a constructor if debugger stops at class declaration
private fun getElementForDeclarationLine(location: Location, file: KtFile, lineNumber: Int): KtElement? {
val lineStartOffset = file.getLineStartOffset(lineNumber) ?: return null
+1
View File
@@ -94,6 +94,7 @@
<library.javaSourceRootDetector implementation="org.jetbrains.kotlin.idea.configuration.KotlinSourceRootDetector"/>
<editorNotificationProvider implementation="org.jetbrains.kotlin.idea.configuration.KotlinSetupEnvironmentNotificationProvider"/>
<editorNotificationProvider implementation="org.jetbrains.kotlin.idea.debugger.KotlinAlternativeSourceNotificationProvider"/>
<consoleFilterProvider implementation="org.jetbrains.kotlin.idea.run.KotlinConsoleFilterProvider"/>