All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javarepl.Main Maven / Gradle / Ivy

There is a newer version: 431
Show newest version
package javarepl;

import com.googlecode.totallylazy.Option;
import com.googlecode.totallylazy.Sequence;
import com.googlecode.totallylazy.Sequences;
import com.googlecode.totallylazy.functions.Function1;
import javarepl.client.EvaluationResult;
import javarepl.client.JavaREPLClient;
import javarepl.completion.CompletionCandidate;
import javarepl.completion.CompletionResult;
import jline.console.ConsoleReader;
import jline.console.CursorBuffer;
import jline.console.completer.CandidateListCompletionHandler;
import jline.console.completer.CompletionHandler;
import jline.console.history.MemoryHistory;
import org.fusesource.jansi.AnsiConsole;

import java.io.IOException;
import java.util.*;

import static com.googlecode.totallylazy.Option.none;
import static com.googlecode.totallylazy.Option.some;
import static com.googlecode.totallylazy.Sequences.sequence;
import static com.googlecode.totallylazy.Strings.replaceAll;
import static com.googlecode.totallylazy.Strings.startsWith;
import static com.googlecode.totallylazy.functions.Callables.compose;
import static com.googlecode.totallylazy.numbers.Numbers.intValue;
import static com.googlecode.totallylazy.numbers.Numbers.valueOf;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.util.Arrays.asList;
import static javarepl.Utils.applicationVersion;
import static javarepl.Utils.randomServerPort;
import static javarepl.completion.CompletionResult.methods.fromJson;
import static javarepl.completion.CompletionResult.methods.toJson;
import static javax.tools.ToolProvider.getSystemJavaCompiler;

public class Main {

    private static Option process = none();
    private static ResultPrinter console;

    public static void main(String... args) throws Exception {
        console = new ResultPrinter(printColors(args));

        JavaREPLClient client = clientFor(hostname(args), port(args));
        ExpressionReader expressionReader = expressionReaderFor(client);

        Option expression = none();
        Option result = none();
        while (expression.isEmpty() || !result.isEmpty()) {
            expression = expressionReader.readExpression();

            if (!expression.isEmpty()) {
                result = client.execute(expression.get());
                if (!result.isEmpty())
                    console.printEvaluationResult(result.get());
            }
        }
    }

    private static JavaREPLClient clientFor(Option hostname, Option port) throws Exception {
        console.printInfo(format("Welcome to JavaREPL version %s (%s, Java %s)",
                applicationVersion(),
                getProperty("java.vm.name"),
                getProperty("java.version")));

        if (hostname.isEmpty() && port.isEmpty()) {
            return startNewLocalInstance("localhost", randomServerPort());
        } else {
            return connectToRemoteInstance(hostname.getOrElse("localhost"), port.getOrElse(randomServerPort()));
        }
    }

    private static JavaREPLClient connectToRemoteInstance(String hostname, Integer port) {
        JavaREPLClient replClient = new JavaREPLClient(hostname, port);

        if (!replClient.status().isRunning()) {
            console.printError("ERROR: Could not connect to remote REPL instance at http://" + hostname + ":" + port);
            System.exit(0);
        } else {
            console.printInfo("Connected to remote instance at http://" + hostname + ":" + port);
        }

        String remoteInstanceVersion = replClient.version();
        if (!remoteInstanceVersion.equals(applicationVersion())) {
            console.printError("WARNING: Client version (" + applicationVersion() + ") is different from remote instance version (" + remoteInstanceVersion + ")");
        }

        return replClient;
    }

