[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:
committed by
Pavel V. Talanov
parent
3271fae7ac
commit
058b539004
@@ -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()
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
+29
-31
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
-15
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user