Simple isComplete support for terminal frontends including a naive auto indenter. Closes #35

This commit is contained in:
SpencerPark 2018-10-01 00:32:20 -04:00
parent 2ad58cadc9
commit 992ed21fe5
3 changed files with 106 additions and 7 deletions

View File

@ -96,7 +96,7 @@ public class IJava {
String contents = new String(Files.readAllBytes(connectionFile));
JupyterSocket.JUPYTER_LOGGER.setLevel(Level.ALL);
JupyterSocket.JUPYTER_LOGGER.setLevel(Level.WARNING);
KernelConnectionProperties connProps = KernelConnectionProperties.parse(contents);
JupyterConnection connection = new JupyterConnection(connProps);

View File

@ -34,6 +34,9 @@ import io.github.spencerpark.jupyter.kernel.util.GlobFinder;
import io.github.spencerpark.jupyter.kernel.util.StringStyler;
import io.github.spencerpark.jupyter.kernel.util.TextColor;
import io.github.spencerpark.jupyter.messages.Header;
import io.github.spencerpark.jupyter.messages.publish.PublishError;
import io.github.spencerpark.jupyter.messages.reply.ErrorReply;
import io.github.spencerpark.jupyter.messages.reply.ExecuteReply;
import jdk.jshell.*;
import java.io.IOException;
@ -44,6 +47,18 @@ import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
public class JavaKernel extends BaseKernel {
public static String completeCodeSignifier() {
return BaseKernel.IS_COMPLETE_YES;
}
public static String invalidCodeSignifier() {
return BaseKernel.IS_COMPLETE_BAD;
}
public static String maybeCompleteCodeSignifier() {
return BaseKernel.IS_COMPLETE_MAYBE;
}
private static final CharPredicate IDENTIFIER_CHAR = CharPredicate.builder()
.inRange('a', 'z')
.inRange('A', 'Z')
@ -259,13 +274,21 @@ public class JavaKernel extends BaseKernel {
@Override
public DisplayData eval(String expr) throws Exception {
Object result = this.evalRaw(expr);
try {
Object result = this.evalRaw(expr);
if (result != null)
return result instanceof DisplayData
? (DisplayData) result
: this.getRenderer().render(result);
if (result != null)
return result instanceof DisplayData
? (DisplayData) result
: this.getRenderer().render(result);
} catch (Exception e) {
ErrorReply error = ErrorReply.of(e);
PublishError pubError = PublishError.of(e, this::formatError);
System.out.println(error);
System.out.println(pubError);
throw e;
}
return null;
}
@ -337,7 +360,7 @@ public class JavaKernel extends BaseKernel {
@Override
public String isComplete(String code) {
return super.isComplete(code);
return this.evaluator.isComplete(code);
}
@Override

View File

@ -23,11 +23,17 @@
*/
package io.github.spencerpark.ijava.execution;
import io.github.spencerpark.ijava.JavaKernel;
import jdk.jshell.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CodeEvaluator {
private static final Pattern WHITESPACE_PREFIX = Pattern.compile("(?:^|\r?\n)(?<ws>\\s*).*$");
private static final Pattern LAST_LINE = Pattern.compile("(?:^|\r?\n)(?<last>.*)$");
private static final String NO_MAGIC_RETURN = "\"__NO_MAGIC_RETURN\"";
private final JShell shell;
@ -38,6 +44,8 @@ public class CodeEvaluator {
private boolean isInitialized = false;
private final List<String> startupScripts;
private final String indentation = " ";
public CodeEvaluator(JShell shell, IJavaExecutionControlProvider executionControlProvider, String executionControlID, List<String> startupScripts) {
this.shell = shell;
this.executionControlProvider = executionControlProvider;
@ -144,6 +152,74 @@ public class CodeEvaluator {
return lastEvalResult;
}
private String computeIndentation(String partialStatement) {
// Find the indentation of the last line
Matcher m = WHITESPACE_PREFIX.matcher(partialStatement);
String currentIndentation = m.find() ? m.group("ws") : "";
m = LAST_LINE.matcher(partialStatement);
if (!m.find())
throw new Error("Pattern broken. Every string should have a last line.");
// If a brace or paren was opened on the last line and not closed, indent some more.
String lastLine = m.group("last");
int newlyOpenedBraces = -1;
int newlyOpenedParens = -1;
for (int i = 0; i < lastLine.length(); i++) {
switch (lastLine.charAt(i)) {
case '}':
// Ignore closing if one has not been opened on this line yet
if (newlyOpenedBraces == -1) continue;
// Otherwise close an opened one from this line
newlyOpenedBraces--;
break;
case ')':
// Same as for braces, but with the parens
if (newlyOpenedParens == -1) continue;
newlyOpenedParens--;
break;
case '{':
// A brace was opened on this line!
// If the first then get out og the -1 special case with an extra addition
if (newlyOpenedBraces == -1) newlyOpenedBraces++;
newlyOpenedBraces++;
break;
case '(':
if (newlyOpenedParens == -1) newlyOpenedParens++;
newlyOpenedParens++;
break;
}
}
return newlyOpenedBraces > 0 || newlyOpenedParens > 0
? currentIndentation + this.indentation
: currentIndentation;
}
public String isComplete(String code) {
SourceCodeAnalysis.CompletionInfo info = this.sourceAnalyzer.analyzeCompletion(code);
while (info.completeness().isComplete())
info = analyzeCompletion(info.remaining());
switch (info.completeness()) {
case UNKNOWN:
// Unknown means "bad code" and the only way to see if is complete is
// to execute it.
return JavaKernel.invalidCodeSignifier();
case COMPLETE:
case COMPLETE_WITH_SEMI:
case EMPTY:
return JavaKernel.completeCodeSignifier();
case CONSIDERED_INCOMPLETE:
case DEFINITELY_INCOMPLETE:
// Compute the indent of the last line and match it
return this.computeIndentation(info.remaining());
default:
// For completeness, return an "I don't know" if we somehow get down here
return JavaKernel.maybeCompleteCodeSignifier();
}
}
public void interrupt() {
IJavaExecutionControl executionControl =
this.executionControlProvider.getRegisteredControlByID(this.executionControlID);