    private static JavaREPLClient startNewLocalInstance(String hostname, Integer port) throws Exception {
        if (getSystemJavaCompiler() == null) {
            console.printError("\nERROR: Java compiler not found.\n" +
                    "This can occur when JavaREPL was run with JRE instead of JDK or JDK is not configured correctly.");
            System.exit(0);
        }

        console.printInfo("Type expression to evaluate, \u001B[32m:help\u001B[0m for more options or press \u001B[32mtab\u001B[0m to auto-complete.");

        ProcessBuilder builder = new ProcessBuilder("java", "-cp", System.getProperty("java.class.path"), Repl.class.getCanonicalName(), "--port=" + port);
        builder.redirectErrorStream(true);

        process = some(builder.start());
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                console.printInfo("\nTerminating...");
                process.get().destroy();
            }
        });

        JavaREPLClient replClient = new JavaREPLClient(hostname, port);
        if (!waitUntilInstanceStarted(replClient)) {
            console.printError("\nERROR: Could not start REPL instance at http://" + hostname + ":" + port);
            System.exit(0);
        }

        return replClient;
    }

    private static boolean waitUntilInstanceStarted(JavaREPLClient client) throws Exception {
        for (int i = 0; i < 500; i++) {
            Thread.sleep(10);
            if (client.status().isRunning())
                return true;
        }

        return false;
    }

    private static Option port(String[] args) {
        return sequence(args).find(startsWith("--port=")).map(compose(replaceAll("--port=", ""), compose(valueOf, intValue)));
    }

    private static Option hostname(String[] args) {
        return sequence(args).find(startsWith("--hostname=")).map(replaceAll("--hostname=", ""));
    }

    private static Boolean printColors(String[] args) {
        return !sequence(args).contains("--noColors");
    }

    private static ExpressionReader expressionReaderFor(final JavaREPLClient client) throws IOException {
        return new ExpressionReader(new Function1, String>() {
            private final ConsoleReader consoleReader;

            {
                consoleReader = new ConsoleReader(System.in, AnsiConsole.out);
                consoleReader.setCompletionHandler(new JlineCompletionHandler());
                consoleReader.setHistoryEnabled(true);
                consoleReader.setExpandEvents(false);
                consoleReader.addCompleter(clientCompleter());
            }

            public String call(Sequence lines) throws Exception {
                consoleReader.setPrompt(console.ansiColored(lines.isEmpty() ? "\u001B[1mjava> \u001B[0m" : "    \u001B[1m| \u001B[0m"));
                consoleReader.setHistory(clientHistory());
                return consoleReader.readLine();
            }

            private MemoryHistory clientHistory() throws Exception {
                MemoryHistory history = new MemoryHistory();
                for (String historyItem : client.history()) {
                    history.add(historyItem);
                }
                return history;
            }

            private jline.console.completer.Completer clientCompleter() {
                return (expression, cursor, candidates) -> {
                    try {
                        CompletionResult result = client.completions(expression);
                        candidates.addAll(asList(toJson(result)));
                        return result.candidates().isEmpty() ? -1 : result.position();
                    } catch (Exception e) {
                        return -1;
                    }
                };
            }
        });
    }


    /**
     * Copied from JLine sourcecode and heavily modified
     * Original sources: https://raw.github.com/jline/jline2/master/src/main/java/jline/console/completer/CandidateListCompletionHandler.java
     */
    public static class JlineCompletionHandler implements CompletionHandler {
        // TODO: handle quotes and escaped quotes && enable automatic escaping of whitespace

        public boolean complete(final ConsoleReader reader, final List candidatesJson, final int pos) throws IOException {
            CursorBuffer buf = reader.getCursorBuffer();
            CompletionResult completionResult = fromJson(sequence(candidatesJson).head().toString());
            Sequence candidatesToPrint = Sequences.empty(String.class);

            // if there is only one completion, then fill in the buffer
            if (completionResult.candidates().size() == 1) {
                CharSequence value = completionResult.candidates().head().value();

                // fail if the only candidate is the same as the current buffer
                if (value.equals(buf.toString())) {
                    return false;
                }

                setBuffer(reader, value, pos);
                candidatesToPrint = completionResult.candidates().flatMap(CompletionCandidate::forms);
            } else if (completionResult.candidates().size() > 1) {
                String value = getUnambiguousCompletions(completionResult.candidates());
                setBuffer(reader, value, pos);
                candidatesToPrint = completionResult.candidates().map(CompletionCandidate::value);
            }

            printCandidates(reader, candidatesToPrint.safeCast(CharSequence.class).toList());

            // redraw the current console buffer
            reader.drawLine();

            return true;
        }

        public static void setBuffer(final ConsoleReader reader, final CharSequence value, final int offset) throws
                IOException {
            while ((reader.getCursorBuffer().cursor > offset) && reader.backspace()) {
                // empty
            }

            reader.putString(value);
            reader.setCursorPosition(offset + value.length());
        }


        public static void printCandidates(final ConsoleReader reader, Collection candidates) throws
                IOException {
            Set distinct = new HashSet(candidates);

            if (distinct.size() > reader.getAutoprintThreshold()) {
                //noinspection StringConcatenation
                reader.print(Messages.DISPLAY_CANDIDATES.format(candidates.size()));
                reader.flush();

                int c;

                String noOpt = Messages.DISPLAY_CANDIDATES_NO.format();
                String yesOpt = Messages.DISPLAY_CANDIDATES_YES.format();
                char[] allowed = {yesOpt.charAt(0), noOpt.charAt(0)};

                while ((c = reader.readCharacter(allowed)) != -1) {
                    String tmp = new String(new char[]{(char) c});

                    if (noOpt.startsWith(tmp)) {
                        reader.println();
                        return;
                    } else if (yesOpt.startsWith(tmp)) {
                        break;
                    } else {
                        reader.beep();
                    }
                }
            }

            // copy the values and make them distinct, without otherwise affecting the ordering. Only do it if the sizes differ.
            if (distinct.size() != candidates.size()) {
                Collection copy = new ArrayList();

                for (CharSequence next : candidates) {
                    if (!copy.contains(next)) {
                        copy.add(next);
                    }
                }

                candidates = copy;
            }

            reader.println();
            reader.printColumns(candidates);
        }

        /**
         * Returns a root that matches all the {@link String} elements of the specified {@link List},
         * or null if there are no commonalities. For example, if the list contains
         * foobar, foobaz, foobuz, the method will return foob.
         */
        private String getUnambiguousCompletions(final Sequence candidates) {
            if (candidates == null || candidates.isEmpty()) {
                return null;
            }

            // convert to an array for speed
            String[] strings = candidates.map(CompletionCandidate::value).toArray(new String[candidates.size()]);

            String first = strings[0];
            StringBuilder candidate = new StringBuilder();

            for (int i = 0; i < first.length(); i++) {
                if (startsWith(first.substring(0, i + 1), strings)) {
                    candidate.append(first.charAt(i));
                } else {
                    break;
                }
            }

            return candidate.toString();
        }

        /**
         * @return true is all the elements of candidates start with starts
         */
        private boolean startsWith(final String starts, final String[] candidates) {
            for (String candidate : candidates) {
                if (!candidate.startsWith(starts)) {
                    return false;
                }
            }

            return true;
        }

        private static enum Messages {
            DISPLAY_CANDIDATES,
            DISPLAY_CANDIDATES_YES,
            DISPLAY_CANDIDATES_NO,;

            private static final
            ResourceBundle
                    bundle =
                    ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName(), Locale.getDefault());

            public String format(final Object... args) {
                if (bundle == null)
                    return "";
                else
                    return String.format(bundle.getString(name()), args);
            }
        }
    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy