diff --git a/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleCompleter.java b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleCompleter.java new file mode 100644 index 0000000000000000000000000000000000000000..15cbe86905a861c1fbc6a106cbcd6defac2f9e17 --- /dev/null +++ b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleCompleter.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.engine.shell; + +import com.oracle.truffle.api.frame.MaterializedFrame; +import com.oracle.truffle.r.nodes.function.PromiseHelperNode; +import com.oracle.truffle.r.runtime.RCaller; +import com.oracle.truffle.r.runtime.RInternalError; +import com.oracle.truffle.r.runtime.context.ConsoleHandler; +import com.oracle.truffle.r.runtime.context.RContext; +import com.oracle.truffle.r.runtime.data.RFunction; +import com.oracle.truffle.r.runtime.data.RList; +import com.oracle.truffle.r.runtime.data.RPromise; +import com.oracle.truffle.r.runtime.data.model.RAbstractStringVector; +import com.oracle.truffle.r.runtime.env.REnvironment; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import jline.console.completer.Completer; + +public class JLineConsoleCompleter implements Completer { + private final ConsoleHandler console; + + private static boolean isTesting = false; + + public static void testingMode() { + isTesting = true; + } + + public JLineConsoleCompleter(ConsoleHandler console) { + this.console = console; + } + + @Override + public int complete(String buffer, int cursor, List<CharSequence> candidates) { + try { + return completeImpl(buffer, cursor, candidates); + } catch (Throwable e) { + if (isTesting) { + throw e; + } + RInternalError.reportErrorAndConsoleLog(e, console, 0); + } + return cursor; + } + + private int completeImpl(String buffer, int cursor, List<CharSequence> candidates) { + if (buffer.isEmpty()) { + return cursor; + } + + REnvironment utils = REnvironment.getRegisteredNamespace("utils"); + Object o = utils.get(".completeToken"); + if (o instanceof RPromise) { + o = PromiseHelperNode.evaluateSlowPath(null, (RPromise) o); + } + RFunction completeToken; + if (o instanceof RFunction) { + completeToken = (RFunction) o; + } else { + return cursor; + } + + o = utils.get(".CompletionEnv"); + if (!(o instanceof RPromise)) { + return cursor; + } + REnvironment env = (REnvironment) PromiseHelperNode.evaluateSlowPath(null, (RPromise) o); + int start = getStart(buffer, env, cursor); + env.safePut("start", start); + env.safePut("end", cursor); + env.safePut("linebuffer", buffer); + env.safePut("token", buffer.substring(start, cursor)); + + MaterializedFrame callingFrame = REnvironment.globalEnv().getFrame(); + RContext.getEngine().evalFunction(completeToken, callingFrame, RCaller.createInvalid(callingFrame), null, new Object[]{}); + + o = env.get("comps"); + if (!(o instanceof RAbstractStringVector)) { + return cursor; + } + + RAbstractStringVector comps = (RAbstractStringVector) o; + List<String> ret = new ArrayList<>(comps.getLength()); + for (int i = 0; i < comps.getLength(); i++) { + ret.add(comps.getDataAt(i)); + } + Collections.sort(ret, String.CASE_INSENSITIVE_ORDER); + candidates.addAll(ret); + return start; + } + + private int getStart(String buffer, REnvironment env, int cursor) { + int start = 0; + Object o = env.get("options"); + if (o instanceof RList) { + RList opt = (RList) o; + start = lastIdxOf(buffer, opt, "funarg.suffix", start, cursor); + start = lastIdxOf(buffer, opt, "function.suffix", start, cursor); + } + start = lastIdxOf(buffer, "\"", start, cursor); + start = lastIdxOf(buffer, "'", start, cursor); + return start; + } + + private int lastIdxOf(String buffer, RList opt, String key, int start, int cursor) { + int optIdx = opt.getElementIndexByName(key); + if (optIdx > -1) { + Object o = opt.getDataAt(optIdx); + if (o instanceof RAbstractStringVector) { + RAbstractStringVector v = (RAbstractStringVector) o; + return lastIdxOf(buffer, v.getLength() > 0 ? v.getDataAt(0) : null, start, cursor); + } + } + return start; + } + + private int lastIdxOf(String buffer, String subs, int start, int cursor) { + if (null != subs && !subs.isEmpty()) { + int idx = buffer.lastIndexOf(subs, cursor); + if (idx == cursor) { + idx = buffer.lastIndexOf(subs, cursor - 1); + } + if (idx > -1) { + idx += subs.length(); + return idx > start ? idx : start; + } + } + return start; + } +} diff --git a/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleHandler.java b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleHandler.java index 2c6f86297d727f7862e273cf4c5faa1bdeb9ef88..e8f01bbdb7885fff8077d0eb76a71cff257ab672 100644 --- a/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleHandler.java +++ b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleHandler.java @@ -47,6 +47,7 @@ class JLineConsoleHandler implements ConsoleHandler { JLineConsoleHandler(RStartParams startParams, InputStream inStream, OutputStream outStream) { try { console = new ConsoleReader(inStream, outStream); + console.addCompleter(new JLineConsoleCompleter(this)); console.setHandleUserInterrupt(true); console.setExpandEvents(false); } catch (IOException ex) { diff --git a/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/engine/shell/TestJLineConsoleCompleter.java b/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/engine/shell/TestJLineConsoleCompleter.java new file mode 100644 index 0000000000000000000000000000000000000000..e7d55912a9dfb820431d590ee1ffa684acb7c531 --- /dev/null +++ b/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/engine/shell/TestJLineConsoleCompleter.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.test.engine.shell; + +import com.oracle.truffle.api.vm.PolyglotEngine; +import com.oracle.truffle.r.engine.shell.JLineConsoleCompleter; +import com.oracle.truffle.r.runtime.RCmdOptions; +import com.oracle.truffle.r.runtime.context.ConsoleHandler; +import com.oracle.truffle.r.runtime.context.ContextInfo; +import com.oracle.truffle.r.runtime.context.RContext; +import java.io.File; +import org.junit.Test; + +import java.util.LinkedList; +import org.junit.After; +import org.junit.Assert; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Before; + +public class TestJLineConsoleCompleter { + + private PolyglotEngine engine; + private ConsoleHandler consoleHandler; + private JLineConsoleCompleter consoleCompleter; + + @Before + public void before() { + consoleHandler = new DummyConsoleHandler(); + consoleCompleter = new JLineConsoleCompleter(consoleHandler); + JLineConsoleCompleter.testingMode(); + ContextInfo info = ContextInfo.createNoRestore(RCmdOptions.Client.R, null, RContext.ContextKind.SHARE_NOTHING, null, consoleHandler); + engine = info.createVM(PolyglotEngine.newBuilder()); + } + + @After + public void dispose() { + if (engine != null) { + engine.dispose(); + } + } + + @Test + public void testCompl() { + assertCompl("", 0); + assertCompl("", 1); + assertCompl(" ", 1); + + assertCompl("(", 0); + assertCompl("(", 1); + assertCompl("=", 1); + assertCompl("$", 1); + + assertCompl("strt", 4, "strtoi", "strtrim"); + assertCompl("strto", 5, "strtoi"); + assertCompl("strtoi", 5, "strtoi"); + assertCompl("strtoi", 4, "strtoi", "strtrim"); + assertCompl("strto ", 6); + + assertCompl("base::strt", 10, "base::strtoi", "base::strtrim"); + assertCompl("base:::strt", 11, "base:::strtoi", "base:::strtrim"); + assertCompl("base:::strttrt", 14, "base:::"); + + assertCompl("strt(", 4, "strtoi", "strtrim"); + assertCompl("strt(", 5); + assertCompl("f(strt", 6, "strtoi", "strtrim"); + assertCompl("f(base::strt", 12, "base::strtoi", "base::strtrim"); + assertCompl("f(strt(trt", 6, "strtoi", "strtrim"); + assertCompl("f(strt(trt", 10); + assertCompl("f(strt(strto", 11, "strtoi", "strtrim"); + assertCompl("f(strt(strto", 12, "strtoi"); + + String noName = "_f_f_f_"; + assertCompl(noName + ".", 7); + assertCompl(noName + ".", 8); + assertCompl(noName + "." + File.separator, 9); + assertCompl(noName + "'", 7); + assertCompl(noName + "'", 8, NOT_EMPTY); + assertCompl(noName + "'." + File.separator, 8, NOT_EMPTY); + assertCompl(noName + "'." + File.separator, 9, NOT_EMPTY); + assertCompl(noName + "'." + File.separator, 10, NOT_EMPTY); + assertCompl(noName + "\"." + File.separator, 8, NOT_EMPTY); + } + + // e.g. check if the file path completion returned at least something + private static final String NOT_EMPTY = "_@_Only.Check.If.Result.Not.Empty_@_"; + + private void assertCompl(String buffer, int cursor, String... expected) { + LinkedList<CharSequence> l = new LinkedList<>(); + consoleCompleter.complete(buffer, cursor, l); + + if (expected == null || expected.length == 0) { + assertTrue(l.isEmpty()); + } else if (expected.length == 1 && NOT_EMPTY.equals(expected[0])) { + assertFalse(l.isEmpty()); + } else { + Assert.assertArrayEquals(expected, l.toArray(new CharSequence[l.size()])); + } + } + + private class DummyConsoleHandler implements ConsoleHandler { + @Override + public void println(String s) { + } + + @Override + public void print(String s) { + } + + @Override + public void printErrorln(String s) { + } + + @Override + public void printError(String s) { + } + + @Override + public String readLine() { + return ""; + } + + @Override + public boolean isInteractive() { + return false; + } + + @Override + public String getPrompt() { + return ""; + } + + @Override + public void setPrompt(String prompt) { + } + + @Override + public String getInputDescription() { + return ""; + } + } +} diff --git a/mx.fastr/mx_fastr.py b/mx.fastr/mx_fastr.py index 5e9c6ebfd3344ee1405840869061861a8af54bc6..e19d9ffd436dc1bf13bc64fb0c82faa9eb1738e1 100644 --- a/mx.fastr/mx_fastr.py +++ b/mx.fastr/mx_fastr.py @@ -407,7 +407,7 @@ def _test_subpackage(name): return '.'.join((_test_package(), name)) def _simple_generated_unit_tests(): - return ','.join(map(_test_subpackage, ['library.base', 'library.grid', 'library.methods', 'library.stats', 'library.utils', 'library.fastr', 'builtins', 'functions', 'parser', 'S4', 'rng', 'runtime.data'])) + return ','.join(map(_test_subpackage, ['engine.shell', 'library.base', 'library.grid', 'library.methods', 'library.stats', 'library.utils', 'library.fastr', 'builtins', 'functions', 'parser', 'S4', 'rng', 'runtime.data'])) def _simple_unit_tests(): return ','.join([_simple_generated_unit_tests(), _test_subpackage('tck')])