mirror of
https://github.com/SpencerPark/IJava.git
synced 2025-04-16 11:26:09 +00:00
231 lines
9.3 KiB
Java
231 lines
9.3 KiB
Java
/*
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2017 Spencer Park
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
package io.github.spencerpark.ijava;
|
|
|
|
import io.github.spencerpark.jupyter.kernel.BaseKernel;
|
|
import io.github.spencerpark.jupyter.kernel.LanguageInfo;
|
|
import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
|
|
import io.github.spencerpark.jupyter.kernel.util.CharPredicate;
|
|
import io.github.spencerpark.jupyter.messages.Header;
|
|
import io.github.spencerpark.jupyter.messages.MIMEBundle;
|
|
import jdk.jshell.*;
|
|
|
|
import java.io.OutputStream;
|
|
import java.io.PrintStream;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.stream.Collectors;
|
|
|
|
public class JavaKernel extends BaseKernel {
|
|
private static OutputStream stdout = new LazyOutputStreamDelegate(() -> System.out);
|
|
private static OutputStream stderr = new LazyOutputStreamDelegate(() -> System.err);
|
|
private static final CharPredicate IDENTIFIER_CHAR = CharPredicate.builder()
|
|
.inRange('a', 'z')
|
|
.inRange('A', 'Z')
|
|
.inRange('0', '9')
|
|
.match('_')
|
|
.build();
|
|
private static final CharPredicate WS = CharPredicate.anyOf(" \t\n\r");
|
|
|
|
private final JShell shell;
|
|
private final SourceCodeAnalysis sourceAnalyzer;
|
|
|
|
private final LanguageInfo languageInfo;
|
|
private final String banner;
|
|
private final List<LanguageInfo.Help> helpLinks;
|
|
|
|
public JavaKernel() {
|
|
this.shell = JShell.builder()
|
|
.out(new PrintStream(stdout))
|
|
.err(new PrintStream(stderr))
|
|
.build();
|
|
this.sourceAnalyzer = this.shell.sourceCodeAnalysis();
|
|
this.languageInfo = new LanguageInfo.Builder("Java")
|
|
.version(Runtime.version().toString())
|
|
.mimetype("text/x-java-source")
|
|
.fileExtension(".java")
|
|
.pygments("java")
|
|
.codemirror("java")
|
|
.build();
|
|
this.banner = String.format("Java %s :: IJava kernel %s \nProtocol v%s implementation by %s %s",
|
|
Runtime.version().toString(),
|
|
IJava.VERSION,
|
|
Header.PROTOCOL_VERISON,
|
|
KERNEL_META.getOrDefault("project", "UNKNOWN"),
|
|
KERNEL_META.getOrDefault("version", "UNKNOWN")
|
|
);
|
|
this.helpLinks = List.of(
|
|
new LanguageInfo.Help("Java tutorial", "https://docs.oracle.com/javase/tutorial/java/nutsandbolts/index.html"),
|
|
new LanguageInfo.Help("IJava homepage", "https://github.com/SpencerPark/IJava")
|
|
);
|
|
}
|
|
|
|
@Override
|
|
public LanguageInfo getLanguageInfo() {
|
|
return this.languageInfo;
|
|
}
|
|
|
|
@Override
|
|
public String getBanner() {
|
|
return this.banner;
|
|
}
|
|
|
|
@Override
|
|
public List<LanguageInfo.Help> getHelpLinks() {
|
|
return this.helpLinks;
|
|
}
|
|
|
|
private SourceCodeAnalysis.CompletionInfo analyzeCompletion(String source) {
|
|
return this.sourceAnalyzer.analyzeCompletion(source);
|
|
}
|
|
|
|
private void printDiagnostics(Snippet snippet) {
|
|
this.shell.diagnostics(snippet)
|
|
.map(d -> d.getMessage(Locale.getDefault()))
|
|
.forEach(msg -> {
|
|
for (String line : NEWLINE_PATTERN.split(msg)) {
|
|
if (!line.trim().startsWith("location:"))
|
|
System.err.println(line);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public MIMEBundle eval(String expr) throws Exception {
|
|
String lastEvalResult = null;
|
|
SourceCodeAnalysis.CompletionInfo info;
|
|
for (info = analyzeCompletion(expr); info.completeness().isComplete(); info = analyzeCompletion(info.remaining())) {
|
|
String src = info.source();
|
|
for (SnippetEvent event : this.shell.eval(src)) {
|
|
if (event.causeSnippet() == null) {
|
|
// Fresh snippet
|
|
JShellException e = event.exception();
|
|
if (e != null) {
|
|
if (e instanceof EvalException) {
|
|
System.err.println(((EvalException) e).getExceptionClassName());
|
|
printDiagnostics(event.snippet());
|
|
} else if (e instanceof UnresolvedReferenceException) {
|
|
printDiagnostics(((UnresolvedReferenceException) e).getSnippet());
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
if (!event.status().isDefined()) {
|
|
printDiagnostics(event.snippet());
|
|
throw new IllegalArgumentException("Cannot compile '" + event.snippet().source().trim() + "'");
|
|
}
|
|
|
|
lastEvalResult = event.snippet().kind() == Snippet.Kind.EXPRESSION
|
|
? event.value()
|
|
: null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (info.completeness() != SourceCodeAnalysis.Completeness.EMPTY) {
|
|
// There is source that was not evaluated because it was not complete
|
|
// TODO raise a better exception
|
|
throw new IllegalArgumentException(String.format("Incomplete source code: '%s'", info.remaining()));
|
|
}
|
|
|
|
return lastEvalResult == null || lastEvalResult.isEmpty() ? null : new MIMEBundle(lastEvalResult);
|
|
}
|
|
|
|
@Override
|
|
public MIMEBundle inspect(String code, int at, boolean extraDetail) {
|
|
// Move the code position to the end of the identifier to make the inspection work at any
|
|
// point in the identifier. i.e "System.o|ut" or "System.out|" will return the same result.
|
|
while (at + 1 < code.length() && IDENTIFIER_CHAR.test(code.charAt(at + 1))) at++;
|
|
|
|
// If the next non-whitespace character is an opening paren '(' then this must be included
|
|
// in the documentation search to ensure it searches for a method call.
|
|
int parenIdx = at;
|
|
while (parenIdx + 1 < code.length() && WS.test(code.charAt(parenIdx + 1))) parenIdx++;
|
|
if (parenIdx + 1 < code.length() && code.charAt(parenIdx + 1) == '(') at = parenIdx + 1;
|
|
|
|
List<SourceCodeAnalysis.Documentation> documentations = this.sourceAnalyzer.documentation(code, at + 1, true);
|
|
if (documentations == null || documentations.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
MIMEBundle fmtDocs = new MIMEBundle(
|
|
documentations.stream()
|
|
.map(doc -> {
|
|
String formatted = doc.signature();
|
|
|
|
String javadoc = doc.javadoc();
|
|
if (javadoc != null) formatted += '\n' + javadoc;
|
|
|
|
return formatted;
|
|
}).collect(Collectors.joining("\n\n")
|
|
)
|
|
);
|
|
|
|
fmtDocs.putHTML(
|
|
documentations.stream()
|
|
.map(doc -> {
|
|
String formatted = doc.signature();
|
|
|
|
// TODO consider compiling the javadoc to html for pretty printing
|
|
String javadoc = doc.javadoc();
|
|
if (javadoc != null) formatted += "<br/>" + javadoc;
|
|
|
|
return formatted;
|
|
}).collect(Collectors.joining("<br/><br/>")
|
|
)
|
|
);
|
|
|
|
return fmtDocs;
|
|
}
|
|
|
|
@Override
|
|
public ReplacementOptions complete(String code, int at) {
|
|
int[] replaceStart = new int[1]; // As of now this is always the same as the cursor...
|
|
List<SourceCodeAnalysis.Suggestion> suggestions = this.sourceAnalyzer.completionSuggestions(code, at, replaceStart);
|
|
if (suggestions == null || suggestions.isEmpty()) return null;
|
|
|
|
List<String> options = suggestions.stream()
|
|
.sorted((s1, s2) ->
|
|
s1.matchesType()
|
|
? s2.matchesType() ? 0 : -1
|
|
: s2.matchesType() ? 1 : 0
|
|
)
|
|
.map(SourceCodeAnalysis.Suggestion::continuation)
|
|
.distinct()
|
|
.collect(Collectors.toList());
|
|
|
|
return new ReplacementOptions(options, replaceStart[0], at + 1);
|
|
}
|
|
|
|
@Override
|
|
public String isComplete(String code) {
|
|
return super.isComplete(code);
|
|
}
|
|
|
|
@Override
|
|
public void onShutdown(boolean isRestarting) {
|
|
this.shell.close();
|
|
}
|
|
}
|