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 c997d4e3cb0..bee60bec946 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 @@ -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); diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/getModuleInfo.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/getModuleInfo.kt index b2cad6480cf..9a82f1244c1 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/getModuleInfo.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/getModuleInfo.kt @@ -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) { diff --git a/idea/idea-repl/idea-repl.iml b/idea/idea-repl/idea-repl.iml index bf72f114c54..1718e7e5725 100644 --- a/idea/idea-repl/idea-repl.iml +++ b/idea/idea-repl/idea-repl.iml @@ -8,6 +8,7 @@ + diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt new file mode 100644 index 00000000000..80dc5359ef3 --- /dev/null +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt @@ -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 = "" + 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, "

Broken process stream

") + val charset = (processHandler as? BaseOSProcessHandler)?.charset ?: Charsets.UTF_8 + + val xmlRes = "$XML_PREFIX${StringUtil.escapeXml(StringUtil.escapeLineBreak(command.trim()))}" + val bytes = ("$xmlRes\n").toByteArray(charset) + processInputOS.write(bytes) + processInputOS.flush() + } +} \ No newline at end of file diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KtConsoleKeyListener.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleHistoryManager.kt similarity index 76% rename from idea/idea-repl/src/org/jetbrains/kotlin/console/KtConsoleKeyListener.kt rename to idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleHistoryManager.kt index 718fe14566e..5d21aa64a74 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KtConsoleKeyListener.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleHistoryManager.kt @@ -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() + 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 diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt index 04c2a0767d1..32ad6e08b5e 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt @@ -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, "

Module SDK not found

") - return null - } + val cmdLine = createCommandLine(module) ?: return errorNotification(project, "

Module SDK not found

") let { null } - val consoleRunner = KotlinConsoleRunner(path, cmdLine, project, REPL_TITLE) + val consoleRunner = KotlinConsoleRunner(path, cmdLine, module, project, REPL_TITLE) consoleRunner.initAndRun() consoleRunner.setupGutters() 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 598ee808ad5..243f57f259f 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleRunner.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleRunner.kt @@ -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(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 = arrayListOf() + private val editorToIndicator = ConcurrentHashMap() + 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(" 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(" 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, "

Broken process stream

") - 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 } } \ No newline at end of file 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 163b62e1474..cf086257b8f 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplOutputHandler.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplOutputHandler.kt @@ -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 = " = 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() + + 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 + } } \ No newline at end of file diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplResultHighlighter.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplResultHighlighter.kt deleted file mode 100644 index 12e9feeb9c7..00000000000 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinReplResultHighlighter.kt +++ /dev/null @@ -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 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) - } -} \ No newline at end of file diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/actions/KtExecuteCommandAction.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/actions/KtExecuteCommandAction.kt deleted file mode 100644 index 1e272fc8b35..00000000000 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/actions/KtExecuteCommandAction.kt +++ /dev/null @@ -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, "

Cannot find project

") - val ktConsole = KotlinConsoleKeeper.getInstance(project).getConsoleByVirtualFile(consoleFile) ?: return errorNotification(project, "

Action performed in not valid console

") - - 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) - } - } -} \ No newline at end of file diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/actions/RunKotlinConsoleAction.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/actions/RunExecuteActions.kt similarity index 82% rename from idea/idea-repl/src/org/jetbrains/kotlin/console/actions/RunKotlinConsoleAction.kt rename to idea/idea-repl/src/org/jetbrains/kotlin/console/actions/RunExecuteActions.kt index 4496514d4e3..82bc8f6d396 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/actions/RunKotlinConsoleAction.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/actions/RunExecuteActions.kt @@ -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, "

Cannot find project

") + val ktConsole = KotlinConsoleKeeper.getInstance(project).getConsoleByVirtualFile(consoleFile) ?: return errorNotification(project, "

Action performed in an invalid console

") + + ktConsole.executor.executeCommand() + } } \ No newline at end of file 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 new file mode 100644 index 00000000000..7cf24afc998 --- /dev/null +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleErrorRenderer.kt @@ -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) : 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 { "${msgType(it.severity)} ${it.description}" } + return "${htmlTooltips.join("
")}" + } + + override fun getIcon() = ReplIcons.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/KotlinConsoleGutterContentProvider.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleGutterContentProvider.kt new file mode 100644 index 00000000000..b2d5460770b --- /dev/null +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleGutterContentProvider.kt @@ -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 +} \ No newline at end of file diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleIndicatorRenderer.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleIndicatorRenderer.kt new file mode 100644 index 00000000000..5e922e818cf --- /dev/null +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/KotlinConsoleIndicatorRenderer.kt @@ -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 +} \ 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 new file mode 100644 index 00000000000..906f6d1e3bc --- /dev/null +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/gutter/ReplIcons.kt @@ -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 +} \ 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 new file mode 100644 index 00000000000..41b915935d2 --- /dev/null +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinHistoryHighlighter.kt @@ -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() + } + } +} \ 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 new file mode 100644 index 00000000000..4180b75899f --- /dev/null +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/KotlinReplOutputHighlighter.kt @@ -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 { + 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) = 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() + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..5b7a3177624 --- /dev/null +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/highlight/ReplColors.kt @@ -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 } + ) +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/console/KotlinReplTest.kt b/idea/tests/org/jetbrains/kotlin/idea/console/KotlinReplTest.kt index 8aab992d944..cf87d10bed0 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/console/KotlinReplTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/console/KotlinReplTest.kt @@ -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") } - assertTrue(docHistory.endsWith('2'), "1 + 1 should be equal 2, but document history is: '$docHistory'") + assertTrue(docHistory.endsWith("2"), "1 + 1 should be equal 2, but document history is: '$docHistory'") } } \ No newline at end of file