From 058b539004729d4bb8007716923abcb4b61b3557 Mon Sep 17 00:00:00 2001 From: Dmitry Kovanikov Date: Thu, 27 Aug 2015 19:41:54 +0300 Subject: [PATCH] [ide-console][cli-repl] Decorations for smooth console view Icons on history commands Linebreaks between `command-result` blocks Exceptions stacktrace and icons Bold invitation --- .../kotlin/cli/jvm/repl/ReplFromTerminal.java | 16 +++-- .../jvm/repl/messages/ReplSystemOutWrapper.kt | 2 + .../kotlin/console/KotlinConsoleExecutor.kt | 8 +-- .../kotlin/console/KotlinConsoleRunner.kt | 1 + .../kotlin/console/KotlinReplOutputHandler.kt | 5 +- .../gutter/KotlinConsoleErrorRenderer.kt | 2 +- .../kotlin/console/gutter/ReplIcons.kt | 10 ++- .../highlight/KotlinHistoryHighlighter.kt | 60 +++++++++-------- .../highlight/KotlinReplOutputHighlighter.kt | 65 ++++++++++++++----- .../kotlin/console/highlight/ReplColors.kt | 6 ++ 10 files changed, 112 insertions(+), 63 deletions(-) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.java b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.java index ec421fbbaa5..e45c8474cbc 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.java +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.java @@ -218,11 +218,12 @@ public class ReplFromTerminal { private boolean oneCommand(@NotNull String command) throws Exception { List 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 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 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; } } diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemOutWrapper.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemOutWrapper.kt index 5b2057c8e7f..442348c0366 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemOutWrapper.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemOutWrapper.kt @@ -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) diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt index f36b77ab6c9..d8d136d0936 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt @@ -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) )}" + "" + val bytes = ("$xmlRes\n").toByteArray(charset) processInputOS.write(bytes) processInputOS.flush() diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleRunner.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleRunner.kt index 638afeb8e57..80029b0b6e9 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleRunner.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleRunner.kt @@ -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 diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplOutputHandler.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplOutputHandler.kt index cb4880fdc31..d04ec2830a2 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplOutputHandler.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplOutputHandler.kt @@ -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() diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleErrorRenderer.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleErrorRenderer.kt index 7cf24afc998..84064a56d82 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleErrorRenderer.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleErrorRenderer.kt @@ -32,7 +32,7 @@ public class KotlinConsoleErrorRenderer(private val messages: List${htmlTooltips.join("
")}" } - override fun getIcon() = ReplIcons.ERROR + override fun getIcon() = ReplIcons.COMPILER_ERROR override fun hashCode() = System.identityHashCode(this) override fun equals(other: Any?) = this === other } \ No newline at end of file diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/ReplIcons.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/ReplIcons.kt index 906f6d1e3bc..7d61e3b77df 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/ReplIcons.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/ReplIcons.kt @@ -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 } \ No newline at end of file diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinHistoryHighlighter.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinHistoryHighlighter.kt index 0cf7e763c8d..a059cd37fba 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinHistoryHighlighter.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinHistoryHighlighter.kt @@ -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") } } } \ No newline at end of file diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinReplOutputHighlighter.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinReplOutputHighlighter.kt index c8f274af405..de13f13783e 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinReplOutputHighlighter.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinReplOutputHighlighter.kt @@ -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 { + private fun textOffsets(text: String): Pair { + 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) = 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 { diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/ReplColors.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/ReplColors.kt index 5b7a3177624..83497edac5f 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/ReplColors.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/ReplColors.kt @@ -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",