Add kotlin console repl

This commit is contained in:
Dmitry Kovanikov
2015-07-27 17:58:38 +03:00
committed by Pavel V. Talanov
parent 80cbee83ee
commit a11f411f7d
18 changed files with 1092 additions and 141 deletions
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="library" name="idea-full" level="project" />
<orderEntry type="module" module-name="frontend" />
<orderEntry type="module" module-name="util" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -0,0 +1,108 @@
/*
* Copyright 2010-2015 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.console
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.JavaParameters
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.JavaSdkType
import com.intellij.openapi.projectRoots.JdkUtil
import com.intellij.openapi.projectRoots.SimpleJavaSdkType
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.SystemProperties
import org.jetbrains.kotlin.console.actions.errorNotification
import org.jetbrains.kotlin.utils.PathUtil
import java.io.File
import java.util.concurrent.ConcurrentHashMap
import kotlin.platform.platformStatic
public class KotlinConsoleKeeper(val project: Project) {
private val consoleMap: MutableMap<VirtualFile, KotlinConsoleRunner> = ConcurrentHashMap()
fun getConsoleByVirtualFile(virtualFile: VirtualFile) = consoleMap.get(virtualFile)
fun putVirtualFileToConsole(virtualFile: VirtualFile, console: KotlinConsoleRunner) = consoleMap.put(virtualFile, console)
fun removeConsole(virtualFile: VirtualFile?) = consoleMap.remove(virtualFile)
fun run(module: Module): KotlinConsoleRunner? {
val path = module.moduleFilePath
val cmdLine = createCommandLine(module)
if (cmdLine == null) {
errorNotification(project, "<p>Module SDK not found</p>")
return null
}
val consoleRunner = KotlinConsoleRunner(path, cmdLine, project, REPL_TITLE)
consoleRunner.initAndRun()
consoleRunner.setupGutters()
return consoleRunner
}
private fun createCommandLine(module: Module): GeneralCommandLine? {
val javaParameters = createJavaParametersWithSdk(module)
val sdk = javaParameters.jdk ?: return null
val sdkType = sdk.sdkType
val exePath = (sdkType as JavaSdkType).getVMExecutablePath(sdk)
val commandLine = JdkUtil.setupJVMCommandLine(exePath, javaParameters, true)
// set parameters to run compiler
val paramList = commandLine.parametersList
paramList.clearAll()
// use to debug repl process
//paramList.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")
// set classpath for process to compiler
paramList.add("-cp")
paramList.add("${PathUtil.getKotlinPathsForIdeaPlugin().libPath.absolutePath}/*")
// set param to prevent process from repeating input backwards
paramList.add("-Drepl.ideMode=true")
// path to compiler and his options
paramList.add("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler")
return commandLine
}
private fun createJavaParametersWithSdk(module: Module?): JavaParameters {
val params = JavaParameters()
params.charset = null
if (module != null) {
val sdk = ModuleRootManager.getInstance(module).sdk
if (sdk != null && sdk.sdkType is JavaSdkType && File(sdk.homePath).exists()) {
params.jdk = sdk
}
}
if (params.jdk == null) {
params.jdk = SimpleJavaSdkType().createJdk("tmp", SystemProperties.getJavaHome())
}
return params
}
companion object {
private val REPL_TITLE = "Kotlin REPL"
platformStatic fun getInstance(project: Project) = ServiceManager.getService(project, javaClass<KotlinConsoleKeeper>())
}
}
@@ -0,0 +1,163 @@
/*
* Copyright 2010-2015 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.console
import com.intellij.execution.Executor
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.console.*
import com.intellij.execution.process.*
import com.intellij.execution.runners.AbstractConsoleRunnerWithHistory
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.event.DocumentAdapter
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.markup.*
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.console.actions.KtExecuteCommandAction
import org.jetbrains.kotlin.console.actions.logError
import org.jetbrains.kotlin.idea.JetLanguage
import java.awt.Color
import javax.swing.Icon
import kotlin.properties.Delegates
public class KotlinConsoleRunner(
val title: String,
private val cmdLine: GeneralCommandLine,
myProject: Project,
path: String?
) : AbstractConsoleRunnerWithHistory<LanguageConsoleView>(myProject, title, path) {
companion object {
private val HISTORY_GUTTER_ICON = AllIcons.Debugger.Console
private val EDITOR_GUTTER_ICON = AllIcons.Debugger.CommandLine
}
private val keyEventListener = KtConsoleKeyListener(this)
private var historyHighlighter: KotlinReplResultHighlighter by Delegates.notNull()
val history: MutableList<String> = arrayListOf()
override fun createProcess() = cmdLine.createProcess()
override fun createConsoleView(): LanguageConsoleView? {
val consoleView = LanguageConsoleBuilder().build(project, JetLanguage.INSTANCE)
consoleView.prompt = null
val historyEditor = consoleView.historyViewer
val consoleEditor = consoleView.consoleEditor
consoleEditor.contentComponent.addKeyListener(keyEventListener)
historyEditor.document.addDocumentListener(object : DocumentAdapter() {
override fun documentChanged(e: DocumentEvent): Unit =
if (historyEditor.document.textLength == 0) addGutterIcon(historyEditor, HISTORY_GUTTER_ICON)
})
historyHighlighter = KotlinReplResultHighlighter(historyEditor)
historyEditor.document.addDocumentListener(historyHighlighter)
consoleEditor.setPlaceholder("<Ctrl+Enter> to execute")
consoleEditor.setShowPlaceholderWhenFocused(true)
val placeholderAttrs = consoleEditor.foldingModel.placeholderAttributes
placeholderAttrs.foregroundColor = Color.GRAY
val executeAction = KtExecuteCommandAction(consoleView.virtualFile)
executeAction.registerCustomShortcutSet(CommonShortcuts.CTRL_ENTER, consoleView.consoleEditor.component)
return consoleView
}
override fun createProcessHandler(process: Process): OSProcessHandler {
val processHandler = KotlinReplOutputHandler(process, cmdLine.commandLineString)
historyHighlighter.rangeQueue = processHandler.rangeQueue
val consoleFile = consoleView.virtualFile
val keeper = KotlinConsoleKeeper.getInstance(project)
keeper.putVirtualFileToConsole(consoleFile, this)
processHandler.addProcessListener(object : ProcessAdapter() {
override fun processTerminated(event: ProcessEvent) {
keeper.removeConsole(consoleFile)
}
})
return processHandler
}
override fun createExecuteActionHandler() = object : ProcessBackedConsoleExecuteActionHandler(processHandler, false) {
override fun sendText(line: String) {
submitCommand(line)
}
}
override fun fillToolBarActions(toolbarActions: DefaultActionGroup,
defaultExecutor: Executor,
contentDescriptor: RunContentDescriptor
): List<AnAction> {
val actionList = arrayListOf<AnAction>(
createCloseAction(defaultExecutor, contentDescriptor),
createConsoleExecAction(consoleExecuteActionHandler)
)
toolbarActions.addAll(actionList)
return actionList
}
override fun createConsoleExecAction(consoleExecuteActionHandler: ProcessBackedConsoleExecuteActionHandler)
= ConsoleExecuteAction(consoleView, consoleExecuteActionHandler, "KotlinShellExecute", consoleExecuteActionHandler)
fun setupGutters() {
fun configureEditorGutter(editor: EditorEx, color: Color, icon: Icon) {
editor.settings.isLineMarkerAreaShown = true // hack to show gutter
editor.settings.isFoldingOutlineShown = true
editor.gutterComponentEx.setPaintBackground(true)
val editorColorScheme = editor.colorsScheme
editorColorScheme.setColor(EditorColors.GUTTER_BACKGROUND, color)
editor.colorsScheme = editorColorScheme
addGutterIcon(editor, icon)
}
val consoleView = consoleView
val lightGray = Color(0xF2, 0xF2, 0xF2)
val lightBlue = Color(0x93, 0xDE, 0xFF)
configureEditorGutter(consoleView.historyViewer, lightGray, HISTORY_GUTTER_ICON)
configureEditorGutter(consoleView.consoleEditor, lightBlue, EDITOR_GUTTER_ICON)
}
private fun addGutterIcon(editor: EditorEx, icon: Icon) {
val editorMarkup = editor.markupModel
val highlighter = editorMarkup.addRangeHighlighter(0, editor.document.textLength, HighlighterLayer.LAST, null, HighlighterTargetArea.LINES_IN_RANGE)
highlighter.gutterIconRenderer = object : GutterIconRenderer() {
override fun getIcon() = icon
override fun hashCode() = System.identityHashCode(this)
override fun equals(other: Any?) = this === other
}
}
public fun submitCommand(command: String) {
val res = command.trim()
if (res.isEmpty()) return
history.add(res)
keyEventListener.resetHistoryPosition()
val processInputOS = processHandler.processInput ?: return logError(javaClass, "<p>Broken process stream</p>")
val charset = (processHandler as? BaseOSProcessHandler)?.charset ?: Charsets.UTF_8
val bytes = ("$res\n").toByteArray(charset)
processInputOS.write(bytes)
processInputOS.flush()
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2010-2015 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.console
import com.intellij.execution.process.OSProcessHandler
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil
import org.w3c.dom.Element
import org.xml.sax.InputSource
import java.io.ByteArrayInputStream
import java.nio.charset.Charset
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import javax.xml.parsers.DocumentBuilderFactory
public class KotlinReplOutputHandler(
process: Process,
commandLine: String
) : OSProcessHandler(process, commandLine) {
private val dBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val rangeQueue: Queue<TextRange> = ConcurrentLinkedQueue()
override fun notifyTextAvailable(text: String, key: Key<*>?) {
// skip "/usr/lib/jvm/java-8-oracle/bin/java -cp ..." intro
if (!text.startsWith("<?xml version=")) return super.notifyTextAvailable(text, key)
val output = dBuilder.parse(strToSource(text))
val root = output.firstChild as Element
val outputType = root.getAttribute("type")
val content = StringUtil.unescapeStringCharacters(root.textContent).trim()
when (outputType) {
"ORDINARY" -> super.notifyTextAvailable("$content\n", key)
"REPORT" -> {
val report = dBuilder.parse(strToSource(content, Charsets.UTF_16BE))
val entries = report.getElementsByTagName("reportEntry")
for (i in 0..entries.length - 1) {
val reportEntry = entries.item(i) as Element
val rangeStart = reportEntry.getAttribute("rangeStart").toInt()
val rangeEnd = reportEntry.getAttribute("rangeEnd").toInt()
rangeQueue.add(TextRange(rangeStart, rangeEnd))
super.notifyTextAvailable("${StringUtil.unescapeXml(reportEntry.textContent)}\n", key)
}
}
}
}
private fun strToSource(s: String, encoding: Charset = Charsets.UTF_8) = InputSource(ByteArrayInputStream(s.toByteArray(encoding)))
}
@@ -0,0 +1,101 @@
/*
* Copyright 2010-2015 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.console
import com.intellij.execution.console.BasicGutterContentProvider
import com.intellij.openapi.editor.event.DocumentAdapter
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.TextRange
import com.intellij.ui.JBColor
import java.util.*
import kotlin.properties.Delegates
public class KotlinReplResultHighlighter(private val editor: EditorEx) : DocumentAdapter() {
private val EVAL_MARKERS_LENGTH = BasicGutterContentProvider.EVAL_IN_MARKER.length()
private val ERROR_PREFIX = "Error: "
private val WARNING_PREFIX = "Warning: "
var rangeQueue: Queue<TextRange> by Delegates.notNull()
private var curLine = 0
private enum class LineType(val color: JBColor) {
ERROR(JBColor.RED), WARNING(JBColor.YELLOW), USUAL(JBColor.BLACK)
}
private fun lineTypeByPrefix(s: String) =
if (s.startsWith(ERROR_PREFIX))
LineType.ERROR
else if (s.startsWith(WARNING_PREFIX))
LineType.WARNING
else
LineType.USUAL
override fun documentChanged(e: DocumentEvent) {
val document = editor.document
val docText = document.text
if (docText.isEmpty()) {
curLine = 0
return
}
val lastErrorPos = docText.lastIndexOf(ERROR_PREFIX)
val lastWarningPos = docText.lastIndexOf(WARNING_PREFIX)
if (lastErrorPos <= curLine && lastWarningPos <= curLine) return
val text = document.text
val totalLines = document.lineCount
var codeLineOffset = -1
while (curLine < totalLines) {
val lineStart = document.getLineStartOffset(curLine)
val lineEnd = document.getLineEndOffset(curLine)
val lineText = text.substring(lineStart, lineEnd)
val lineType = lineTypeByPrefix(lineText)
if (lineType != LineType.USUAL) {
if (codeLineOffset == -1) codeLineOffset = document.getLineStartOffset(curLine - 1) + EVAL_MARKERS_LENGTH
highlightLine(codeLineOffset, lineStart, lineEnd, lineType)
}
curLine++
}
}
private fun highlightLine(codeLineOffset: Int, msgLineStart: Int, msgLineEne: Int, lineType: LineType) {
val historyMarkup = editor.markupModel
// highlight error or warning message
val msgTextAttributes = TextAttributes()
msgTextAttributes.foregroundColor = lineType.color
msgTextAttributes.backgroundColor = JBColor.LIGHT_GRAY
historyMarkup.addRangeHighlighter(msgLineStart, msgLineEne, HighlighterLayer.LAST, msgTextAttributes, HighlighterTargetArea.LINES_IN_RANGE)
// highlight range in [codeLine]
val range = rangeQueue.poll()
val highlightedPlaceStart = codeLineOffset + range.startOffset
val highlightedPlaceSEnd = codeLineOffset + range.endOffset + if (range.endOffset == range.startOffset) 1 else 0
val errorWaveAttrs = TextAttributes()
errorWaveAttrs.effectType = EffectType.WAVE_UNDERSCORE
errorWaveAttrs.effectColor = lineType.color
historyMarkup.addRangeHighlighter(highlightedPlaceStart, highlightedPlaceSEnd, HighlighterLayer.LAST, errorWaveAttrs, HighlighterTargetArea.EXACT_RANGE)
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2010-2015 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.console
import com.intellij.openapi.command.WriteCommandAction
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
public class KtConsoleKeyListener(private val ktConsole: KotlinConsoleRunner) : KeyAdapter() {
private var historyPos = 0
private var prevCaretOffset = -1
private var unfinishedCommand = ""
private enum class HistoryMove {
UP, DOWN
}
public fun resetHistoryPosition() {
historyPos = ktConsole.history.size()
prevCaretOffset = -1
unfinishedCommand = ""
}
override fun keyReleased(e: KeyEvent): Unit = when (e.keyCode) {
KeyEvent.VK_UP -> moveHistoryCursor(HistoryMove.UP)
KeyEvent.VK_DOWN -> moveHistoryCursor(HistoryMove.DOWN)
}
private fun moveHistoryCursor(move: HistoryMove) {
val history = ktConsole.history
if (history.isEmpty()) return
val caret = ktConsole.consoleView.consoleEditor.caretModel
val document = ktConsole.consoleView.editorDocument
val curOffset = caret.offset
val curLine = document.getLineNumber(curOffset)
val totalLines = document.lineCount
val isMultiline = totalLines > 1
when (move) {
HistoryMove.UP -> {
if (curLine != 0 || (isMultiline && prevCaretOffset != 0)) {
prevCaretOffset = curOffset
return
}
if (historyPos == history.size()) {
unfinishedCommand = document.text
}
historyPos = Math.max(historyPos - 1, 0)
WriteCommandAction.runWriteCommandAction(ktConsole.project) {
document.setText(history[historyPos])
caret.moveToOffset(0)
prevCaretOffset = 0
}
}
HistoryMove.DOWN -> {
if (curLine != totalLines - 1 || (isMultiline && prevCaretOffset != document.textLength)) {
prevCaretOffset = curOffset
return
}
historyPos = Math.min(historyPos + 1, history.size())
WriteCommandAction.runWriteCommandAction(ktConsole.project) {
document.setText(if (historyPos == history.size()) unfinishedCommand else history[historyPos])
caret.moveToOffset(document.textLength)
prevCaretOffset = document.textLength
}
}
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2010-2015 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.console.actions
import com.intellij.execution.console.LanguageConsoleImpl
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.console.KotlinConsoleKeeper
public class KtExecuteCommandAction(private val consoleFile: VirtualFile) : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return errorNotification(null, "<p>Cannot find project</p>")
val ktConsole = KotlinConsoleKeeper.getInstance(project).getConsoleByVirtualFile(consoleFile) ?: return errorNotification(project, "<p>Action performed in not valid console</p>")
WriteCommandAction.runWriteCommandAction(project) {
val consoleView = ktConsole.consoleView
val document = consoleView.editorDocument
val command = document.text
document.setText(command)
LanguageConsoleImpl.printWithHighlighting(consoleView, consoleView.consoleEditor, TextRange(0, command.length()))
document.setText("")
ktConsole.submitCommand(command)
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2010-2015 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.console.actions
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.console.KotlinConsoleKeeper
fun errorNotification(project: Project?, message: String) {
val tag = "KOTLIN REPL ERROR"
val title = "Kotlin REPL Configuration Error"
Notifications.Bus.notify(Notification(tag, title, message, NotificationType.ERROR), project)
}
fun logError(cl: Class<*>, message: String) {
val logger = Logger.getInstance(cl)
logger.error(message)
}
public class RunKotlinConsoleAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return errorNotification(null, "<p>Project not found</p>")
val module = getModule(e) ?: return errorNotification(project, "<p>Module not found</p>")
KotlinConsoleKeeper.getInstance(project).run(module)
}
private fun getModule(e: AnActionEvent): Module? {
val project = e.project ?: return null
val file = CommonDataKeys.VIRTUAL_FILE.getData(e.dataContext)
if (file != null) {
val moduleForFile = ModuleUtilCore.findModuleForFile(file, project)
if (moduleForFile != null) return moduleForFile
}
return ModuleManager.getInstance(project).modules.firstOrNull()
}
}