[ide-console] Highlight repl process output

Add icons on gutter
Send messages to process in xml
This commit is contained in:
Dmitry Kovanikov
2015-08-07 17:33:35 +03:00
committed by Pavel V. Talanov
parent cb1756f5fa
commit cb5ff094bf
19 changed files with 543 additions and 237 deletions
@@ -114,9 +114,9 @@ public class ReplFromTerminal {
private void doRun() {
try {
replWriter.println("Welcome to Kotlin version " + KotlinVersion.VERSION +
replWriter.printlnInit("Welcome to Kotlin version " + KotlinVersion.VERSION +
" (JRE " + System.getProperty("java.runtime.version") + ")");
replWriter.println("Type :help for help, :quit for quit");
replWriter.printlnInit("Type :help for help, :quit for quit");
WhatNextAfterOneLine next = WhatNextAfterOneLine.READ_LINE;
while (true) {
next = one(next);
@@ -48,7 +48,7 @@ fun PsiElement.getModuleInfo(): IdeaModuleInfo {
)
}
val explicitModuleInfo = containingJetFile?.moduleInfo
val explicitModuleInfo = containingJetFile?.moduleInfo ?: (containingJetFile?.originalFile as? JetFile)?.moduleInfo
if (explicitModuleInfo is IdeaModuleInfo) return explicitModuleInfo
if (containingJetFile is JetCodeFragment) {
+1
View File
@@ -8,6 +8,7 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="library" name="idea-full" level="project" />
<orderEntry type="module" module-name="frontend" />
<orderEntry type="module" module-name="idea-analysis" />
<orderEntry type="module" module-name="util" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
@@ -0,0 +1,58 @@
/*
* 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.BaseOSProcessHandler
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.console.actions.logError
import org.jetbrains.kotlin.console.highlight.KotlinHistoryHighlighter
public class KotlinConsoleExecutor(
private val runner: KotlinConsoleRunner,
private val historyManager: KotlinConsoleHistoryManager
) {
private val XML_PREFIX = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
private val historyHighlighter = KotlinHistoryHighlighter(runner)
fun executeCommand() = WriteCommandAction.runWriteCommandAction(runner.project) {
val consoleView = runner.consoleView
val document = consoleView.editorDocument
val inputText = document.text.trim()
if (inputText.isNotEmpty()) {
val command = "$inputText\n"
document.setText("")
historyHighlighter.addAndHighlightNewCommand(command)
submitCommand(command)
}
}
private fun submitCommand(command: String) {
historyManager.updateHistory(command)
val processHandler = runner.processHandler
val processInputOS = processHandler.processInput ?: return logError(javaClass, "<p>Broken process stream</p>")
val charset = (processHandler as? BaseOSProcessHandler)?.charset ?: Charsets.UTF_8
val xmlRes = "$XML_PREFIX<input>${StringUtil.escapeXml(StringUtil.escapeLineBreak(command.trim()))}</input>"
val bytes = ("$xmlRes\n").toByteArray(charset)
processInputOS.write(bytes)
processInputOS.flush()
}
}
@@ -17,31 +17,45 @@
package org.jetbrains.kotlin.console
import com.intellij.openapi.command.WriteCommandAction
import org.jetbrains.kotlin.console.highlight.ReplOutputType
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
public class KtConsoleKeyListener(private val ktConsole: KotlinConsoleRunner) : KeyAdapter() {
public class KotlinConsoleHistoryManager(private val ktConsole: KotlinConsoleRunner) : KeyAdapter() {
private val history = arrayListOf<String>()
private var historyPos = 0
private var prevCaretOffset = -1
private var unfinishedCommand = ""
var lastCommandType = ReplOutputType.USER_OUTPUT
val lastCommandLength: Int
get() = history.last().length()
public fun updateHistory(command: String) {
if (lastCommandType == ReplOutputType.INCOMPLETE)
history[history.lastIndex] = "${history.last()}$command"
else
history.add(command)
// reset history positions
historyPos = history.size()
prevCaretOffset = -1
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)
KeyEvent.VK_UP -> moveHistoryCursor(HistoryMove.UP)
KeyEvent.VK_DOWN -> moveHistoryCursor(HistoryMove.DOWN)
KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT -> prevCaretOffset = ktConsole.consoleView.consoleEditor.caretModel.offset
}
private fun moveHistoryCursor(move: HistoryMove) {
val history = ktConsole.history
if (history.isEmpty()) return
val caret = ktConsole.consoleView.consoleEditor.caretModel
@@ -42,13 +42,9 @@ public class KotlinConsoleKeeper(val project: Project) {
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 cmdLine = createCommandLine(module) ?: return errorNotification(project, "<p>Module SDK not found</p>") let { null }
val consoleRunner = KotlinConsoleRunner(path, cmdLine, project, REPL_TITLE)
val consoleRunner = KotlinConsoleRunner(path, cmdLine, module, project, REPL_TITLE)
consoleRunner.initAndRun()
consoleRunner.setupGutters()
@@ -22,58 +22,53 @@ 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.module.Module
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.console.actions.KtExecuteCommandAction
import org.jetbrains.kotlin.console.actions.logError
import org.jetbrains.kotlin.console.gutter.KotlinConsoleGutterContentProvider
import org.jetbrains.kotlin.console.gutter.KotlinConsoleIndicatorRenderer
import org.jetbrains.kotlin.console.gutter.ReplIcons
import org.jetbrains.kotlin.console.highlight.KotlinReplOutputHighlighter
import org.jetbrains.kotlin.console.highlight.ReplColors
import org.jetbrains.kotlin.idea.JetLanguage
import org.jetbrains.kotlin.idea.caches.resolve.productionSourceInfo
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.psi.moduleInfo
import java.awt.Color
import java.awt.Font
import java.util.concurrent.ConcurrentHashMap
import javax.swing.Icon
import kotlin.properties.Delegates
public class KotlinConsoleRunner(
val title: String,
private val title: String,
private val cmdLine: GeneralCommandLine,
private val module: Module,
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()
private val editorToIndicator = ConcurrentHashMap<EditorEx, KotlinConsoleIndicatorRenderer>()
private val historyManager = KotlinConsoleHistoryManager(this)
val executor = KotlinConsoleExecutor(this, historyManager)
override fun createProcess() = cmdLine.createProcess()
override fun createConsoleView(): LanguageConsoleView? {
val consoleView = LanguageConsoleBuilder().build(project, JetLanguage.INSTANCE)
val consoleView = LanguageConsoleBuilder()
.gutterContentProvider(KotlinConsoleGutterContentProvider())
.build(project, JetLanguage.INSTANCE)
consoleView.prompt = null
configureModuleForConsoleFile(consoleView)
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
setupPlaceholder(consoleEditor)
consoleEditor.contentComponent.addKeyListener(historyManager)
val executeAction = KtExecuteCommandAction(consoleView.virtualFile)
executeAction.registerCustomShortcutSet(CommonShortcuts.CTRL_ENTER, consoleView.consoleEditor.component)
@@ -82,25 +77,20 @@ public class KotlinConsoleRunner(
}
override fun createProcessHandler(process: Process): OSProcessHandler {
val processHandler = KotlinReplOutputHandler(process, cmdLine.commandLineString)
historyHighlighter.rangeQueue = processHandler.rangeQueue
val processHandler = KotlinReplOutputHandler(KotlinReplOutputHighlighter(this, historyManager), process, cmdLine.commandLineString)
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)
}
override fun processTerminated(_: ProcessEvent) { keeper.removeConsole(consoleFile) }
})
return processHandler
}
override fun createExecuteActionHandler() = object : ProcessBackedConsoleExecuteActionHandler(processHandler, false) {
override fun sendText(line: String) {
submitCommand(line)
}
override fun runExecuteAction(_: LanguageConsoleView) = executor.executeCommand()
}
override fun fillToolBarActions(toolbarActions: DefaultActionGroup,
@@ -118,6 +108,22 @@ public class KotlinConsoleRunner(
override fun createConsoleExecAction(consoleExecuteActionHandler: ProcessBackedConsoleExecuteActionHandler)
= ConsoleExecuteAction(consoleView, consoleExecuteActionHandler, "KotlinShellExecute", consoleExecuteActionHandler)
private fun setupPlaceholder(editor: EditorEx) {
editor.setPlaceholder("<Ctrl+Enter> to execute")
editor.setShowPlaceholderWhenFocused(true)
val placeholderAttrs = TextAttributes()
placeholderAttrs.foregroundColor = ReplColors.PLACEHOLDER_COLOR
placeholderAttrs.fontType = Font.ITALIC
editor.setPlaceholderAttributes(placeholderAttrs)
}
private fun configureModuleForConsoleFile(consoleView: LanguageConsoleView) {
val consoleFile = consoleView.virtualFile
val jetFile = PsiManager.getInstance(project).findFile(consoleFile) as JetFile
jetFile.moduleInfo = module.productionSourceInfo()
}
fun setupGutters() {
fun configureEditorGutter(editor: EditorEx, color: Color, icon: Icon) {
editor.settings.isLineMarkerAreaShown = true // hack to show gutter
@@ -127,37 +133,27 @@ public class KotlinConsoleRunner(
editorColorScheme.setColor(EditorColors.GUTTER_BACKGROUND, color)
editor.colorsScheme = editorColorScheme
addGutterIcon(editor, icon)
addGutterIndicator(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)
configureEditorGutter(consoleView.historyViewer, ReplColors.HISTORY_GUTTER_COLOR, ReplIcons.HISTORY_INDICATOR)
configureEditorGutter(consoleView.consoleEditor, ReplColors.EDITOR_GUTTER_COLOR, ReplIcons.EDITOR_INDICATOR)
consoleView.consoleEditor.settings.isCaretRowShown = true
}
private fun addGutterIcon(editor: EditorEx, icon: Icon) {
fun addGutterIndicator(editor: EditorEx, icon: Icon) {
val indicator = KotlinConsoleIndicatorRenderer(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
}
val indicatorHighlighter = editorMarkup.addRangeHighlighter(
0, editor.document.textLength, HighlighterLayer.LAST, null, HighlighterTargetArea.LINES_IN_RANGE
)
indicatorHighlighter.gutterIconRenderer = indicator
editorToIndicator[editor] = indicator
}
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()
fun changeEditorIndicatorIcon(editor: EditorEx, newIcon: Icon) {
editorToIndicator[editor]?.indicatorIcon = newIcon
}
}
@@ -20,21 +20,23 @@ 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.jetbrains.kotlin.console.highlight.KotlinReplOutputHighlighter
import org.jetbrains.kotlin.diagnostics.Severity
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
data class SeverityDetails(val severity: Severity, val description: String, val range: TextRange)
public class KotlinReplOutputHandler(
private val outputHighlighter: KotlinReplOutputHighlighter,
process: Process,
commandLine: String
) : OSProcessHandler(process, commandLine) {
private val XML_START = "<?xml"
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
@@ -46,21 +48,38 @@ public class KotlinReplOutputHandler(
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))
"INITIAL_PROMPT" -> super.notifyTextAvailable("$content\n", key)
"USER_OUTPUT" -> outputHighlighter.printUserOutput(content)
"REPL_RESULT" -> outputHighlighter.printResultWithGutterIcon("$content\n")
"REPL_INCOMPLETE" -> outputHighlighter.changeIndicatorOnIncomplete()
"ERROR" -> {
val compilerMessages = arrayListOf<SeverityDetails>()
val report = dBuilder.parse(strToSource(content, Charsets.UTF_16))
val entries = report.getElementsByTagName("reportEntry")
for (i in 0..entries.length - 1) {
val reportEntry = entries.item(i) as Element
val severityLevel = reportEntry.getAttribute("severity").toSeverity()
val rangeStart = reportEntry.getAttribute("rangeStart").toInt()
val rangeEnd = reportEntry.getAttribute("rangeEnd").toInt()
val description = reportEntry.textContent
rangeQueue.add(TextRange(rangeStart, rangeEnd))
super.notifyTextAvailable("${StringUtil.unescapeXml(reportEntry.textContent)}\n", key)
compilerMessages.add(SeverityDetails(severityLevel, description, TextRange(rangeStart, rangeEnd)))
}
outputHighlighter.highlightErrors(compilerMessages)
}
else -> super.notifyTextAvailable("UNEXPECTED TEXT: $content\n", key)
}
}
private fun strToSource(s: String, encoding: Charset = Charsets.UTF_8) = InputSource(ByteArrayInputStream(s.toByteArray(encoding)))
private fun String.toSeverity() = when (this) {
"ERROR" -> Severity.ERROR
"WARNING" -> Severity.WARNING
"INFO" -> Severity.INFO
else -> throw IllegalArgumentException("Unsupported Severity: '$this'") // this case shouldn't occur
}
}
@@ -1,101 +0,0 @@
/*
* 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)
}
}
@@ -1,44 +0,0 @@
/*
* 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)
}
}
}
@@ -27,6 +27,7 @@ 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 com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.console.KotlinConsoleKeeper
fun errorNotification(project: Project?, message: String) {
@@ -59,4 +60,13 @@ public class RunKotlinConsoleAction : AnAction() {
return ModuleManager.getInstance(project).modules.firstOrNull()
}
}
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 an invalid console</p>")
ktConsole.executor.executeCommand()
}
}
@@ -0,0 +1,38 @@
/*
* 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.gutter
import com.intellij.openapi.editor.markup.GutterIconRenderer
import org.jetbrains.kotlin.console.SeverityDetails
import org.jetbrains.kotlin.diagnostics.Severity
public class KotlinConsoleErrorRenderer(private val messages: List<SeverityDetails>) : GutterIconRenderer() {
private fun msgType(severity: Severity) = when (severity) {
Severity.ERROR -> "Error:"
Severity.WARNING -> "Warning:"
Severity.INFO -> "Info:"
}
override fun getTooltipText(): String {
val htmlTooltips = messages map { "<b>${msgType(it.severity)}</b> ${it.description}" }
return "<html>${htmlTooltips.join("<hr size=1 noshade>")}</html>"
}
override fun getIcon() = ReplIcons.ERROR
override fun hashCode() = System.identityHashCode(this)
override fun equals(other: Any?) = this === other
}
@@ -0,0 +1,28 @@
/*
* 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.gutter
import com.intellij.execution.console.BasicGutterContentProvider
import com.intellij.openapi.editor.Editor
public class KotlinConsoleGutterContentProvider : BasicGutterContentProvider() {
/**
* This method overriding is needed to prevent [BasicGutterContentProvider] from adding some strange unicode
* symbols of zero width and to ease range highlighting.
*/
override fun beforeEvaluate(_: Editor) = Unit
}
@@ -0,0 +1,26 @@
/*
* 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.gutter
import com.intellij.openapi.editor.markup.GutterIconRenderer
import javax.swing.Icon
public class KotlinConsoleIndicatorRenderer(var indicatorIcon: Icon) : GutterIconRenderer() {
override fun getIcon() = indicatorIcon
override fun hashCode() = System.identityHashCode(this)
override fun equals(other: Any?) = this === other
}
@@ -0,0 +1,28 @@
/*
* 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.gutter
import com.intellij.icons.AllIcons
import javax.swing.Icon
public object ReplIcons {
public val HISTORY_INDICATOR: Icon = AllIcons.General.MessageHistory
public val EDITOR_INDICATOR: Icon = AllIcons.Debugger.CommandLine
public val INCOMPLETE_INDICATOR: Icon = AllIcons.Nodes.Desktop
public val RESULT: Icon = AllIcons.Vcs.Equal
public val ERROR: Icon = AllIcons.General.Error
}
@@ -0,0 +1,64 @@
/*
* 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.highlight
import com.intellij.execution.impl.ConsoleViewUtil
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import org.jetbrains.kotlin.console.KotlinConsoleRunner
import org.jetbrains.kotlin.console.gutter.ReplIcons
public class KotlinHistoryHighlighter(private val runner: KotlinConsoleRunner ) {
fun addAndHighlightNewCommand(command: String) {
val historyEditor = runner.consoleView.historyViewer
val historyDocument = historyEditor.document
if (historyDocument.textLength == 0) { // this will work first time after 'Clear all' action
historyDocument.setText("\n")
runner.addGutterIndicator(historyEditor, ReplIcons.HISTORY_INDICATOR)
}
val oldHistoryLength = historyDocument.textLength
historyDocument.insertString(oldHistoryLength, command)
EditorUtil.scrollToTheEnd(historyEditor)
val inputEditor = runner.consoleView.consoleEditor
val lexerHighlighter = inputEditor.highlighter as? LexerEditorHighlighter ?: return
val syntaxHighlighter = lexerHighlighter.syntaxHighlighter ?: return
val lexer = syntaxHighlighter.highlightingLexer
lexer.start(command, 0, command.length(), 0)
val historyMarkupModel = historyEditor.markupModel
while (true) {
val tokenType = lexer.tokenType ?: break
val tokenStartOffset = oldHistoryLength + lexer.tokenStart
val tokenEndOffset = oldHistoryLength + lexer.tokenEnd
val contentType = ConsoleViewUtil.getContentTypeForToken(tokenType, syntaxHighlighter)
historyMarkupModel.addRangeHighlighter(
tokenStartOffset, tokenEndOffset, HighlighterLayer.LAST,
contentType.attributes, HighlighterTargetArea.EXACT_RANGE
)
lexer.advance()
}
}
}
@@ -0,0 +1,138 @@
/*
* 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.highlight
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfoType
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.colors.CodeInsightColors
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.psi.PsiDocumentManager
import org.jetbrains.kotlin.console.KotlinConsoleHistoryManager
import org.jetbrains.kotlin.console.KotlinConsoleRunner
import org.jetbrains.kotlin.console.SeverityDetails
import org.jetbrains.kotlin.console.gutter.KotlinConsoleErrorRenderer
import org.jetbrains.kotlin.console.gutter.KotlinConsoleIndicatorRenderer
import org.jetbrains.kotlin.console.gutter.ReplIcons
import org.jetbrains.kotlin.diagnostics.Severity
enum class ReplOutputType {
USER_OUTPUT,
RESULT,
INCOMPLETE,
ERROR
}
public class KotlinReplOutputHighlighter(
private val runner: KotlinConsoleRunner,
private val historyManager: KotlinConsoleHistoryManager
) {
private val consoleView = runner.consoleView
private val historyEditor = consoleView.historyViewer
private val historyDocument = historyEditor.document
private val historyMarkup = historyEditor.markupModel
private fun resetConsoleEditorIndicator() = runner.changeEditorIndicatorIcon(consoleView.consoleEditor, ReplIcons.EDITOR_INDICATOR)
private fun insertText(text: String): Pair<Int, Int> {
val oldLen = historyDocument.textLength
val newLen = oldLen + text.length()
historyDocument.insertString(oldLen, text)
EditorUtil.scrollToTheEnd(historyEditor)
return oldLen to newLen
}
fun printUserOutput(command: String) {
resetConsoleEditorIndicator()
historyManager.lastCommandType = ReplOutputType.USER_OUTPUT
consoleView.print(command, ReplColors.USER_OUTPUT_CONTENT_TYPE)
}
fun printResultWithGutterIcon(result: String) = WriteCommandAction.runWriteCommandAction(runner.project) {
resetConsoleEditorIndicator()
historyManager.lastCommandType = ReplOutputType.RESULT
val (startOffset, endOffset) = insertText(result)
val highlighter = historyMarkup.addRangeHighlighter(startOffset, endOffset, HighlighterLayer.LAST, null, HighlighterTargetArea.EXACT_RANGE)
highlighter.gutterIconRenderer = KotlinConsoleIndicatorRenderer(ReplIcons.RESULT)
}
fun changeIndicatorOnIncomplete() {
historyManager.lastCommandType = ReplOutputType.INCOMPLETE
runner.changeEditorIndicatorIcon(consoleView.consoleEditor, ReplIcons.INCOMPLETE_INDICATOR)
}
fun highlightErrors(compilerMessages: List<SeverityDetails>) = WriteCommandAction.runWriteCommandAction(runner.project) {
resetConsoleEditorIndicator()
historyManager.lastCommandType = ReplOutputType.ERROR
val lastCommandStartOffset = historyDocument.textLength - historyManager.lastCommandLength
val highlighterAndMessagesByLine = compilerMessages.filter {
it.severity == Severity.ERROR || it.severity == Severity.WARNING
}.groupBy { message ->
val cmdStart = lastCommandStartOffset + message.range.startOffset
historyEditor.document.getLineNumber(cmdStart)
}.values().map { messages ->
val highlighters = messages.map { message ->
val cmdStart = lastCommandStartOffset + message.range.startOffset
val cmdEnd = lastCommandStartOffset + message.range.endOffset
val textAttributes = getAttributesForSeverity(cmdStart, cmdEnd, message.severity)
historyMarkup.addRangeHighlighter(
cmdStart, cmdEnd, HighlighterLayer.LAST, textAttributes, HighlighterTargetArea.EXACT_RANGE
)
}
Pair(highlighters.first(), messages)
}
for ((highlighter, messages) in highlighterAndMessagesByLine) {
highlighter.gutterIconRenderer = KotlinConsoleErrorRenderer(messages)
}
}
private fun getAttributesForSeverity(start: Int, end: Int, severity: Severity): TextAttributes {
val attributes = when (severity) {
Severity.ERROR -> getAttributesForSeverity(HighlightInfoType.ERROR, HighlightSeverity.ERROR, CodeInsightColors.ERRORS_ATTRIBUTES, start, end)
Severity.WARNING -> getAttributesForSeverity(HighlightInfoType.WARNING, HighlightSeverity.WARNING, CodeInsightColors.WARNINGS_ATTRIBUTES, start, end)
Severity.INFO -> getAttributesForSeverity(HighlightInfoType.WEAK_WARNING, HighlightSeverity.WEAK_WARNING, CodeInsightColors.WEAK_WARNING_ATTRIBUTES, start, end)
}
return attributes
}
private fun getAttributesForSeverity(
infoType: HighlightInfoType,
severity: HighlightSeverity,
insightColors: TextAttributesKey,
start: Int,
end: Int
): TextAttributes {
val highlightInfo = HighlightInfo.newHighlightInfo(infoType).range(start, end).severity(severity).textAttributes(insightColors).create()
val psiFile = PsiDocumentManager.getInstance(runner.project).getPsiFile(runner.consoleView.consoleEditor.document)
val colorScheme = runner.consoleView.consoleEditor.colorsScheme
return highlightInfo?.getTextAttributes(psiFile, colorScheme) ?: TextAttributes()
}
}
@@ -0,0 +1,35 @@
/*
* 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.highlight
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.Colors
import com.intellij.ui.Gray
import com.intellij.ui.JBColor
import java.awt.Font
public object ReplColors {
public val HISTORY_GUTTER_COLOR: JBColor = JBColor(Gray.xF2, Gray.x41)
public val EDITOR_GUTTER_COLOR: JBColor = JBColor(Gray.xCF, Gray.x31)
public val PLACEHOLDER_COLOR: JBColor = JBColor.LIGHT_GRAY
public val USER_OUTPUT_CONTENT_TYPE: ConsoleViewContentType =
ConsoleViewContentType(
"KOTLIN_CONSOLE_USER_OUTPUT",
TextAttributes() apply { fontType = Font.ITALIC; foregroundColor = Colors.DARK_GREEN }
)
}
@@ -83,9 +83,9 @@ public class KotlinReplTest : PlatformTestCase() {
}
@Test fun testSimpleCommand() {
consoleRunner.submitCommand("1 + 1")
val docHistory = checkHistoryUpdate { x: String -> x.endsWith('2') }
consoleRunner.pipeline.submitCommand("1 + 1")
val docHistory = checkHistoryUpdate { x: String -> x.endsWith("2</output>") }
assertTrue(docHistory.endsWith('2'), "1 + 1 should be equal 2, but document history is: '$docHistory'")
assertTrue(docHistory.endsWith("2</output>"), "1 + 1 should be equal 2, but document history is: '$docHistory'")
}
}