Add kotlin console repl

This commit is contained in:
Dmitry Kovanikov
2015-07-27 17:58:38 +03:00
committed by Pavel V. Talanov
parent 80cbee83ee
commit a11f411f7d
18 changed files with 1092 additions and 141 deletions
@@ -242,6 +242,36 @@ public final class AnalyzerWithCompilerReport {
return new SyntaxErrorReport(visitor.hasErrors, visitor.allErrorsAtEof);
}
public static SyntaxErrorReport reportSyntaxErrorsWithDiagnostics(@NotNull final PsiElement file, @NotNull final MessageCollector messageCollector,
@NotNull final List<Diagnostic> diagnostics) {
class ErrorReportingVisitor extends AnalyzingUtils.PsiErrorElementVisitor {
boolean hasErrors = false;
boolean allErrorsAtEof = true;
private <E extends PsiElement> void reportDiagnostic(E element, DiagnosticFactory0<E> factory, String message) {
MyDiagnostic<?> diagnostic = new MyDiagnostic<E>(element, factory, message);
AnalyzerWithCompilerReport.reportDiagnostic(diagnostic, messageCollector);
if (element.getTextRange().getStartOffset() != file.getTextRange().getEndOffset()) {
allErrorsAtEof = false;
}
hasErrors = true;
diagnostics.add(diagnostic);
}
@Override
public void visitErrorElement(PsiErrorElement element) {
String description = element.getErrorDescription();
reportDiagnostic(element, SYNTAX_ERROR_FACTORY, StringUtil.isEmpty(description) ? "Syntax error" : description);
}
}
ErrorReportingVisitor visitor = new ErrorReportingVisitor();
file.accept(visitor);
return new SyntaxErrorReport(visitor.hasErrors, visitor.allErrorsAtEof);
}
@Nullable
public AnalysisResult getAnalysisResult() {
return analysisResult;
@@ -18,6 +18,8 @@ package org.jetbrains.kotlin.cli.jvm.repl;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.testFramework.LightVirtualFile;
import jline.console.ConsoleReader;
import jline.console.history.FileHistory;
import org.jetbrains.annotations.NotNull;
@@ -26,6 +28,7 @@ import org.jetbrains.kotlin.config.CompilerConfiguration;
import org.jetbrains.kotlin.utils.UtilsPackage;
import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
@@ -36,6 +39,9 @@ public class ReplFromTerminal {
private Throwable replInitializationFailed;
private final Object waitRepl = new Object();
private final boolean ideMode;
private ReplSystemOutWrapper replWriter;
private final ConsoleReader consoleReader;
public ReplFromTerminal(
@@ -56,8 +62,24 @@ public class ReplFromTerminal {
}
}.start();
String replIdeMode = System.getProperty("repl.ideMode");
ideMode = replIdeMode != null && replIdeMode.equals("true");
try {
consoleReader = new ConsoleReader("kotlin", System.in, System.out, null);
OutputStream outStream = System.out;
if (ideMode) {
// consoleReader duplicates his input in his output stream;
// to prevent such behaviour we redirect his output to dummy output stream
VirtualFile consoleOutputRedirect = new LightVirtualFile("kotlinConsoleReplRedirect");
outStream = consoleOutputRedirect.getOutputStream(null);
// wrapper is required to escape every input;
// if user calls [System.setOut(...)] then undefined behaviour
replWriter = new ReplSystemOutWrapper(System.out);
System.setOut(replWriter);
}
consoleReader = new ConsoleReader("kotlin", System.in, outStream, null);
consoleReader.setHistoryEnabled(true);
consoleReader.setExpandEvents(false);
consoleReader.setHistory(new FileHistory(new File(new File(System.getProperty("user.home")), ".kotlin_history")));
@@ -65,6 +87,8 @@ public class ReplFromTerminal {
catch (Exception e) {
throw UtilsPackage.rethrow(e);
}
getReplInterpreter().setIdeMode(ideMode);
}
private ReplInterpreter getReplInterpreter() {
@@ -122,7 +146,9 @@ public class ReplFromTerminal {
@NotNull
private WhatNextAfterOneLine one(@NotNull WhatNextAfterOneLine next) {
try {
String line = consoleReader.readLine(next == WhatNextAfterOneLine.INCOMPLETE ? "... " : ">>> ");
String prompt = getPrompt(next);
String line = consoleReader.readLine(prompt);
if (line == null) {
return WhatNextAfterOneLine.QUIT;
}
@@ -145,6 +171,15 @@ public class ReplFromTerminal {
}
}
private String getPrompt(@NotNull WhatNextAfterOneLine next) {
String prompt = null;
// consoleReader always print prompt; in [ideMode] we don't allow it
if (!ideMode) {
prompt = next == WhatNextAfterOneLine.INCOMPLETE ? "... " : ">>> ";
}
return prompt;
}
@NotNull
private ReplInterpreter.LineResultType eval(@NotNull String line) {
ReplInterpreter.LineResult lineResult = getReplInterpreter().eval(line);
@@ -156,7 +191,11 @@ public class ReplFromTerminal {
else if (lineResult.getType() == ReplInterpreter.LineResultType.INCOMPLETE) {
}
else if (lineResult.getType() == ReplInterpreter.LineResultType.ERROR) {
System.out.print(lineResult.getErrorText());
if (ideMode) {
replWriter.printlnWithEscaping(lineResult.getErrorText(), EscapeType.REPORT);
} else {
System.out.print(lineResult.getErrorText());
}
}
else {
throw new IllegalStateException("unknown line result type: " + lineResult);
@@ -21,6 +21,8 @@ import com.google.common.collect.Lists;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFileFactory;
@@ -47,6 +49,7 @@ import org.jetbrains.kotlin.context.MutableModuleContext;
import org.jetbrains.kotlin.descriptors.ScriptDescriptor;
import org.jetbrains.kotlin.descriptors.impl.CompositePackageFragmentProvider;
import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl;
import org.jetbrains.kotlin.diagnostics.Diagnostic;
import org.jetbrains.kotlin.idea.JetLanguage;
import org.jetbrains.kotlin.name.FqName;
import org.jetbrains.kotlin.parsing.JetParserDefinition;
@@ -65,18 +68,30 @@ import org.jetbrains.kotlin.resolve.scopes.JetScope;
import org.jetbrains.kotlin.types.JetType;
import org.jetbrains.kotlin.utils.UtilsPackage;
import org.jetbrains.org.objectweb.asm.Type;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
import static org.jetbrains.kotlin.cli.jvm.config.ConfigPackage.getJvmClasspathRoots;
import static org.jetbrains.kotlin.cli.jvm.config.ConfigPackage.getModuleName;
@@ -102,6 +117,8 @@ public class ReplInterpreter {
private final ResolveSession resolveSession;
private final ScriptMutableDeclarationProviderFactory scriptDeclarationFactory;
private boolean ideMode;
public ReplInterpreter(@NotNull Disposable disposable, @NotNull CompilerConfiguration configuration) {
KotlinCoreEnvironment environment =
KotlinCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES);
@@ -159,12 +176,21 @@ public class ReplInterpreter {
c.getScripts().clear();
}
public void setIdeMode(boolean ideMode) {
this.ideMode = ideMode;
}
public enum LineResultType {
SUCCESS,
ERROR,
INCOMPLETE,
}
private enum ErrorType {
CODE_ERROR,
THROWABLE
}
public static class LineResult {
private final Object value;
private final boolean unit;
@@ -208,16 +234,77 @@ public class ReplInterpreter {
return new LineResult(value, unit, null, LineResultType.SUCCESS);
}
public static LineResult error(@NotNull String errorText) {
public static LineResult error(@NotNull String errorText, boolean ideMode,
@NotNull ErrorType errType, @Nullable Collection<Diagnostic> diagnostics
) {
if (errorText.isEmpty()) {
errorText = "<unknown error>";
}
else if (!errorText.endsWith("\n")) {
String[] lines = errorTextToLinesWithPreprocessing(errorText);
if (ideMode && errType == ErrorType.CODE_ERROR) {
assert diagnostics != null : "diagnostics should not be null in not-throwable case";
errorText = convertLinesToXML(lines, diagnostics);
}
else {
errorText = StringUtil.join(lines, "\n");
}
if (!errorText.endsWith("\n")) {
errorText += "\n";
}
return new LineResult(null, false, errorText, LineResultType.ERROR);
}
private static String[] errorTextToLinesWithPreprocessing(@NotNull String errorText) {
// cut "/line_i.kts:row:column" prefix and capitalize "error:" and "warning:"
String[] lines = errorText.split("\\n");
for (int i = 0; i < lines.length; i++) {
if (lines[i].contains("error:")) {
int pos = lines[i].indexOf("error:");
lines[i] = "E" + lines[i].substring(pos + 1);
}
else if (lines[i].contains("warning:")) {
int pos = lines[i].indexOf("warning:");
lines[i] = "W" + lines[i].substring(pos + 1);
}
}
return lines;
}
private static String convertLinesToXML(@NotNull String[] lines, @NotNull Collection<Diagnostic> diagnostics) {
try {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
Document errorReport = docBuilder.newDocument();
Element rootElement = errorReport.createElement("report");
errorReport.appendChild(rootElement);
Iterator<Diagnostic> di = diagnostics.iterator();
for (String s : lines) {
if (s.startsWith("Error:") || s.startsWith("Warning:")) {
TextRange errorRange = di.hasNext() ? di.next().getTextRanges().get(0) : TextRange.EMPTY_RANGE;
Element reportEntry = errorReport.createElement("reportEntry");
reportEntry.setAttribute("rangeStart", String.valueOf(errorRange.getStartOffset()));
reportEntry.setAttribute("rangeEnd", String.valueOf(errorRange.getEndOffset()));
reportEntry.appendChild(errorReport.createTextNode(StringUtil.escapeXml(s)));
rootElement.appendChild(reportEntry);
}
}
DOMImplementationLS domImplementation = (DOMImplementationLS) errorReport.getImplementation();
LSSerializer lsSerializer = domImplementation.createLSSerializer();
return lsSerializer.writeToString(errorReport);
} catch (ParserConfigurationException e) {
throw UtilsPackage.rethrow(e);
}
}
public static LineResult incomplete() {
return new LineResult(null, false, null, LineResultType.INCOMPLETE);
}
@@ -242,9 +329,10 @@ public class ReplInterpreter {
assert psiFile != null : "Script file not analyzed at line " + lineNumber + ": " + fullText;
ReplMessageCollectorWrapper errorCollector = new ReplMessageCollectorWrapper();
List<Diagnostic> syntaxDiagnostics = new ArrayList<Diagnostic>();
AnalyzerWithCompilerReport.SyntaxErrorReport syntaxErrorReport =
AnalyzerWithCompilerReport.reportSyntaxErrors(psiFile, errorCollector.getMessageCollector());
AnalyzerWithCompilerReport.reportSyntaxErrorsWithDiagnostics(psiFile, errorCollector.getMessageCollector(), syntaxDiagnostics);
if (syntaxErrorReport.isHasErrors() && syntaxErrorReport.isAllErrorsAtEof()) {
previousIncompleteLines.add(line);
@@ -254,7 +342,7 @@ public class ReplInterpreter {
previousIncompleteLines.clear();
if (syntaxErrorReport.isHasErrors()) {
return LineResult.error(errorCollector.getString());
return LineResult.error(errorCollector.getString(), ideMode, ErrorType.CODE_ERROR, syntaxDiagnostics);
}
prepareForTheNextReplLine(topDownAnalysisContext);
@@ -265,7 +353,7 @@ public class ReplInterpreter {
ScriptDescriptor scriptDescriptor = doAnalyze(psiFile, errorCollector);
if (scriptDescriptor == null) {
return LineResult.error(errorCollector.getString());
return LineResult.error(errorCollector.getString(), ideMode, ErrorType.CODE_ERROR, trace.getBindingContext().getDiagnostics().all());
}
List<Pair<ScriptDescriptor, Type>> earlierScripts = Lists.newArrayList();
@@ -303,7 +391,7 @@ public class ReplInterpreter {
scriptInstance = scriptInstanceConstructor.newInstance(constructorArgs);
}
catch (Throwable e) {
return LineResult.error(renderStackTrace(e.getCause()));
return LineResult.error(renderStackTrace(e.getCause()), ideMode, ErrorType.THROWABLE, null);
}
Field rvField = scriptClass.getDeclaredField("rv");
rvField.setAccessible(true);
@@ -0,0 +1,40 @@
/*
* 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.cli.jvm.repl
import com.intellij.openapi.util.text.StringUtil
import java.io.PrintStream
enum class EscapeType {
ORDINARY, REPORT
}
public class ReplSystemOutWrapper(private val standardOut: PrintStream) : PrintStream(standardOut, true) {
private val XML_PREFIX = "<?xml version=\"1.0\" encoding=\"UTF-16\"?>"
private fun xmlEscape(s: String, escapeType: EscapeType): String {
val singleLine = StringUtil.escapeLineBreak(s)
return "$XML_PREFIX<output type=\"$escapeType\">${StringUtil.escapeXml(singleLine)}</output>"
}
override fun print(x: Any) = printWithEscaping(x.toString())
private fun printWithEscaping(text: String, escapeType: EscapeType = EscapeType.ORDINARY) = super.print(xmlEscape(text, escapeType))
override fun println(x: String) = printlnWithEscaping(x)
override fun println(x: Any) = printlnWithEscaping(x.toString())
public fun printlnWithEscaping(text: String, escapeType: EscapeType = EscapeType.ORDINARY): Unit = super.println(xmlEscape(text, escapeType))
}