package org.rascalmpl.repl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.rascalmpl.interpreter.asserts.Ambiguous;
import org.rascalmpl.interpreter.result.IRascalResult;
import org.rascalmpl.interpreter.utils.LimitedResultWriter.IOLimitReachedException;
import org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages;
import org.rascalmpl.interpreter.utils.StringUtils;
import org.rascalmpl.interpreter.utils.StringUtils.OffsetLengthTerm;
import org.rascalmpl.library.experiments.Compiler.RVM.Interpreter.ideservices.IDEServices;
import org.rascalmpl.library.util.PathConfig;
import org.rascalmpl.uri.URIResolverRegistry;
import org.rascalmpl.uri.URIUtil;
import org.rascalmpl.value.IConstructor;
import org.rascalmpl.value.ISourceLocation;
import org.rascalmpl.value.IValue;
import org.rascalmpl.value.IValueFactory;
import org.rascalmpl.value.io.StandardTextWriter;
import org.rascalmpl.value.type.Type;
import org.rascalmpl.values.ValueFactoryFactory;
import org.rascalmpl.values.uptr.RascalValueFactory;
import org.rascalmpl.values.uptr.TreeAdapter;
import jline.Terminal;
public abstract class BaseRascalREPL extends BaseREPL {
protected enum State {
FRESH,
CONTINUATION,
DEBUG,
DEBUG_CONTINUATION
}
private State currentState = State.FRESH;
protected State getState() {
return currentState;
}
private final static int LINE_LIMIT = 200;
private final static int CHAR_LIMIT = LINE_LIMIT * 20;
protected String currentPrompt = ReadEvalPrintDialogMessages.PROMPT;
private StringBuffer currentCommand;
private final StandardTextWriter indentedPrettyPrinter;
private final StandardTextWriter singleLinePrettyPrinter;
private final static IValueFactory VF = ValueFactoryFactory.getValueFactory();
public BaseRascalREPL(PathConfig pcfg, InputStream stdin, OutputStream stdout, boolean prettyPrompt, boolean allowColors,File persistentHistory, Terminal terminal, IDEServices ideServices)
throws IOException, URISyntaxException {
super(pcfg, stdin, stdout, prettyPrompt, allowColors, persistentHistory, terminal, ideServices);
if (terminal.isAnsiSupported() && allowColors) {
indentedPrettyPrinter = new ReplTextWriter();
singleLinePrettyPrinter = new ReplTextWriter(false);
}
else {
indentedPrettyPrinter = new StandardTextWriter();
singleLinePrettyPrinter = new StandardTextWriter(false);
}
}
@Override
protected String getPrompt() {
return currentPrompt;
}
@Override
protected void handleInput(String line) throws InterruptedException {
assert line != null;
try {
if (line.trim().length() == 0) {
// cancel command
getErrorWriter().println(ReadEvalPrintDialogMessages.CANCELLED);
currentPrompt = ReadEvalPrintDialogMessages.PROMPT;
currentCommand = null;
currentState = State.FRESH;
return;
}
if (currentCommand == null) {
// we are still at a new command so let's see if the line is a full command
if (isStatementComplete(line)) {
printResult(evalStatement(line, line));
}
else {
currentCommand = new StringBuffer(line);
currentPrompt = ReadEvalPrintDialogMessages.CONTINUE_PROMPT;
currentState = State.CONTINUATION;
return;
}
}
else {
currentCommand.append('\n');
currentCommand.append(line);
if (isStatementComplete(currentCommand.toString())) {
printResult(evalStatement(currentCommand.toString(), line));
currentPrompt = ReadEvalPrintDialogMessages.PROMPT;
currentCommand = null;
currentState = State.FRESH;
return;
}
}
} catch (IOException ie) {
throw new RuntimeException(ie);
} catch (Ambiguous e) {
getErrorWriter().println("Internal error: ambiguous command: " + TreeAdapter.yield(e.getTree()));
return;
}
}
@Override
protected void handleReset() throws InterruptedException {
handleInput("");
}
private void printResult(IRascalResult result) throws IOException {
if (result == null) {
return;
}
PrintWriter out = getOutputWriter();
IValue value = result.getValue();
if (value == null) {
out.println("ok");
out.flush();
return;
}
Type type = result.getType();
if (type.isAbstractData() && type.isStrictSubtypeOf(RascalValueFactory.Tree) && !type.isBottom()) {
out.print(type.toString());
out.print(": ");
// we unparse the tree
out.print("(" + type.toString() +") `");
TreeAdapter.yield((IConstructor)result.getValue(), true, out);
out.print("`");
}
else {
out.print(type.toString());
out.print(": ");
// limit both the lines and the characters
try (Writer wrt = new LimitedWriter(new LimitedLineWriter(out, LINE_LIMIT), CHAR_LIMIT)) {
indentedPrettyPrinter.write(value, wrt);
}
catch (IOLimitReachedException e) {
// ignore since this is what we wanted
}
}
out.println();
out.flush();
}
protected abstract PrintWriter getErrorWriter();
protected abstract PrintWriter getOutputWriter();
protected abstract boolean isStatementComplete(String command);
protected abstract IRascalResult evalStatement(String statement, String lastLine) throws InterruptedException;
/**
* provide which :set flags (:set profiling true for example)
* @return strings that can be set
*/
protected abstract SortedSet<String> getCommandLineOptions();
protected abstract Collection<String> completePartialIdentifier(String line, int cursor, String qualifier, String identifier);
protected abstract Collection<String> completeModule(String qualifier, String partialModuleName);
protected boolean isREPLCommand(String line){
return line.startsWith(":");
}
@Override
protected CompletionResult completeFragment(String line, int cursor) {
if (currentState == State.FRESH) {
String trimmedLine = line.trim();
if (isREPLCommand(trimmedLine)) {
return completeREPLCommand(line, cursor);
}
if (trimmedLine.startsWith("import ") || trimmedLine.startsWith("extend ")) {
return completeModule(line, cursor);
}
}
int locationStart = StringUtils.findRascalLocationStart(line, cursor);
if (locationStart != -1) {
return completeLocation(line, locationStart);
}
return completeIdentifier(line, cursor);
}
protected CompletionResult completeIdentifier(String line, int cursor) {
OffsetLengthTerm identifier = StringUtils.findRascalIdentifierAtOffset(line, cursor);
if (identifier != null) {
String[] qualified = StringUtils.splitQualifiedName(unescapeKeywords(identifier.term));
String qualifier = qualified.length == 2 ? qualified[0] : "";
String qualifee = qualified.length == 2 ? qualified[1] : qualified[0];
Collection<String> suggestions = completePartialIdentifier(line, cursor, qualifier, qualifee);
if (suggestions != null && ! suggestions.isEmpty()) {
return new CompletionResult(identifier.offset, escapeKeywords(suggestions));
}
}
return null;
}
private static final Pattern splitIdentifiers = Pattern.compile("[:][:]");
private static Collection<String> escapeKeywords(Collection<String> suggestions) {
return suggestions.stream()
.map(s -> splitIdentifiers.splitAsStream(s + " ") // add space such that the ending "::" is not lost
.map(BaseRascalREPL::escapeKeyword)
.collect(Collectors.joining("::")).trim()
)
.collect(Collectors.toList());
}
private static String unescapeKeywords(String term) {
return splitIdentifiers.splitAsStream(term + " ") // add space such that the ending "::" is not lost
.map(BaseRascalREPL::unescapeKeyword)
.collect(Collectors.joining("::")).trim()
;
}
private static final Set<String> RASCAL_KEYWORDS = new HashSet<String>();
private static void assureKeywordsAreScrapped() {
if (RASCAL_KEYWORDS.isEmpty()) {
synchronized (RASCAL_KEYWORDS) {
if (!RASCAL_KEYWORDS.isEmpty()) {
return;
}
String rascalGrammar = "";
try (Reader grammarReader = URIResolverRegistry.getInstance().getCharacterReader(ValueFactoryFactory.getValueFactory().sourceLocation("std", "", "/lang/rascal/syntax/Rascal.rsc"))) {
StringBuilder res = new StringBuilder();
char[] chunk = new char[8 * 1024];
int read;
while ((read = grammarReader.read(chunk, 0, chunk.length)) != -1) {
res.append(chunk, 0, read);
}
rascalGrammar = res.toString();
}
catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
if (!rascalGrammar.isEmpty()) {
/*
* keyword RascalKeywords
* = "o"
* | "syntax"
* | "keyword"
* | "lexical"
* ...
* ;
*/
Pattern findKeywordSection = Pattern.compile("^\\s*keyword([^=]|\\s)*=(?<keywords>([^;]|\\s)*);", Pattern.MULTILINE);
Matcher m = findKeywordSection.matcher(rascalGrammar);
if (m.find()) {
String keywords = "|" + m.group("keywords");
Pattern keywordEntry = Pattern.compile("\\s*[|]\\s*[\"](?<keyword>[^\"]*)[\"]");
m = keywordEntry.matcher(keywords);
while (m.find()) {
RASCAL_KEYWORDS.add(m.group("keyword"));
}
}
/*
* syntax BasicType
= \value: "value"
| \loc: "loc"
| \node: "node"
*/
Pattern findBasicTypeSection = Pattern.compile("^\\s*syntax\\s*BasicType([^=]|\\s)*=(?<keywords>([^;]|\\s)*);", Pattern.MULTILINE);
m = findBasicTypeSection.matcher(rascalGrammar);
if (m.find()) {
String keywords = "|" + m.group("keywords");
Pattern keywordEntry = Pattern.compile("\\s*[|][^:]*:\\s*[\"](?<keyword>[^\"]*)[\"]");
m = keywordEntry.matcher(keywords);
while (m.find()) {
RASCAL_KEYWORDS.add(m.group("keyword"));
}
}
}
if (RASCAL_KEYWORDS.isEmpty()) {
RASCAL_KEYWORDS.add("syntax");
}
}
}
}
private static String escapeKeyword(String s) {
assureKeywordsAreScrapped();
if (RASCAL_KEYWORDS.contains(s)) {
return "\\" + s;
}
return s;
}
private static String unescapeKeyword(String s) {
assureKeywordsAreScrapped();
if (s.startsWith("\\") && !s.contains("-")) {
return s.substring(1);
}
return s;
}
@Override
protected boolean supportsCompletion() {
return true;
}
@Override
protected boolean printSpaceAfterFullCompletion() {
return false;
}
private CompletionResult completeLocation(String line, int locationStart) {
int locationEnd = StringUtils.findRascalLocationEnd(line, locationStart);
try {
String locCandidate = line.substring(locationStart + 1, locationEnd + 1);
if (!locCandidate.contains("://")) {
return null;
}
ISourceLocation directory = VF.sourceLocation(new URI(locCandidate));
String fileName = "";
URIResolverRegistry reg = URIResolverRegistry.getInstance();
if (!reg.isDirectory(directory)) {
// split filename and directory
String fullPath = directory.getPath();
int lastSeparator = fullPath.lastIndexOf('/');
fileName = fullPath.substring(lastSeparator + 1);
fullPath = fullPath.substring(0, lastSeparator);
directory = VF.sourceLocation(directory.getScheme(), directory.getAuthority(), fullPath);
if (!reg.isDirectory(directory)) {
return null;
}
}
String[] filesInPath = reg.listEntries(directory);
URI directoryURI = directory.getURI();
Set<String> result = new TreeSet<>(); // sort it up
for (String currentFile : filesInPath) {
if (currentFile.startsWith(fileName)) {
URI currentDir = URIUtil.getChildURI(directoryURI, currentFile);
boolean isDirectory = reg.isDirectory(VF.sourceLocation(currentDir));
result.add(currentDir.toString() + (isDirectory ? "/" : "|"));
}
}
if (result.size() > 0) {
return new CompletionResult(locationStart + 1, result);
}
return null;
}
catch (URISyntaxException|IOException e) {
return null;
}
}
public CompletionResult completeModule(String line, int cursor) {
OffsetLengthTerm identifier = StringUtils.findRascalIdentifierAtOffset(line, line.length());
if (identifier != null) {
String[] qualified = StringUtils.splitQualifiedName(unescapeKeywords(identifier.term));
String qualifier = qualified.length == 2 ? qualified[0] : "";
String qualifee = qualified.length == 2 ? qualified[1] : qualified[0];
Collection<String> suggestions = completeModule(qualifier, qualifee);
if (suggestions != null && ! suggestions.isEmpty()) {
return new CompletionResult(identifier.offset, escapeKeywords(suggestions));
}
}
return null;
}
protected CompletionResult completeREPLCommand(String line, int cursor) {
return RascalCommandCompletion.complete(line, cursor, getCommandLineOptions(), (l,i) -> completeIdentifier(l,i), (l,i) -> completeModule(l,i));
}
}