From 7480c74750a5bd9f68b0aa8d946c0432860550dc Mon Sep 17 00:00:00 2001 From: Max Weber Date: Mon, 22 Feb 2021 10:40:05 -0700 Subject: [PATCH] devtools: add shell window --- pom.xml | 13 + runelite-api/pom.xml | 1 - runelite-client/pom.xml | 14 +- .../runelite/client/eventbus/EventBus.java | 72 ++- .../plugins/devtools/DevToolsPanel.java | 18 + .../plugins/devtools/DevToolsPlugin.java | 2 + .../client/plugins/devtools/ShellFrame.java | 70 +++ runelite-jshell/pom.xml | 83 +++ .../jshell/JShellAutocompleteProvider.java | 137 +++++ .../jshell/RLShellExecutionControl.java | 50 ++ .../runelite/jshell/RemappingThrowable.java | 142 +++++ .../java/net/runelite/jshell/ShellPanel.java | 447 ++++++++++++++++ .../java/net/runelite/jshell/TeeLogger.java | 483 ++++++++++++++++++ .../resources/net/runelite/jshell/darcula.xml | 73 +++ .../resources/net/runelite/jshell/default.jsh | 8 + .../resources/net/runelite/jshell/prelude.jsh | 68 +++ 16 files changed, 1645 insertions(+), 36 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/devtools/ShellFrame.java create mode 100644 runelite-jshell/pom.xml create mode 100644 runelite-jshell/src/main/java/net/runelite/jshell/JShellAutocompleteProvider.java create mode 100644 runelite-jshell/src/main/java/net/runelite/jshell/RLShellExecutionControl.java create mode 100644 runelite-jshell/src/main/java/net/runelite/jshell/RemappingThrowable.java create mode 100644 runelite-jshell/src/main/java/net/runelite/jshell/ShellPanel.java create mode 100644 runelite-jshell/src/main/java/net/runelite/jshell/TeeLogger.java create mode 100644 runelite-jshell/src/main/resources/net/runelite/jshell/darcula.xml create mode 100644 runelite-jshell/src/main/resources/net/runelite/jshell/default.jsh create mode 100644 runelite-jshell/src/main/resources/net/runelite/jshell/prelude.jsh diff --git a/pom.xml b/pom.xml index 0443ee0b52..5f1f37e418 100644 --- a/pom.xml +++ b/pom.xml @@ -119,6 +119,7 @@ cache-updater runelite-api runelite-client + runelite-jshell runelite-script-assembler-plugin http-api http-service @@ -142,6 +143,11 @@ gson 2.8.5 + + com.google.code.findbugs + jsr305 + 3.0.2 + ch.qos.logback logback-parent @@ -149,6 +155,13 @@ pom import + + com.google.inject + guice-bom + 4.1.0 + pom + import + diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml index 8634829df5..94a578e767 100644 --- a/runelite-api/pom.xml +++ b/runelite-api/pom.xml @@ -48,7 +48,6 @@ com.google.code.findbugs jsr305 - 3.0.2 provided diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index 37364a3f5b..ab36e6df91 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -36,8 +36,6 @@ RuneLite Client - 4.1.0 - true true @@ -85,7 +83,6 @@ com.google.inject guice - ${guice.version} no_aop @@ -215,7 +212,6 @@ com.google.code.findbugs jsr305 - 3.0.2 @@ -223,6 +219,12 @@ runelite-api ${project.version} + + net.runelite + jshell + ${project.version} + true + net.runelite client-patch @@ -267,13 +269,11 @@ com.google.inject.extensions guice-testlib - ${guice.version} test com.google.inject.extensions guice-grapher - ${guice.version} test @@ -318,7 +318,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.0.0 + 3.2.1 package diff --git a/runelite-client/src/main/java/net/runelite/client/eventbus/EventBus.java b/runelite-client/src/main/java/net/runelite/client/eventbus/EventBus.java index 2340e306dd..a6d123a974 100644 --- a/runelite-client/src/main/java/net/runelite/client/eventbus/EventBus.java +++ b/runelite-client/src/main/java/net/runelite/client/eventbus/EventBus.java @@ -51,26 +51,20 @@ import net.runelite.client.util.ReflectUtil; @ThreadSafe public class EventBus { - @FunctionalInterface - public interface SubscriberMethod - { - void invoke(Object event); - } - @Value - private static class Subscriber + public static class Subscriber { private final Object object; private final Method method; private final float priority; @EqualsAndHashCode.Exclude - private final SubscriberMethod lamda; + private final Consumer lambda; void invoke(final Object arg) throws Exception { - if (lamda != null) + if (lambda != null) { - lamda.invoke(arg); + lambda.accept(arg); } else { @@ -80,7 +74,9 @@ public class EventBus } private final Consumer exceptionHandler; - private ImmutableMultimap subscribers = ImmutableMultimap.of(); + + @Nonnull + private ImmutableMultimap, Subscriber> subscribers = ImmutableMultimap.of(); /** * Instantiates EventBus with default exception handler @@ -99,13 +95,8 @@ public class EventBus */ public synchronized void register(@Nonnull final Object object) { - final ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); - - if (subscribers != null) - { - builder.putAll(subscribers); - } - + final ImmutableMultimap.Builder, Subscriber> builder = ImmutableMultimap.builder(); + builder.putAll(subscribers); builder.orderValuesBy(Comparator.comparing(Subscriber::getPriority).reversed() .thenComparing(s -> s.object.getClass().getName())); @@ -141,7 +132,7 @@ public class EventBus Preconditions.checkArgument(method.getName().equals(preferredName), "Subscribed method " + method + " should be named " + preferredName); method.setAccessible(true); - SubscriberMethod lambda = null; + Consumer lambda = null; try { @@ -150,14 +141,14 @@ public class EventBus final MethodHandle target = caller.findVirtual(clazz, method.getName(), subscription); final CallSite site = LambdaMetafactory.metafactory( caller, - "invoke", - MethodType.methodType(SubscriberMethod.class, clazz), + "accept", + MethodType.methodType(Consumer.class, clazz), subscription.changeParameterType(0, Object.class), target, subscription); final MethodHandle factory = site.getTarget(); - lambda = (SubscriberMethod) factory.bindTo(object).invokeExact(); + lambda = (Consumer) factory.bindTo(object).invokeExact(); } catch (Throwable e) { @@ -173,6 +164,21 @@ public class EventBus subscribers = builder.build(); } + public synchronized Subscriber register(Class clazz, Consumer subFn, float priority) + { + final ImmutableMultimap.Builder, Subscriber> builder = ImmutableMultimap.builder(); + builder.putAll(subscribers); + builder.orderValuesBy(Comparator.comparing(Subscriber::getPriority).reversed() + .thenComparing(s -> s.object.getClass().getName())); + + Subscriber sub = new Subscriber(subFn, null, priority, (Consumer) subFn); + builder.put(clazz, sub); + + subscribers = builder.build(); + + return sub; + } + /** * Unregisters all subscribed methods from provided subscriber object. * @@ -180,12 +186,7 @@ public class EventBus */ public synchronized void unregister(@Nonnull final Object object) { - if (subscribers == null) - { - return; - } - - final Multimap map = HashMultimap.create(); + final Multimap, Subscriber> map = HashMultimap.create(); map.putAll(subscribers); for (Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass()) @@ -207,6 +208,21 @@ public class EventBus subscribers = ImmutableMultimap.copyOf(map); } + public synchronized void unregister(Subscriber sub) + { + if (sub == null) + { + return; + } + + final Multimap, Subscriber> map = HashMultimap.create(); + map.putAll(subscribers); + + map.values().remove(sub); + + subscribers = ImmutableMultimap.copyOf(map); + } + /** * Posts provided event to all registered subscribers. Subscriber calls are invoked immediately, * ordered by priority then their declaring class' name. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java index 7651a5ad69..9ba1efd092 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java @@ -25,6 +25,7 @@ */ package net.runelite.client.plugins.devtools; +import com.google.inject.ProvisionException; import java.awt.GridLayout; import java.awt.TrayIcon; import java.util.concurrent.ScheduledExecutorService; @@ -32,6 +33,7 @@ import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.swing.JButton; import javax.swing.JPanel; +import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.MenuAction; @@ -44,6 +46,7 @@ import net.runelite.client.ui.overlay.infobox.Counter; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; import net.runelite.client.util.ImageUtil; +@Slf4j class DevToolsPanel extends PluginPanel { private final Client client; @@ -174,6 +177,21 @@ class DevToolsPanel extends PluginPanel disconnectBtn.addActionListener(e -> clientThread.invoke(() -> client.setGameState(GameState.CONNECTION_LOST))); container.add(disconnectBtn); + try + { + ShellFrame sf = plugin.getInjector().getInstance(ShellFrame.class); + container.add(plugin.getShell()); + plugin.getShell().addFrame(sf); + } + catch (LinkageError | ProvisionException e) + { + log.debug("Shell is not supported", e); + } + catch (Exception e) + { + log.info("Shell couldn't be loaded", e); + } + return container; } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java index 424c26e4c3..b690f54e71 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java @@ -143,6 +143,7 @@ public class DevToolsPlugin extends Plugin private DevToolsButton soundEffects; private DevToolsButton scriptInspector; private DevToolsButton inventoryInspector; + private DevToolsButton shell; private NavigationButton navButton; @Provides @@ -187,6 +188,7 @@ public class DevToolsPlugin extends Plugin soundEffects = new DevToolsButton("Sound Effects"); scriptInspector = new DevToolsButton("Script Inspector"); inventoryInspector = new DevToolsButton("Inventory Inspector"); + shell = new DevToolsButton("Shell"); overlayManager.add(overlay); overlayManager.add(locationOverlay); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ShellFrame.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ShellFrame.java new file mode 100644 index 0000000000..f6472575ad --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/ShellFrame.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.devtools; + +import java.util.concurrent.ScheduledExecutorService; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.client.RuneLite; +import net.runelite.client.callback.ClientThread; +import net.runelite.jshell.ShellPanel; + +@Singleton +class ShellFrame extends DevToolsFrame +{ + private final ShellPanel shellPanel; + + @Inject + ShellFrame(ClientThread clientThread, ScheduledExecutorService executor) + { + this.shellPanel = new ShellPanel(executor) + { + @Override + protected void invokeOnClientThread(Runnable r) + { + clientThread.invoke(r); + } + }; + setContentPane(shellPanel); + + setTitle("RuneLite Shell"); + + pack(); + } + + @Override + public void open() + { + shellPanel.switchContext(RuneLite.getInjector()); + super.open(); + } + + @Override + public void close() + { + super.close(); + shellPanel.freeContext(); + } +} diff --git a/runelite-jshell/pom.xml b/runelite-jshell/pom.xml new file mode 100644 index 0000000000..85536adbe3 --- /dev/null +++ b/runelite-jshell/pom.xml @@ -0,0 +1,83 @@ + + + + 4.0.0 + + + net.runelite + runelite-parent + 1.7.2-SNAPSHOT + + + jshell + RuneLite JShell + + + + org.slf4j + slf4j-api + + + com.google.inject + guice + no_aop + + + org.projectlombok + lombok + provided + + + com.google.code.findbugs + jsr305 + + + com.fifesoft + rsyntaxtextarea + 3.1.2 + + + com.fifesoft + autocomplete + 3.1.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + 11 + + + + + diff --git a/runelite-jshell/src/main/java/net/runelite/jshell/JShellAutocompleteProvider.java b/runelite-jshell/src/main/java/net/runelite/jshell/JShellAutocompleteProvider.java new file mode 100644 index 0000000000..2a0a2d8418 --- /dev/null +++ b/runelite-jshell/src/main/java/net/runelite/jshell/JShellAutocompleteProvider.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2021 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.jshell; + +import java.awt.Point; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import javax.swing.text.JTextComponent; +import jdk.jshell.JShell; +import jdk.jshell.SourceCodeAnalysis; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.fife.ui.autocomplete.BasicCompletion; +import org.fife.ui.autocomplete.Completion; +import org.fife.ui.autocomplete.CompletionProviderBase; +import org.fife.ui.autocomplete.ParameterizedCompletion; + +@Slf4j +@RequiredArgsConstructor +public class JShellAutocompleteProvider extends CompletionProviderBase +{ + private final JShell shell; + private String anchorText; + private List completions; + + @Override + protected List getCompletionsImpl(JTextComponent comp) + { + return completions; + } + + @Override + public String getAlreadyEnteredText(JTextComponent comp) + { + complete(comp); + return anchorText; + } + + private void complete(JTextComponent comp) + { + completions = Collections.emptyList(); + + String src = comp.getText(); + int cursor = comp.getCaretPosition(); + + for (int offset = 0; offset < src.length() && cursor >= offset; ) + { + var snipSrc = src.substring(offset); + int thisOffset = offset; + var ci = shell.sourceCodeAnalysis().analyzeCompletion(snipSrc); + offset = src.length() - ci.remaining().length(); + boolean mayHaveMore = ci.completeness() == SourceCodeAnalysis.Completeness.COMPLETE_WITH_SEMI + || ci.completeness() == SourceCodeAnalysis.Completeness.COMPLETE; + + if (cursor <= offset || !mayHaveMore) + { + var anchor = new int[1]; + + completions = shell.sourceCodeAnalysis() + .completionSuggestions(snipSrc, cursor - thisOffset, anchor) + .stream() + .filter(v -> !v.continuation().startsWith("$")) + .map(s -> + { + return new BasicCompletion(this, s.continuation()); + }) + .collect(Collectors.toList()); + anchorText = snipSrc.substring(anchor[0], cursor - thisOffset); + break; + } + } + + if (completions.isEmpty()) + { + anchorText = null; + } + } + + @Override + public List getCompletionsAt(JTextComponent comp, Point p) + { + return Collections.emptyList(); + } + + @Override + public boolean isAutoActivateOkay(JTextComponent comp) + { + // try not to start autocomplete when it has no useful context + String text = comp.getText(); + for (int i = comp.getCaretPosition(); i >= 0; i--) + { + char c = text.charAt(i); + if (Character.isJavaIdentifierPart(c) || c == '.' || c == '(') + { + return true; + } + if (Character.isWhitespace(c)) + { + continue; + } + + return false; + } + + return false; + } + + @Override + public List getParameterizedCompletions(JTextComponent tc) + { + return Collections.emptyList(); + } +} + diff --git a/runelite-jshell/src/main/java/net/runelite/jshell/RLShellExecutionControl.java b/runelite-jshell/src/main/java/net/runelite/jshell/RLShellExecutionControl.java new file mode 100644 index 0000000000..76a32c6aac --- /dev/null +++ b/runelite-jshell/src/main/java/net/runelite/jshell/RLShellExecutionControl.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.jshell; + +import java.util.Map; +import jdk.jshell.execution.DirectExecutionControl; +import jdk.jshell.spi.ExecutionControl; +import jdk.jshell.spi.ExecutionControlProvider; +import jdk.jshell.spi.ExecutionEnv; + +public class RLShellExecutionControl extends DirectExecutionControl implements ExecutionControlProvider +{ + public RLShellExecutionControl() + { + } + + @Override + public String name() + { + return getClass().getName(); + } + + @Override + public ExecutionControl generate(ExecutionEnv env, Map parameters) throws Throwable + { + return this; + } +} diff --git a/runelite-jshell/src/main/java/net/runelite/jshell/RemappingThrowable.java b/runelite-jshell/src/main/java/net/runelite/jshell/RemappingThrowable.java new file mode 100644 index 0000000000..7fc5e6342d --- /dev/null +++ b/runelite-jshell/src/main/java/net/runelite/jshell/RemappingThrowable.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2021 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.jshell; + +import com.google.common.base.Strings; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import jdk.jshell.EvalException; + +class RemappingThrowable extends Throwable +{ + private final String source; + private final Map offsets; + private final Throwable wrapped; + private final Map dejaVu; + + public RemappingThrowable(String source, Map offsets, Throwable other) + { + this(source, offsets, other, new HashMap<>()); + } + + private RemappingThrowable(String source, Map offsets, Throwable other, Map dejaVu) + { + super(); + + this.source = source; + this.offsets = offsets; + this.wrapped = other; + this.dejaVu = dejaVu; + + dejaVu.put(wrapped, this); + + setStackTrace(Stream.of(wrapped.getStackTrace()) + .map(e -> + { + Integer boxOffset = offsets.get(e.getFileName()); + if (boxOffset == null) + { + return e; + } + + int offset = boxOffset; + int line = e.getLineNumber(); + for (int i = 0; i <= offset && i < source.length(); i++) + { + if (source.charAt(i) == '\n') + { + line++; + } + } + return new StackTraceElement( + Strings.isNullOrEmpty(e.getClassName()) ? "Shell" : e.getClassName(), + Strings.isNullOrEmpty(e.getMethodName()) ? "global" : e.getMethodName(), + "", + line); + }) + .toArray(StackTraceElement[]::new)); + + if (wrapped.getCause() != null) + { + initCause(remap(wrapped.getCause())); + } + + for (Throwable suppressed : wrapped.getSuppressed()) + { + addSuppressed(remap(suppressed)); + } + } + + private Throwable remap(Throwable other) + { + Throwable remap = dejaVu.get(other); + if (remap == null) + { + remap = new RemappingThrowable(source, offsets, other, dejaVu); + // ctor inserts into the map + } + return remap; + } + + @Override + public String getMessage() + { + return wrapped.getMessage(); + } + + @Override + public String getLocalizedMessage() + { + return wrapped.getLocalizedMessage(); + } + + @Override + public synchronized Throwable fillInStackTrace() + { + return this; + } + + @Override + public String toString() + { + String className; + if (wrapped instanceof EvalException) + { + className = ((EvalException) wrapped).getExceptionClassName(); + } + else + { + className = wrapped.getClass().getName(); + } + + String message = wrapped.getLocalizedMessage(); + if (message == null) + { + return className; + } + return className + ": " + message; + } +} diff --git a/runelite-jshell/src/main/java/net/runelite/jshell/ShellPanel.java b/runelite-jshell/src/main/java/net/runelite/jshell/ShellPanel.java new file mode 100644 index 0000000000..1fd17a5711 --- /dev/null +++ b/runelite-jshell/src/main/java/net/runelite/jshell/ShellPanel.java @@ -0,0 +1,447 @@ +/* + * Copyright (c) 2021 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.jshell; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Injector; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.RenderingHints; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; +import javax.swing.text.Segment; +import jdk.jshell.Diag; +import jdk.jshell.JShell; +import jdk.jshell.Snippet; +import jdk.jshell.SnippetEvent; +import jdk.jshell.SourceCodeAnalysis; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.fife.ui.autocomplete.AutoCompletion; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rsyntaxtextarea.Theme; +import org.fife.ui.rtextarea.RTextScrollPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Slf4j +public abstract class ShellPanel extends JPanel +{ + private final ScheduledExecutorService executor; + + private final RSyntaxTextArea textArea; + private final JTextArea console = new JTextArea(); + + @Getter + private final Logger shellLogger; + + private final List cleanup = new ArrayList<>(); + + private RLShellExecutionControl exec; + private JShell shell; + private Set prelude; + private Injector injector; + private AutoCompletion autoCompletion; + + public static ShellPanel INSTANCE; + + public ShellPanel(ScheduledExecutorService executor) + { + this.executor = executor; + + Font codeFont = Stream.of( + "Source code pro", + "DejaVu Sans Code", + "Consolas", + Font.MONOSPACED) + .map(name -> new Font(name, Font.PLAIN, 12)) + .filter(f -> !"Dialog.plain".equals(f.getFontName())) + .findFirst() + .get(); + + setLayout(new BorderLayout()); + + JPanel topPanel = new JPanel(); + topPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + + JButton run = new JButton("⯈"); + run.setToolTipText("Run"); + run.addActionListener(ev -> run()); + topPanel.add(run); + + JButton clear = new JButton("🗑"); + run.setToolTipText("Clear console"); + clear.addActionListener(ev -> console.setText("")); + topPanel.add(clear); + + add(topPanel, BorderLayout.NORTH); + + textArea = new RSyntaxTextArea(); + + try + { + // RSyntaxTextArea::setAntiAliasingEnabled actually forces it to match the platform's + // default, which is pointless + var map = new HashMap(); + map.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + var f = RSyntaxTextArea.class.getDeclaredField("aaHints"); + f.setAccessible(true); + f.set(textArea, map); + } + catch (ReflectiveOperationException e) + { + throw new RuntimeException(e); + } + + textArea.setFont(codeFont); + textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); + textArea.setAutoIndentEnabled(true); + textArea.setPaintTabLines(true); + textArea.setShowMatchedBracketPopup(true); + textArea.setCloseCurlyBraces(false); + textArea.setTabSize(2); + textArea.setMarkOccurrences(true); + textArea.setMarkOccurrencesDelay(200); + textArea.addKeyListener(new KeyAdapter() + { + @Override + public void keyPressed(KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_R && (e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) + { + run(); + e.consume(); + } + if (e.getKeyCode() == KeyEvent.VK_F10) + { + run(); + e.consume(); + } + } + }); + + var textScrollArea = new RTextScrollPane(textArea); + + try + { + Theme.load(ShellPanel.class.getResourceAsStream("darcula.xml"), codeFont) + .apply(textArea); + + try (var is = ShellPanel.class.getResourceAsStream("default.jsh")) + { + textArea.setText(new String(is.readAllBytes(), StandardCharsets.UTF_8)); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + + console.setFont(codeFont); + console.setFocusable(false); + console.setEditable(false); + console.setOpaque(false); // this turns off the hover effect for some reason + + var split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, textScrollArea, new JScrollPane(console)); + split.setResizeWeight(.8); + split.setPreferredSize(new Dimension(800, 800)); + add(split, BorderLayout.CENTER); + + shellLogger = new TeeLogger(LoggerFactory.getLogger("Shell"), this::logToConsole); + INSTANCE = this; + + // make sure jshell is on the classpath + JShell.builder(); + } + + public void switchContext(Injector injector) + { + freeContext(); + + this.injector = injector; + + exec = new RLShellExecutionControl() + { + @Override + protected String invoke(Method doitMethod) throws Exception + { + var result = new AtomicReference<>(); + var sema = new Semaphore(0); + invokeOnClientThread(() -> + { + try + { + result.set(super.invoke(doitMethod)); + } + catch (Exception e) + { + result.set(e); + } + finally + { + sema.release(); + } + }); + sema.acquire(); + if (result.get() instanceof String) + { + return (String) result.get(); + } + throw (Exception) result.get(); + } + }; + + shell = JShell.builder() + .executionEngine(exec, null) + .build(); + + String preludeStr; + try (var is = ShellPanel.class.getResourceAsStream("prelude.jsh")) + { + preludeStr = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + prelude = ImmutableSet.copyOf(eval(preludeStr, false)); + + var cp = new JShellAutocompleteProvider(shell); + autoCompletion = new AutoCompletion(cp); + autoCompletion.setAutoActivationDelay(200); + autoCompletion.setAutoActivationEnabled(true); + autoCompletion.setAutoCompleteSingleChoices(false); + autoCompletion.install(this.textArea); + } + + public void logToConsole(String message) + { + SwingUtilities.invokeLater(() -> + { + try + { + var doc = console.getDocument(); + if (doc.getLength() > 100_000) + { + Segment seg = new Segment(); + int i = doc.getLength() - 75_000; + for (; i < doc.getLength(); i++) + { + doc.getText(i, 1, seg); + if (seg.array[0] == '\n') + { + break; + } + } + doc.remove(0, i); + } + doc.insertString(doc.getLength(), message + "\n", null); + console.setCaretPosition(doc.getLength()); + } + catch (BadLocationException e) + { + throw new RuntimeException(e); + } + }); + } + + private List eval(String src, boolean isUserCode) + { + var out = new ArrayList(); + var offsets = new HashMap(); + String output = null; + evaluation: + for (int offset = 0; offset < src.length(); ) + { + // Workaround a jdk bug + for (; src.charAt(offset) == '\n'; offset++); + + var ci = shell.sourceCodeAnalysis().analyzeCompletion(src.substring(offset)); + int thisOffset = offset; + offset = src.length() - ci.remaining().length(); + if (ci.completeness() == SourceCodeAnalysis.Completeness.EMPTY) + { + break; + } + + List evs = shell.eval(ci.source()); + for (var ev : evs) + { + Snippet snip = ev.snippet(); + offsets.put("#" + snip.id(), thisOffset); + if (ev.status() != Snippet.Status.VALID && ev.status() != Snippet.Status.RECOVERABLE_DEFINED) + { + var diags = shell.diagnostics(snip).collect(Collectors.toList()); + for (var diag : diags) + { + String msg = toStringDiagnostic(src, thisOffset, diag); + if (isUserCode) + { + logToConsole(msg); + // It might be nice to highlight stuff here + } + else + { + throw new RuntimeException("prelude error: " + msg); + } + } + if (diags.isEmpty()) + { + logToConsole("bad snippet" + ev.status()); + } + break evaluation; + } + if (ev.exception() != null) + { + if (isUserCode) + { + shellLogger.error("", new RemappingThrowable(src, offsets, ev.exception())); + } + else + { + throw new RuntimeException("prelude error", ev.exception()); + } + } + output = ev.value(); + + out.add(snip); + } + } + + if (isUserCode && !Strings.isNullOrEmpty(output)) + { + logToConsole("[OUTPUT] " + output); + } + + return out; + } + + private String toStringDiagnostic(String source, int offset, Diag diag) + { + int line = 1; + int column = 1; + offset += (int) diag.getPosition(); + for (int i = 0; i < offset && i < source.length(); i++) + { + if (source.charAt(i) == '\n') + { + line++; + column = 1; + } + else + { + column++; + } + } + + return line + ":" + column + ": " + diag.getMessage(Locale.getDefault()); + } + + protected void run() + { + String text = textArea.getText(); + executor.submit(() -> + { + shell.snippets() + .filter(v -> !prelude.contains(v)) + .forEach(shell::drop); + + cleanup(); + + eval(text, true); + }); + } + + public void freeContext() + { + cleanup(); + + exec = null; + shell = null; + prelude = null; + injector = null; + + if (autoCompletion != null) + { + autoCompletion.uninstall(); + } + autoCompletion = null; + + console.setText(""); + } + + private void cleanup() + { + for (var c : cleanup) + { + try + { + c.run(); + } + catch (Exception e) + { + shellLogger.error("Cleanup threw:", e); + } + } + cleanup.clear(); + } + + protected abstract void invokeOnClientThread(Runnable r); + + public T inject(Class clazz) + { + return injector.getInstance(clazz); + } + + public void cleanup(Runnable r) + { + cleanup.add(r); + } +} diff --git a/runelite-jshell/src/main/java/net/runelite/jshell/TeeLogger.java b/runelite-jshell/src/main/java/net/runelite/jshell/TeeLogger.java new file mode 100644 index 0000000000..9893842243 --- /dev/null +++ b/runelite-jshell/src/main/java/net/runelite/jshell/TeeLogger.java @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2021 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.jshell; + +import java.io.CharArrayWriter; +import java.io.PrintWriter; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.helpers.FormattingTuple; +import org.slf4j.helpers.MessageFormatter; + +@SuppressWarnings("PlaceholderCountMatchesArgumentCount") +@RequiredArgsConstructor +public class TeeLogger implements Logger +{ + private static final String TRACE = "[TRACE] "; + private static final String DEBUG = "[DEBUG] "; + private static final String INFO = "[INFO] "; + private static final String WARN = "[WARN] "; + private static final String ERROR = "[ERROR] "; + + private final Logger delegate; + private final Consumer messageConsumer; + + @Override + public String getName() + { + return "RLShell"; + } + + @Override + public boolean isTraceEnabled() + { + return true; + } + + private void log(String level, String message, Object... format) + { + FormattingTuple fmt = MessageFormatter.arrayFormat(message, format); + StringBuilder msg = new StringBuilder(); + msg.append(level).append(fmt.getMessage()); + Throwable throwable = fmt.getThrowable(); + if (throwable != null) + { + msg.append("\n"); + var caw = new CharArrayWriter(); + try (PrintWriter pw = new PrintWriter(caw)) + { + throwable.printStackTrace(pw); + } + msg.append(caw.toString()); + } + messageConsumer.accept(msg.toString()); + } + + @Override + public void trace(String msg) + { + delegate.trace(msg); + log(TRACE, msg); + } + + @Override + public void trace(String format, Object arg) + { + delegate.trace(format, arg); + log(TRACE, format, arg); + } + + @Override + public void trace(String format, Object arg1, Object arg2) + { + delegate.trace(format, arg1, arg2); + log(TRACE, format, arg1, arg2); + } + + @Override + public void trace(String format, Object... arguments) + { + delegate.trace(format, arguments); + log(TRACE, format, arguments); + } + + @Override + public void trace(String msg, Throwable t) + { + delegate.trace(msg, t); + log(TRACE, msg, t); + } + + @Override + public boolean isTraceEnabled(Marker marker) + { + return true; + } + + @Override + public void trace(Marker marker, String msg) + { + delegate.trace(marker, msg); + log(TRACE, msg); + } + + @Override + public void trace(Marker marker, String format, Object arg) + { + delegate.trace(marker, format, arg); + log(TRACE, format, arg); + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) + { + delegate.trace(marker, format, arg1, arg2); + log(TRACE, format, arg1, arg2); + } + + @Override + public void trace(Marker marker, String format, Object... argArray) + { + delegate.trace(marker, format, argArray); + log(TRACE, format, argArray); + } + + @Override + public void trace(Marker marker, String msg, Throwable t) + { + delegate.trace(marker, msg, t); + log(TRACE, msg, t); + } + + @Override + public boolean isDebugEnabled() + { + return true; + } + + @Override + public void debug(String msg) + { + delegate.debug(msg); + log(DEBUG, msg); + } + + @Override + public void debug(String format, Object arg) + { + delegate.debug(format, arg); + log(DEBUG, format, arg); + } + + @Override + public void debug(String format, Object arg1, Object arg2) + { + delegate.debug(format, arg1, arg2); + log(DEBUG, format, arg1, arg2); + } + + @Override + public void debug(String format, Object... arguments) + { + delegate.debug(format, arguments); + log(DEBUG, format, arguments); + } + + @Override + public void debug(String msg, Throwable t) + { + delegate.debug(msg, t); + log(DEBUG, msg, t); + } + + @Override + public boolean isDebugEnabled(Marker marker) + { + return true; + } + + @Override + public void debug(Marker marker, String msg) + { + delegate.debug(marker, msg); + log(DEBUG, msg); + } + + @Override + public void debug(Marker marker, String format, Object arg) + { + delegate.debug(marker, format, arg); + log(DEBUG, format, arg); + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) + { + delegate.debug(marker, format, arg1, arg2); + log(DEBUG, format, arg1, arg2); + } + + @Override + public void debug(Marker marker, String format, Object... arguments) + { + delegate.debug(marker, format, arguments); + log(DEBUG, format, arguments); + } + + @Override + public void debug(Marker marker, String msg, Throwable t) + { + delegate.debug(marker, msg, t); + log(DEBUG, msg, t); + } + + @Override + public boolean isInfoEnabled() + { + return true; + } + + @Override + public void info(String msg) + { + delegate.info(msg); + log(INFO, msg); + } + + @Override + public void info(String format, Object arg) + { + delegate.info(format, arg); + log(INFO, format, arg); + } + + @Override + public void info(String format, Object arg1, Object arg2) + { + delegate.info(format, arg1, arg2); + log(INFO, format, arg1, arg2); + } + + @Override + public void info(String format, Object... arguments) + { + delegate.info(format, arguments); + log(INFO, format, arguments); + } + + @Override + public void info(String msg, Throwable t) + { + delegate.info(msg, t); + log(INFO, msg, t); + } + + @Override + public boolean isInfoEnabled(Marker marker) + { + return true; + } + + @Override + public void info(Marker marker, String msg) + { + delegate.info(marker, msg); + log(INFO, msg); + } + + @Override + public void info(Marker marker, String format, Object arg) + { + delegate.info(marker, format, arg); + log(INFO, format, arg); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) + { + delegate.info(marker, format, arg1, arg2); + log(INFO, format, arg1, arg2); + } + + @Override + public void info(Marker marker, String format, Object... arguments) + { + delegate.info(marker, format, arguments); + log(INFO, format, arguments); + } + + @Override + public void info(Marker marker, String msg, Throwable t) + { + delegate.info(marker, msg, t); + log(INFO, msg, t); + } + + @Override + public boolean isWarnEnabled() + { + return true; + } + + @Override + public void warn(String msg) + { + delegate.warn(msg); + log(WARN, msg); + } + + @Override + public void warn(String format, Object arg) + { + delegate.warn(format, arg); + log(WARN, format, arg); + } + + @Override + public void warn(String format, Object... arguments) + { + delegate.warn(format, arguments); + log(WARN, format, arguments); + } + + @Override + public void warn(String format, Object arg1, Object arg2) + { + delegate.warn(format, arg1, arg2); + log(WARN, format, arg1, arg2); + } + + @Override + public void warn(String msg, Throwable t) + { + delegate.warn(msg, t); + log(WARN, msg, t); + } + + @Override + public boolean isWarnEnabled(Marker marker) + { + return true; + } + + @Override + public void warn(Marker marker, String msg) + { + delegate.warn(marker, msg); + log(WARN, msg); + } + + @Override + public void warn(Marker marker, String format, Object arg) + { + delegate.warn(marker, format, arg); + log(WARN, format, arg); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) + { + delegate.warn(marker, format, arg1, arg2); + log(WARN, format, arg1, arg2); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) + { + delegate.warn(marker, format, arguments); + log(WARN, format, arguments); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) + { + delegate.warn(marker, msg, t); + log(WARN, msg, t); + } + + @Override + public boolean isErrorEnabled() + { + return true; + } + + @Override + public void error(String msg) + { + delegate.error(msg); + log(ERROR, msg); + } + + @Override + public void error(String format, Object arg) + { + delegate.error(format, arg); + log(ERROR, format, arg); + } + + @Override + public void error(String format, Object arg1, Object arg2) + { + delegate.error(format, arg1, arg2); + log(ERROR, format, arg1, arg2); + } + + @Override + public void error(String format, Object... arguments) + { + delegate.error(format, arguments); + log(ERROR, format, arguments); + } + + @Override + public void error(String msg, Throwable t) + { + delegate.error(msg, t); + log(ERROR, msg, t); + } + + @Override + public boolean isErrorEnabled(Marker marker) + { + return true; + } + + @Override + public void error(Marker marker, String msg) + { + delegate.error(marker, msg); + log(ERROR, msg); + } + + @Override + public void error(Marker marker, String format, Object arg) + { + delegate.error(marker, format, arg); + log(ERROR, format, arg); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) + { + delegate.error(marker, format, arg1, arg2); + log(ERROR, format, arg1, arg2); + } + + @Override + public void error(Marker marker, String format, Object... arguments) + { + delegate.error(marker, format, arguments); + log(ERROR, format, arguments); + } + + @Override + public void error(Marker marker, String msg, Throwable t) + { + delegate.error(marker, msg, t); + log(ERROR, msg, t); + } +} diff --git a/runelite-jshell/src/main/resources/net/runelite/jshell/darcula.xml b/runelite-jshell/src/main/resources/net/runelite/jshell/darcula.xml new file mode 100644 index 0000000000..43729730ad --- /dev/null +++ b/runelite-jshell/src/main/resources/net/runelite/jshell/darcula.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +