[ide-console][cli-repl] Decorations for smooth console view

Icons on history commands
Linebreaks between `command-result` blocks
Exceptions stacktrace and icons
Bold invitation
This commit is contained in:
Dmitry Kovanikov
2015-08-27 19:41:54 +03:00
committed by Pavel V. Talanov
parent 3271fae7ac
commit 058b539004
10 changed files with 112 additions and 63 deletions
@@ -218,11 +218,12 @@ public class ReplFromTerminal {
private boolean oneCommand(@NotNull String command) throws Exception {
List<String> split = splitCommand(command);
if (split.size() >= 1 && command.equals("help")) {
replWriter.println("Available commands:");
replWriter.println(":help show this help");
replWriter.println(":quit exit the interpreter");
replWriter.println(":dump bytecode dump classes to terminal");
replWriter.println(":load <file> load script from specified file");
replWriter.printlnHelp("Available commands:\n" +
":help show this help\n" +
":quit exit the interpreter\n" +
":dump bytecode dump classes to terminal\n" +
":load <file> load script from specified file"
);
return true;
}
else if (split.size() >= 2 && split.get(0).equals("dump") && split.get(1).equals("bytecode")) {
@@ -239,8 +240,9 @@ public class ReplFromTerminal {
return true;
}
else {
replWriter.println("Unknown command");
replWriter.println("Type :help for help");
replWriter.printlnHelp("Unknown command\n" +
"Type :help for help"
);
return true;
}
}
@@ -25,6 +25,7 @@ public class ReplSystemOutWrapper(private val standardOut: PrintStream, private
private enum class EscapeType {
INITIAL_PROMPT,
HELP_PROMPT,
USER_OUTPUT,
REPL_RESULT,
REPL_INCOMPLETE,
@@ -57,6 +58,7 @@ public class ReplSystemOutWrapper(private val standardOut: PrintStream, private
}
fun printlnInit(x: String) = printlnWithEscaping(x, EscapeType.INITIAL_PROMPT)
fun printlnHelp(x: String) = printlnWithEscaping(x, EscapeType.HELP_PROMPT)
fun printlnResult(x: Any?) = printlnWithEscaping(x.toString(), EscapeType.REPL_RESULT)
fun printlnIncomplete() = printlnWithEscaping("", EscapeType.REPL_INCOMPLETE)
fun printlnCompileError(x: String) = printlnWithEscaping(x, EscapeType.COMPILE_ERROR)
@@ -36,11 +36,8 @@ public class KotlinConsoleExecutor(
val inputText = document.text.trim()
if (inputText.isNotEmpty()) {
val command = "$inputText\n"
document.setText("")
historyHighlighter.addAndHighlightNewCommand(command)
submitCommand(command)
historyHighlighter.printNewCommandInHistory(inputText, historyManager.lastCommandType)
submitCommand("$inputText\n")
}
}
@@ -57,6 +54,7 @@ public class KotlinConsoleExecutor(
StringUtil.replace(command.trim(), SOURCE_CHARS, XML_REPLACEMENTS)
)}" +
"</input>"
val bytes = ("$xmlRes\n").toByteArray(charset)
processInputOS.write(bytes)
processInputOS.flush()
@@ -148,6 +148,7 @@ public class KotlinConsoleRunner(
configureEditorGutter(consoleEditor, ReplColors.EDITOR_GUTTER_COLOR, ReplIcons.EDITOR_INDICATOR)
historyEditor.settings.isUseSoftWraps = true
historyEditor.settings.additionalLinesCount = 0
consoleEditor.settings.isCaretRowShown = true
consoleEditor.settings.additionalLinesCount = 2
@@ -53,10 +53,11 @@ public class KotlinReplOutputHandler(
val output = dBuilder.parse(strToSource(text))
val root = output.firstChild as Element
val outputType = root.getAttribute("type")
val content = StringUtil.replace(root.textContent, XML_REPLACEMENTS, SOURCE_CHARS).trim()
val content = StringUtil.replace(root.textContent, XML_REPLACEMENTS, SOURCE_CHARS)
when (outputType) {
"INITIAL_PROMPT" -> super.notifyTextAvailable("$content\n", key)
"INITIAL_PROMPT" -> outputHighlighter.printInitialPrompt(content)
"HELP_PROMPT" -> outputHighlighter.printHelp(content)
"USER_OUTPUT" -> outputHighlighter.printUserOutput(content)
"REPL_RESULT" -> outputHighlighter.printResultWithGutterIcon(content)
"REPL_INCOMPLETE" -> outputHighlighter.changeIndicatorOnIncomplete()
@@ -32,7 +32,7 @@ public class KotlinConsoleErrorRenderer(private val messages: List<SeverityDetai
return "<html>${htmlTooltips.join("<hr size=1 noshade>")}</html>"
}
override fun getIcon() = ReplIcons.ERROR
override fun getIcon() = ReplIcons.COMPILER_ERROR
override fun hashCode() = System.identityHashCode(this)
override fun equals(other: Any?) = this === other
}
@@ -17,12 +17,18 @@
package org.jetbrains.kotlin.console.gutter
import com.intellij.icons.AllIcons
import org.jetbrains.kotlin.idea.JetIcons
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 EDITOR_INDICATOR: Icon = JetIcons.LAUNCH
public val INCOMPLETE_INDICATOR: Icon = AllIcons.Nodes.Desktop
public val COMMAND_MARKER: Icon = AllIcons.General.Run
// command result icons
public val SYSTEM_HELP: Icon = AllIcons.Actions.Menu_help
public val RESULT: Icon = AllIcons.Vcs.Equal
public val ERROR: Icon = AllIcons.General.Error
public val COMPILER_ERROR: Icon = AllIcons.General.Error
public val RUNTIME_EXCEPTION: Icon = AllIcons.General.BalloonWarning
}
@@ -16,60 +16,58 @@
package org.jetbrains.kotlin.console.highlight
import com.intellij.execution.impl.ConsoleViewUtil
import com.intellij.execution.console.LanguageConsoleImpl
import com.intellij.openapi.editor.ex.EditorEx
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 com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.console.KotlinConsoleRunner
import org.jetbrains.kotlin.console.gutter.KotlinConsoleIndicatorRenderer
import org.jetbrains.kotlin.console.gutter.ReplIcons
public class KotlinHistoryHighlighter(private val runner: KotlinConsoleRunner ) {
fun addAndHighlightNewCommand(command: String) {
val historyEditor = runner.consoleView.historyViewer
private val consoleView: LanguageConsoleImpl by lazy { runner.consoleView as LanguageConsoleImpl }
addLineBreakIfNeeded(historyEditor)
fun printNewCommandInHistory(trimmedCommandText: String, lastCommandType: ReplOutputType) {
val historyEditor = consoleView.historyViewer
val historyDocument = historyEditor.document
val oldHistoryLength = historyDocument.textLength
historyDocument.insertString(oldHistoryLength, command)
addLineBreakIfNeeded(historyEditor, lastCommandType)
val consoleEditor = consoleView.consoleEditor
val consoleDocument = consoleEditor.document
consoleDocument.setText(trimmedCommandText)
val startOffset = historyEditor.document.textLength
val endOffset = startOffset + trimmedCommandText.length()
LanguageConsoleImpl.printWithHighlighting(consoleView, consoleEditor, TextRange(0, consoleDocument.textLength))
consoleView.flushDeferredText()
EditorUtil.scrollToTheEnd(historyEditor)
consoleDocument.setText("")
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()
historyEditor.markupModel let {
it.addRangeHighlighter(startOffset, endOffset, HighlighterLayer.LAST, null, HighlighterTargetArea.EXACT_RANGE)
} apply {
gutterIconRenderer = KotlinConsoleIndicatorRenderer(ReplIcons.COMMAND_MARKER)
}
}
private fun addLineBreakIfNeeded(historyEditor: EditorEx) {
private fun addLineBreakIfNeeded(historyEditor: EditorEx, lastCommandType: ReplOutputType) {
val historyDocument = historyEditor.document
val historyText = historyDocument.text
val textLength = historyText.length()
if (!historyText.endsWith('\n')) {
val textLength = historyText.length()
historyDocument.insertString(textLength, "\n")
if (textLength == 0) // this will work first time after 'Clear all' action
runner.addGutterIndicator(historyEditor, ReplIcons.HISTORY_INDICATOR)
else if (lastCommandType != ReplOutputType.INCOMPLETE)
historyDocument.insertString(textLength + 1, "\n")
} else if (!historyText.endsWith("\n\n")) {
historyDocument.insertString(textLength, "\n")
}
}
}
@@ -18,6 +18,7 @@ package org.jetbrains.kotlin.console.highlight
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfoType
import com.intellij.execution.console.LanguageConsoleImpl
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.command.WriteCommandAction
@@ -35,6 +36,7 @@ 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
import javax.swing.Icon
enum class ReplOutputType {
USER_OUTPUT,
@@ -48,24 +50,42 @@ public class KotlinReplOutputHighlighter(
private val historyManager: KotlinConsoleHistoryManager
) {
private val project = runner.project
private val consoleView = runner.consoleView
private val consoleView = runner.consoleView as LanguageConsoleImpl
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> {
private fun textOffsets(text: String): Pair<Int, Int> {
consoleView.flushDeferredText() // flush before getting offsets to calculate them properly
val oldLen = historyDocument.textLength
val newLen = oldLen + text.length()
historyDocument.insertString(oldLen, text)
EditorUtil.scrollToTheEnd(historyEditor)
return oldLen to newLen
return Pair(oldLen, newLen)
}
fun printUserOutput(command: String) {
private fun printOutput(output: String, contentType: ConsoleViewContentType, icon: Icon) {
val (startOffset, endOffset) = textOffsets(output)
consoleView.print(output, contentType)
consoleView.flushDeferredText()
historyMarkup.addRangeHighlighter(
startOffset, endOffset, HighlighterLayer.LAST, null, HighlighterTargetArea.EXACT_RANGE
) apply { gutterIconRenderer = KotlinConsoleIndicatorRenderer(icon) }
}
fun printInitialPrompt(command: String) = consoleView.print(command, ReplColors.INITIAL_PROMPT_CONTENT_TYPE)
fun printHelp(help: String) = WriteCommandAction.runWriteCommandAction(project) {
resetConsoleEditorIndicator()
historyManager.lastCommandType = ReplOutputType.USER_OUTPUT
printOutput(help, ConsoleViewContentType.SYSTEM_OUTPUT, ReplIcons.SYSTEM_HELP)
}
fun printUserOutput(command: String) = WriteCommandAction.runWriteCommandAction(project) {
resetConsoleEditorIndicator()
historyManager.lastCommandType = ReplOutputType.USER_OUTPUT
consoleView.print(command, ReplColors.USER_OUTPUT_CONTENT_TYPE)
@@ -75,15 +95,21 @@ public class KotlinReplOutputHighlighter(
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)
printOutput(result, ConsoleViewContentType.NORMAL_OUTPUT, ReplIcons.RESULT)
}
fun changeIndicatorOnIncomplete() {
fun changeIndicatorOnIncomplete() = WriteCommandAction.runWriteCommandAction(project) {
historyManager.lastCommandType = ReplOutputType.INCOMPLETE
runner.changeEditorIndicatorIcon(consoleView.consoleEditor, ReplIcons.INCOMPLETE_INDICATOR)
// remove line breaks after incomplete part
val historyText = historyDocument.text
val historyLength = historyText.length()
val trimmedHistoryText = historyText.trim()
val whitespaceCharsToDelete = historyLength - trimmedHistoryText.length()
historyDocument.deleteString(historyLength - whitespaceCharsToDelete, historyLength)
EditorUtil.scrollToTheEnd(historyEditor)
}
fun highlightCompilerErrors(compilerMessages: List<SeverityDetails>) = WriteCommandAction.runWriteCommandAction(project) {
@@ -91,6 +117,11 @@ public class KotlinReplOutputHighlighter(
historyManager.lastCommandType = ReplOutputType.ERROR
val lastCommandStartOffset = historyDocument.textLength - historyManager.lastCommandLength
val lastCommandStartLine = historyDocument.getLineNumber(lastCommandStartOffset)
val historyCommandRunIndicator = historyMarkup.allHighlighters filter {
historyDocument.getLineNumber(it.startOffset) == lastCommandStartLine && it.gutterIconRenderer != null
} first { true }
val highlighterAndMessagesByLine = compilerMessages.filter {
it.severity == Severity.ERROR || it.severity == Severity.WARNING
}.groupBy { message ->
@@ -110,14 +141,18 @@ public class KotlinReplOutputHighlighter(
}
for ((highlighter, messages) in highlighterAndMessagesByLine) {
highlighter.gutterIconRenderer = KotlinConsoleErrorRenderer(messages)
if (historyDocument.getLineNumber(highlighter.startOffset) == lastCommandStartLine)
historyCommandRunIndicator.gutterIconRenderer = KotlinConsoleErrorRenderer(messages)
else
highlighter.gutterIconRenderer = KotlinConsoleErrorRenderer(messages)
}
}
fun printRuntimeError(errorText: String) {
fun printRuntimeError(errorText: String) = WriteCommandAction.runWriteCommandAction(project) {
resetConsoleEditorIndicator()
historyManager.lastCommandType = ReplOutputType.ERROR
consoleView.print("\t$errorText", ConsoleViewContentType.ERROR_OUTPUT)
printOutput(errorText, ConsoleViewContentType.ERROR_OUTPUT, ReplIcons.RUNTIME_EXCEPTION)
}
private fun getAttributesForSeverity(start: Int, end: Int, severity: Severity): TextAttributes {
@@ -27,6 +27,12 @@ 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 INITIAL_PROMPT_CONTENT_TYPE: ConsoleViewContentType =
ConsoleViewContentType(
"KOTLIN_CONSOLE_INITIAL_PROMPT",
TextAttributes() apply { fontType = Font.BOLD }
)
public val USER_OUTPUT_CONTENT_TYPE: ConsoleViewContentType =
ConsoleViewContentType(
"KOTLIN_CONSOLE_USER_OUTPUT",