feat(gui): add run, check and format script actions

This commit is contained in:
Skylot
2022-10-29 21:00:15 +01:00
parent 98cc1b1022
commit a2f018a00b
40 changed files with 719 additions and 83 deletions
+8 -1
View File
@@ -4,7 +4,7 @@ plugins {
id 'com.github.ben-manes.versions' version '0.45.0'
id 'com.diffplug.spotless' version '6.13.0'
id 'org.jetbrains.kotlin.jvm' version '1.7.20' // needed for spotless
id 'org.jetbrains.kotlin.jvm' version '1.7.20'
}
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -23,6 +23,12 @@ allprojects {
compileJava {
options.encoding = "UTF-8"
}
compileKotlin {
kotlinOptions {
incremental = false // cause unexpected issues sometime
// useK2 = true
}
}
jar {
manifest {
@@ -78,6 +84,7 @@ spotless {
kotlin {
target fileTree(rootDir).matching {
include 'jadx-plugins/jadx-script/**/*.kt'
include 'jadx-gui/src/main/**/*.kt'
// include 'jadx-plugins/jadx-script/examples/scripts/**/*.jadx.kts'
}
ktlint()
@@ -128,6 +128,17 @@ public final class JadxDecompiler implements Closeable {
loadFinished();
}
public void reloadPasses() {
LOG.info("reloading (passes only) ...");
customPasses.clear();
root.resetPasses();
loadPlugins();
root.mergePasses(customPasses);
root.restartVisitors();
root.initPasses();
loadFinished();
}
private void loadInputFiles() {
loadedInputs.clear();
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
@@ -350,7 +350,7 @@ public class ClassNode extends NotificationAttrNode
innerClasses.forEach(ClassNode::deepUnload);
}
private void unloadFromCache() {
public void unloadFromCache() {
if (isInner()) {
return;
}
@@ -343,6 +343,24 @@ public class RootNode {
}
}
// TODO: make better API for reload passes lists
public void resetPasses() {
preDecompilePasses.clear();
preDecompilePasses.addAll(Jadx.getPreDecompilePassesList());
processClasses.getPasses().clear();
processClasses.getPasses().addAll(Jadx.getPassesList(args));
}
public void restartVisitors() {
for (ClassNode cls : classes) {
cls.unload();
cls.clearAttributes();
cls.unloadFromCache();
}
runPreDecompileStage();
}
public List<ClassNode> getClasses() {
return classes;
}
+13 -6
View File
@@ -14,9 +14,13 @@ dependencies {
// jadx-script autocomplete support
implementation(project(":jadx-plugins::jadx-script:jadx-script-ide"))
implementation("org.jetbrains.kotlin:kotlin-scripting-common:1.7.20")
implementation 'org.jetbrains.kotlin:kotlin-scripting-common:1.7.20'
implementation 'com.fifesoft:autocomplete:3.3.0'
// use ktlint for lint and format jadx scripts
implementation 'com.pinterest.ktlint:ktlint-core:0.47.1'
implementation 'com.pinterest.ktlint:ktlint-ruleset-standard:0.47.1'
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.3.5'
@@ -44,10 +48,13 @@ dependencies {
application {
applicationName = 'jadx-gui'
mainClass.set('jadx.gui.JadxGUI')
// The option -XX:+UseG1GC is only relevant for Java 8. Starting with Java 9 G1GC is already the default GC
applicationDefaultJvmArgs = ['-Xms128M', '-XX:MaxRAMPercentage=70.0', '-XX:+UseG1GC',
'-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true',
'-Djava.util.Arrays.useLegacyMergeSort=true']
applicationDefaultJvmArgs = [
'-Xms128M', '-XX:MaxRAMPercentage=70.0',
'-XX:+UseG1GC', // only relevant for Java 8, starting with Java 9 G1GC is already the default GC
'-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true',
'-Djava.util.Arrays.useLegacyMergeSort=true',
'-XX:+IgnoreUnrecognizedVMOptions', '--add-opens=java.base/java.lang=ALL-UNNAMED', // for ktlint formatter
]
}
applicationDistribution.with {
@@ -94,7 +101,7 @@ launch4j {
windowTitle = 'jadx'
companyName = 'jadx'
jreMinVersion = '11'
jvmOptions = ['-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true', '-XX:+UseG1GC', '-Djava.util.Arrays.useLegacyMergeSort=true']
jvmOptions = application.getApplicationDefaultJvmArgs()
jreRuntimeBits = "64"
bundledJre64Bit = true
initialHeapPercent = 5
@@ -182,9 +182,9 @@ public class DiskCodeCache implements ICodeCache {
@Override
public void remove(String clsFullName) {
try {
LOG.debug("Removing class info from disk: {}", clsFullName);
Integer clsId = namesMap.remove(clsFullName);
if (clsId != null) {
LOG.debug("Removing class info from disk: {}", clsFullName);
Files.deleteIfExists(getJavaFile(clsId));
Files.deleteIfExists(getMetadataFile(clsId));
}
@@ -0,0 +1,46 @@
package jadx.gui.plugins.script
import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider
import org.slf4j.Logger
import org.slf4j.LoggerFactory
object KtLintUtils {
val LOG: Logger = LoggerFactory.getLogger(KtLintUtils::class.java)
val rules = lazy { StandardRuleSetProvider().getRuleProviders() }
fun format(code: String, fileName: String): String {
val params = KtLint.ExperimentalParams(
text = code,
fileName = fileName,
ruleProviders = rules.value,
script = true,
cb = { e: LintError, corrected ->
if (!corrected) {
LOG.warn("Lint error: {}", e)
}
}
)
return KtLint.format(params)
}
fun lint(code: String, fileName: String): List<LintError> {
val errors = mutableListOf<LintError>()
val params = KtLint.ExperimentalParams(
text = code,
fileName = fileName,
ruleProviders = rules.value,
script = true,
cb = { e: LintError, corrected ->
if (!corrected) {
errors.add(e)
}
}
)
KtLint.lint(params)
return errors
}
}
@@ -0,0 +1,84 @@
package jadx.gui.plugins.script;
import java.awt.event.KeyEvent;
import javax.swing.KeyStroke;
import org.fife.ui.autocomplete.AutoCompletion;
import org.jetbrains.annotations.NotNull;
import jadx.api.ICodeInfo;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.UiUtils;
public class ScriptCodeArea extends AbstractCodeArea {
private final JInputScript scriptNode;
private final AutoCompletion autoCompletion;
public ScriptCodeArea(ContentPanel contentPanel, JInputScript node) {
super(contentPanel, node);
scriptNode = node;
setSyntaxEditingStyle(node.getSyntaxName());
setCodeFoldingEnabled(true);
setCloseCurlyBraces(true);
JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings();
autoCompletion = addAutoComplete(settings);
}
private AutoCompletion addAutoComplete(JadxSettings settings) {
ScriptCompleteProvider provider = new ScriptCompleteProvider(this);
provider.setAutoActivationRules(false, ".");
AutoCompletion ac = new AutoCompletion(provider);
ac.setListCellRenderer(new ScriptCompletionRenderer(settings));
ac.setTriggerKey(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, UiUtils.ctrlButton()));
ac.setAutoActivationEnabled(true);
ac.setAutoCompleteSingleChoices(true);
ac.install(this);
return ac;
}
@Override
public @NotNull ICodeInfo getCodeInfo() {
return node.getCodeInfo();
}
@Override
public void load() {
if (getText().isEmpty()) {
setText(getCodeInfo().getCodeStr());
setCaretPosition(0);
setLoaded();
}
}
@Override
public void refresh() {
setText(node.getCodeInfo().getCodeStr());
}
public void updateCode(String newCode) {
setText(newCode);
scriptNode.setChanged(true);
}
public void save() {
scriptNode.save(getText());
scriptNode.setChanged(false);
}
public JInputScript getScriptNode() {
return scriptNode;
}
@Override
public void dispose() {
autoCompletion.uninstall();
super.dispose();
}
}
@@ -27,13 +27,13 @@ import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.utils.Icons;
import jadx.plugins.script.ide.JadxScriptAutoComplete;
import jadx.plugins.script.ide.ScriptCompiler;
import jadx.plugins.script.ide.ScriptCompletionResult;
import static jadx.plugins.script.ide.CompleteKt.AUTO_COMPLETE_INSERT_STR;
import static jadx.plugins.script.ide.ScriptCompilerKt.AUTO_COMPLETE_INSERT_STR;
public class JadxScriptCompleteProvider extends CompletionProviderBase {
private static final Logger LOG = LoggerFactory.getLogger(JadxScriptCompleteProvider.class);
public class ScriptCompleteProvider extends CompletionProviderBase {
private static final Logger LOG = LoggerFactory.getLogger(ScriptCompleteProvider.class);
private static final Map<String, Icon> ICONS_MAP = buildIconsMap();
@@ -49,16 +49,19 @@ public class JadxScriptCompleteProvider extends CompletionProviderBase {
}
private final AbstractCodeArea codeArea;
private ScriptCompiler scriptComplete;
public JadxScriptCompleteProvider(AbstractCodeArea codeArea) {
public ScriptCompleteProvider(AbstractCodeArea codeArea) {
this.codeArea = codeArea;
// this.scriptComplete = new ScriptCompiler(codeArea.getNode().getName());
}
private List<Completion> getCompletions() {
try {
String code = codeArea.getText();
int caretPos = codeArea.getCaretPosition();
JadxScriptAutoComplete scriptComplete = new JadxScriptAutoComplete(codeArea.getNode().getName());
// TODO: resolve error after reusing ScriptCompiler
scriptComplete = new ScriptCompiler(codeArea.getNode().getName());
ScriptCompletionResult result = scriptComplete.complete(code, caretPos);
int replacePos = getReplacePos(caretPos, result);
if (!result.getReports().isEmpty()) {
@@ -88,7 +91,7 @@ public class JadxScriptCompleteProvider extends CompletionProviderBase {
continue;
}
JadxScriptCompletion cmpl = new JadxScriptCompletion(this, count - i);
ScriptCompletionData cmpl = new ScriptCompletionData(this, count - i);
cmpl.setData(c.getText(), code, replacePos);
if (Objects.equals(c.getIcon(), "method") && !Objects.equals(c.getText(), c.getDisplayText())) {
// add method args details for methods
@@ -6,7 +6,7 @@ import javax.swing.text.JTextComponent;
import org.fife.ui.autocomplete.Completion;
import org.fife.ui.autocomplete.CompletionProvider;
public class JadxScriptCompletion implements Completion {
public class ScriptCompletionData implements Completion {
private final CompletionProvider provider;
private final int relevance;
@@ -18,7 +18,7 @@ public class JadxScriptCompletion implements Completion {
private String summary;
private String toolTip;
public JadxScriptCompletion(CompletionProvider provider, int relevance) {
public ScriptCompletionData(CompletionProvider provider, int relevance) {
this.provider = provider;
this.relevance = relevance;
}
@@ -11,15 +11,15 @@ import static jadx.gui.utils.UiUtils.escapeHtml;
import static jadx.gui.utils.UiUtils.fadeHtml;
import static jadx.gui.utils.UiUtils.wrapHtml;
public class CompletionRenderer extends CompletionCellRenderer {
public class ScriptCompletionRenderer extends CompletionCellRenderer {
public CompletionRenderer(JadxSettings settings) {
public ScriptCompletionRenderer(JadxSettings settings) {
setDisplayFont(settings.getFont());
}
@Override
protected void prepareForOtherCompletion(JList list, Completion c, int index, boolean selected, boolean hasFocus) {
JadxScriptCompletion cmpl = (JadxScriptCompletion) c;
ScriptCompletionData cmpl = (ScriptCompletionData) c;
setText(wrapHtml(escapeHtml(cmpl.getInputText()) + " "
+ fadeHtml(escapeHtml(cmpl.getSummary()))));
}
@@ -0,0 +1,233 @@
package jadx.gui.plugins.script;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.util.Collections;
import java.util.List;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.border.EmptyBorder;
import org.fife.ui.rsyntaxtextarea.ErrorStrip;
import org.fife.ui.rtextarea.RTextScrollPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.pinterest.ktlint.core.LintError;
import kotlin.script.experimental.api.ScriptDiagnostic;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
import jadx.gui.ui.codearea.SearchBar;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.ActionHandler;
import jadx.gui.utils.ui.NodeLabel;
import jadx.plugins.script.ide.ScriptAnalyzeResult;
import jadx.plugins.script.ide.ScriptCompiler;
public class ScriptContentPanel extends AbstractCodeContentPanel {
private static final long serialVersionUID = 6575696321112417513L;
private static final Logger LOG = LoggerFactory.getLogger(ScriptContentPanel.class);
private final ScriptCodeArea scriptArea;
private final SearchBar searchBar;
private final RTextScrollPane codeScrollPane;
private final JPanel actionPanel;
private final JLabel resultLabel;
private final ScriptErrorService errorService;
public ScriptContentPanel(TabbedPane panel, JInputScript scriptNode) {
super(panel, scriptNode);
scriptArea = new ScriptCodeArea(this, scriptNode);
resultLabel = new NodeLabel("");
errorService = new ScriptErrorService(scriptArea);
actionPanel = buildScriptActionsPanel();
searchBar = new SearchBar(scriptArea);
codeScrollPane = new RTextScrollPane(scriptArea);
initUI();
applySettings();
scriptArea.load();
}
private void initUI() {
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
topPanel.add(actionPanel, BorderLayout.NORTH);
topPanel.add(searchBar, BorderLayout.SOUTH);
JPanel codePanel = new JPanel(new BorderLayout());
codePanel.setBorder(new EmptyBorder(0, 0, 0, 0));
codePanel.add(codeScrollPane);
codePanel.add(new ErrorStrip(scriptArea), BorderLayout.LINE_END);
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 0, 0));
add(topPanel, BorderLayout.NORTH);
add(codeScrollPane, BorderLayout.CENTER);
KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton());
UiUtils.addKeyBinding(scriptArea, key, "SearchAction", searchBar::toggle);
}
private JPanel buildScriptActionsPanel() {
ActionHandler runAction = new ActionHandler(this::runScript);
runAction.setNameAndDesc(NLS.str("script.run"));
runAction.setIcon(Icons.RUN);
runAction.attachKeyBindingFor(scriptArea, KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0));
ActionHandler saveAction = new ActionHandler(scriptArea::save);
saveAction.setNameAndDesc(NLS.str("script.save"));
saveAction.setIcon(Icons.SAVE_ALL);
saveAction.setKeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_S, UiUtils.ctrlButton()));
JButton save = saveAction.makeButton();
scriptArea.getScriptNode().addChangeListener(save::setEnabled);
JButton check = new JButton(NLS.str("script.check"), Icons.CHECK);
check.addActionListener(ev -> checkScript());
JButton format = new JButton(NLS.str("script.format"), Icons.FORMAT);
format.addActionListener(ev -> reformatCode());
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
panel.setBorder(new EmptyBorder(0, 0, 0, 0));
panel.add(runAction.makeButton());
panel.add(Box.createRigidArea(new Dimension(10, 0)));
panel.add(save);
panel.add(Box.createRigidArea(new Dimension(10, 0)));
panel.add(check);
panel.add(Box.createRigidArea(new Dimension(10, 0)));
panel.add(format);
panel.add(Box.createRigidArea(new Dimension(30, 0)));
panel.add(resultLabel);
panel.add(Box.createHorizontalGlue());
return panel;
}
private void runScript() {
scriptArea.save();
if (!checkScript()) {
return;
}
resetResultLabel();
TabbedPane tabbedPane = getTabbedPane();
MainWindow mainWindow = tabbedPane.getMainWindow();
mainWindow.getBackgroundExecutor().execute(NLS.str("script.run"), () -> {
try {
mainWindow.getWrapper().getDecompiler().reloadPasses();
} catch (Exception e) {
LOG.error("Passes reload failed", e);
}
}, taskStatus -> {
tabbedPane.reloadInactiveTabs();
mainWindow.reloadTree();
});
}
private boolean checkScript() {
try {
resetResultLabel();
String code = scriptArea.getText();
String fileName = scriptArea.getNode().getName();
ScriptCompiler scriptCompiler = new ScriptCompiler(fileName);
ScriptAnalyzeResult result = scriptCompiler.analyze(code, scriptArea.getCaretPosition());
List<ScriptDiagnostic> errors = result.getErrors();
for (ScriptDiagnostic error : errors) {
LOG.warn("Parse error: {}", error);
}
List<LintError> lintErrs = Collections.emptyList();
if (errors.isEmpty()) {
lintErrs = getLintIssues(code, fileName);
}
errorService.clearErrors();
errorService.addErrors(errors);
errorService.addLintErrors(lintErrs);
errorService.apply();
boolean success = errors.isEmpty();
if (!success) {
resultLabel.setText("Parsing errors: " + errors.size());
} else if (!lintErrs.isEmpty()) {
resultLabel.setText("Lint issues: " + lintErrs.size());
}
return success;
} catch (Throwable e) {
LOG.error("Failed to check code", e);
return true;
}
}
private List<LintError> getLintIssues(String code, String fileName) {
try {
List<LintError> lintErrs = KtLintUtils.INSTANCE.lint(code, fileName);
for (LintError error : lintErrs) {
LOG.warn("Lint issue: {}", error);
}
return lintErrs;
} catch (Throwable e) { // can throw initialization error
LOG.warn("KtLint failed", e);
return Collections.emptyList();
}
}
private void reformatCode() {
resetResultLabel();
try {
String code = scriptArea.getText();
String fileName = scriptArea.getNode().getName();
String formattedCode = KtLintUtils.INSTANCE.format(code, fileName);
if (!code.equals(formattedCode)) {
scriptArea.updateCode(formattedCode);
resultLabel.setText("Code updated");
errorService.clearErrors();
}
} catch (Throwable e) { // can throw initialization error
LOG.error("Failed to reformat code", e);
}
}
private void resetResultLabel() {
resultLabel.setText("");
}
private void applySettings() {
JadxSettings settings = getSettings();
codeScrollPane.setLineNumbersEnabled(settings.getLineNumbersMode() != LineNumbersMode.DISABLE);
codeScrollPane.getGutter().setLineNumberFont(settings.getFont());
scriptArea.loadSettings();
}
@Override
public AbstractCodeArea getCodeArea() {
return scriptArea;
}
@Override
public void loadSettings() {
applySettings();
updateUI();
}
public void dispose() {
scriptArea.dispose();
}
}
@@ -0,0 +1,107 @@
package jadx.gui.plugins.script;
import java.util.List;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.parser.AbstractParser;
import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult;
import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice;
import org.fife.ui.rsyntaxtextarea.parser.ParseResult;
import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.pinterest.ktlint.core.LintError;
import kotlin.script.experimental.api.ScriptDiagnostic;
import kotlin.script.experimental.api.SourceCode;
public class ScriptErrorService extends AbstractParser {
private static final Logger LOG = LoggerFactory.getLogger(ScriptErrorService.class);
private final DefaultParseResult result;
private final ScriptCodeArea scriptArea;
public ScriptErrorService(ScriptCodeArea scriptArea) {
this.scriptArea = scriptArea;
this.result = new DefaultParseResult(this);
}
@Override
public ParseResult parse(RSyntaxDocument doc, String style) {
return result;
}
public void clearErrors() {
result.clearNotices();
scriptArea.removeParser(this);
}
public void apply() {
scriptArea.removeParser(this);
scriptArea.addParser(this);
scriptArea.addNotify();
jumpCaretToFirstError();
}
private void jumpCaretToFirstError() {
List<ParserNotice> parserNotices = result.getNotices();
if (parserNotices.isEmpty()) {
return;
}
ParserNotice notice = parserNotices.get(0);
int offset = notice.getOffset();
if (offset != -1) {
scriptArea.setCaretPosition(offset);
} else {
try {
scriptArea.setCaretPosition(scriptArea.getLineStartOffset(notice.getLine()));
} catch (Exception e) {
LOG.error("Failed to jump to first error", e);
}
}
}
public void addErrors(List<ScriptDiagnostic> errors) {
for (ScriptDiagnostic error : errors) {
DefaultParserNotice notice;
SourceCode.Location loc = error.getLocation();
if (loc == null) {
notice = new DefaultParserNotice(this, error.getMessage(), 0);
} else {
try {
int line = loc.getStart().getLine();
int offset = scriptArea.getLineStartOffset(line - 1) + loc.getStart().getCol();
int len = loc.getEnd() == null ? -1 : loc.getEnd().getCol() - loc.getStart().getCol();
notice = new DefaultParserNotice(this, error.getMessage(), line, offset - 1, len);
} catch (Exception e) {
LOG.error("Failed to convert script error", e);
continue;
}
}
addNotice(notice);
}
}
public void addLintErrors(List<LintError> errors) {
for (LintError error : errors) {
try {
int line = error.getLine();
int offset = scriptArea.getLineStartOffset(line - 1) + error.getCol() - 1;
String word = scriptArea.getWordByPosition(offset);
int len = word != null ? word.length() : -1;
DefaultParserNotice notice = new DefaultParserNotice(this, error.getDetail(), line, offset, len);
notice.setLevel(ParserNotice.Level.WARNING);
addNotice(notice);
} catch (Exception e) {
LOG.error("Failed to convert lint error", e);
}
}
}
private void addNotice(DefaultParserNotice notice) {
LOG.debug("Add notice: {}:{}:{} - {}",
notice.getLine(), notice.getOffset(), notice.getLength(), notice.getMessage());
result.addNotice(notice);
}
}
@@ -31,5 +31,6 @@ public abstract class JEditableNode extends JNode {
public void addChangeListener(Consumer<Boolean> listener) {
changeListeners.add(listener);
listener.accept(changed);
}
}
@@ -15,9 +15,9 @@ import jadx.api.ICodeInfo;
import jadx.api.impl.SimpleCodeInfo;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.plugins.script.ScriptContentPanel;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -38,7 +38,7 @@ public class JInputScript extends JEditableNode {
@Override
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
return new CodeContentPanel(tabbedPane, this);
return new ScriptContentPanel(tabbedPane, this);
}
@Override
@@ -69,11 +69,6 @@ public class JInputScript extends JEditableNode {
return menu;
}
@Override
public boolean isEditable() {
return true;
}
@Override
public String getSyntaxName() {
return SyntaxConstants.SYNTAX_STYLE_KOTLIN;
@@ -163,7 +163,6 @@ public class MainWindow extends JFrame {
public static final double SPLIT_PANE_RESIZE_WEIGHT = 0.15;
private static final ImageIcon ICON_ADD_FILES = UiUtils.openSvgIcon("ui/addFile");
private static final ImageIcon ICON_SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall");
private static final ImageIcon ICON_RELOAD = UiUtils.openSvgIcon("ui/refresh");
private static final ImageIcon ICON_EXPORT = UiUtils.openSvgIcon("ui/export");
private static final ImageIcon ICON_EXIT = UiUtils.openSvgIcon("ui/exit");
@@ -1083,7 +1082,7 @@ public class MainWindow extends JFrame {
};
closeMappingsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.close_mappings"));
Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), ICON_SAVE_ALL) {
Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), Icons.SAVE_ALL) {
@Override
public void actionPerformed(ActionEvent e) {
saveAll(false);
@@ -33,6 +33,7 @@ import jadx.gui.ui.panel.ImagePanel;
import jadx.gui.utils.JumpManager;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public class TabbedPane extends JTabbedPane {
private static final long serialVersionUID = -8833600618794570904L;
@@ -332,7 +333,9 @@ public class TabbedPane extends JTabbedPane {
private void addContentPanel(ContentPanel contentPanel) {
openTabs.put(contentPanel.getNode(), contentPanel);
add(contentPanel);
int tabCount = getTabCount();
add(contentPanel, tabCount);
setTabComponentAt(tabCount, makeTabComponent(contentPanel));
}
public void closeCodePanel(ContentPanel contentPanel) {
@@ -351,7 +354,6 @@ public class TabbedPane extends JTabbedPane {
}
FocusManager.listen(panel);
addContentPanel(panel);
setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel));
}
return panel;
}
@@ -364,6 +366,27 @@ public class TabbedPane extends JTabbedPane {
}
}
public void reloadInactiveTabs() {
UiUtils.uiThreadGuard();
int tabCount = getTabCount();
if (tabCount == 1) {
return;
}
int current = getSelectedIndex();
for (int i = 0; i < tabCount; i++) {
if (i == current) {
continue;
}
JNode node = ((ContentPanel) getComponentAt(i)).getNode();
ContentPanel panel = node.getContentPanel(this);
FocusManager.listen(panel);
openTabs.put(node, panel);
setComponentAt(i, panel);
setTabComponentAt(i, makeTabComponent(panel));
}
fireStateChanged();
}
@Nullable
public ContentPanel getSelectedCodePanel() {
return (ContentPanel) getSelectedComponent();
@@ -20,7 +20,6 @@ import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
@@ -30,7 +29,6 @@ import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultCaret;
import org.fife.ui.autocomplete.AutoCompletion;
import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.Token;
@@ -46,12 +44,9 @@ import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.plugins.script.CompletionRenderer;
import jadx.gui.plugins.script.JadxScriptCompleteProvider;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JEditableNode;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ContentPanel;
@@ -106,9 +101,6 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
JEditableNode editableNode = (JEditableNode) node;
addSaveActions(editableNode);
addChangeUpdates(editableNode);
if (node instanceof JInputScript) {
addAutoComplete(settings);
}
} else {
addCaretActions();
addFastCopyAction();
@@ -224,17 +216,6 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}));
}
private void addAutoComplete(JadxSettings settings) {
JadxScriptCompleteProvider provider = new JadxScriptCompleteProvider(this);
provider.setAutoActivationRules(false, ".");
AutoCompletion ac = new AutoCompletion(provider);
ac.setListCellRenderer(new CompletionRenderer(settings));
ac.setTriggerKey(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, UiUtils.ctrlButton()));
ac.setAutoActivationEnabled(true);
ac.setAutoCompleteSingleChoices(true);
ac.install(this);
}
private String highlightCaretWord(String lastText, int pos) {
String text = getWordByPosition(pos);
if (StringUtils.isEmpty(text)) {
@@ -24,7 +24,7 @@ import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
class SearchBar extends JToolBar {
public class SearchBar extends JToolBar {
private static final long serialVersionUID = 1836871286618633003L;
private static final Logger LOG = LoggerFactory.getLogger(SearchBar.class);
@@ -4,6 +4,7 @@ import javax.swing.JPanel;
import org.jetbrains.annotations.Nullable;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
@@ -45,6 +46,10 @@ public abstract class ContentPanel extends JPanel {
return node.getName();
}
public JadxSettings getSettings() {
return tabbedPane.getMainWindow().getSettings();
}
public void dispose() {
tabbedPane = null;
node = null;
@@ -13,6 +13,8 @@ public class Icons {
public static final ImageIcon CLOSE = openSvgIcon("ui/closeHovered");
public static final ImageIcon CLOSE_INACTIVE = openSvgIcon("ui/close");
public static final ImageIcon SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall");
public static final ImageIcon STATIC = openSvgIcon("nodes/staticMark");
public static final ImageIcon FINAL = openSvgIcon("nodes/finalMark");
@@ -27,4 +29,8 @@ public class Icons {
public static final ImageIcon FIELD = UiUtils.openSvgIcon("nodes/field");
public static final ImageIcon PROPERTY = UiUtils.openSvgIcon("nodes/property");
public static final ImageIcon PARAMETER = UiUtils.openSvgIcon("nodes/parameter");
public static final ImageIcon RUN = UiUtils.openSvgIcon("ui/run");
public static final ImageIcon CHECK = UiUtils.openSvgIcon("ui/checkConstraint");
public static final ImageIcon FORMAT = UiUtils.openSvgIcon("ui/toolWindowMessages");
}
@@ -1,12 +1,17 @@
package jadx.gui.utils.ui;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.function.Consumer;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import jadx.gui.utils.UiUtils;
public class ActionHandler extends AbstractAction {
private final Consumer<ActionEvent> consumer;
@@ -40,8 +45,27 @@ public class ActionHandler extends AbstractAction {
putValue(ACCELERATOR_KEY, keyStroke);
}
public void attachKeyBindingFor(JComponent component, KeyStroke keyStroke) {
UiUtils.addKeyBinding(component, keyStroke, "run", this);
setKeyBinding(keyStroke);
}
public void addKeyBindToDescription() {
KeyStroke keyStroke = (KeyStroke) getValue(ACCELERATOR_KEY);
if (keyStroke != null) {
String keyText = KeyEvent.getKeyText(keyStroke.getKeyCode());
String desc = (String) getValue(SHORT_DESCRIPTION);
setShortDescription(desc + " (" + keyText + ")");
}
}
@Override
public void actionPerformed(ActionEvent e) {
consumer.accept(e);
}
public JButton makeButton() {
addKeyBindToDescription();
return new JButton(this);
}
}
@@ -248,6 +248,11 @@ popup.search_global=Globale Suche "%s"
#popup.add_scripts=Add scripts
#popup.new_script=New script
#script.run=Run
#script.save=Save
#script.check=Check
#script.format=Reformat
exclude_dialog.title=Paketauswahl
exclude_dialog.ok=OK
exclude_dialog.select_all=Alles auswählen
@@ -248,6 +248,11 @@ popup.add_files=Add files
popup.add_scripts=Add scripts
popup.new_script=New script
script.run=Run
script.save=Save
script.check=Check
script.format=Reformat
exclude_dialog.title=Package Selector
exclude_dialog.ok=OK
exclude_dialog.select_all=Select all
@@ -248,6 +248,11 @@ popup.rename=Nimeta ümber
#popup.add_scripts=Add scripts
#popup.new_script=New script
#script.run=Run
#script.save=Save
#script.check=Check
#script.format=Reformat
#exclude_dialog.title=Package Selector
#exclude_dialog.ok=OK
#exclude_dialog.select_all=Select all
@@ -248,6 +248,11 @@ popup.search_global="%s" 전역 검색
#popup.add_scripts=Add scripts
#popup.new_script=New script
#script.run=Run
#script.save=Save
#script.check=Check
#script.format=Reformat
exclude_dialog.title=패키지 선택기
exclude_dialog.ok=확인
exclude_dialog.select_all=모두 선택
@@ -248,6 +248,11 @@ popup.search_global=Busca global "%s"
#popup.add_scripts=Add scripts
#popup.new_script=New script
#script.run=Run
#script.save=Save
#script.check=Check
#script.format=Reformat
exclude_dialog.title=Selecionar pacote
exclude_dialog.ok=OK
exclude_dialog.select_all=Selecionar tudo
@@ -248,6 +248,11 @@ popup.search_global=全局搜索 “%s”
#popup.add_scripts=Add scripts
#popup.new_script=New script
#script.run=Run
#script.save=Save
#script.check=Check
#script.format=Reformat
exclude_dialog.title=选择要排除的包
exclude_dialog.ok=确定
exclude_dialog.select_all=全选
@@ -248,6 +248,11 @@ popup.search_global=全域搜尋 "%s"
#popup.add_scripts=Add scripts
#popup.new_script=New script
#script.run=Run
#script.save=Save
#script.check=Check
#script.format=Reformat
exclude_dialog.title=套件選擇
exclude_dialog.ok=OK
exclude_dialog.select_all=全選
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="#9AA7B0" fill-rule="evenodd" transform="translate(2 2)">
<rect width="12" height="2"/>
<rect width="2" height="2" y="8"/>
<rect width="9" height="2" y="4"/>
<path d="M9.60330086,10.1966989 L9.60330086,3.19669886 L11.6033009,3.19669886 L11.6033009,10.1966989 L11.6033009,12.1966989 L5.60330086,12.1966989 L5.60330086,10.1966989 L9.60330086,10.1966989 Z" transform="rotate(45 8.603 7.697)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 515 B

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<polygon fill="#59A869" fill-rule="evenodd" points="3 1 10 6 3 11"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

@@ -0,0 +1,8 @@
<!-- Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13">
<g fill="none" fill-rule="evenodd">
<rect width="11" height="2" x="1" y="2" fill="#6E6E6E"/>
<rect width="9" height="2" x="1" y="8" fill="#6E6E6E"/>
<rect width="6" height="2" x="1" y="5" fill="#6E6E6E"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 482 B

@@ -6,12 +6,18 @@ dependencies {
implementation("io.github.microutils:kotlin-logging-jvm:3.0.2")
// script context support in IDE is poor, use stubs and manual imports for now
// kotlinScriptDef(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
// manual imports (IDE can't import dependencies by scripts annotations)
implementation("com.github.javafaker:javafaker:1.0.2")
}
sourceSets {
main {
java.srcDirs("scripts", "context")
kotlin.srcDirs(
"scripts",
"context"
)
}
}
@@ -3,11 +3,14 @@ package jadx.plugins.script.ide
import jadx.plugins.script.runner.ScriptEval
import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.scripting.ide_services.compiler.KJvmReplCompilerWithIdeServices
import kotlin.script.experimental.api.ReplAnalyzerResult
import kotlin.script.experimental.api.ReplCompletionResult
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.api.SourceCodeCompletionVariant
import kotlin.script.experimental.api.analysisDiagnostics
import kotlin.script.experimental.api.renderedResultType
import kotlin.script.experimental.api.valueOrNull
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.util.toSourceCodePosition
@@ -19,14 +22,30 @@ data class ScriptCompletionResult(
val reports: List<ScriptDiagnostic>
)
class JadxScriptAutoComplete(private val scriptName: String) {
data class ScriptAnalyzeResult(
val errors: List<ScriptDiagnostic>,
val renderType: String?,
val reports: List<ScriptDiagnostic>
)
class ScriptCompiler(private val scriptName: String) {
private val replCompiler = KJvmReplCompilerWithIdeServices()
private val compileConf = ScriptEval().buildCompileConf()
fun complete(code: String, cursor: Int): ScriptCompletionResult {
val result = complete(code.toScriptSource(scriptName), cursor)
return ScriptCompletionResult(
completions = result.valueOrNull()?.toList() ?: listOf(),
completions = result.valueOrNull()?.toList() ?: emptyList(),
reports = result.reports
)
}
fun analyze(code: String, cursor: Int): ScriptAnalyzeResult {
val result = analyze(code.toScriptSource(scriptName), cursor)
val analyzerResult = result.valueOrNull()
return ScriptAnalyzeResult(
errors = analyzerResult?.get(ReplAnalyzerResult.analysisDiagnostics)?.toList() ?: emptyList(),
renderType = analyzerResult?.get(ReplAnalyzerResult.renderedResultType),
reports = result.reports
)
}
@@ -36,4 +55,10 @@ class JadxScriptAutoComplete(private val scriptName: String) {
replCompiler.complete(code, cursor.toSourceCodePosition(code), compileConf)
}
}
private fun analyze(code: SourceCode, cursor: Int): ResultWithDiagnostics<ReplAnalyzerResult> {
return runBlocking {
replCompiler.analyze(code, cursor.toSourceCodePosition(code), compileConf)
}
}
}
@@ -2,8 +2,8 @@ package jadx.plugins.script.runner
import jadx.api.JadxDecompiler
import jadx.api.plugins.JadxPluginContext
import jadx.plugins.script.runtime.JadxScript
import jadx.plugins.script.runtime.JadxScriptData
import jadx.plugins.script.runtime.JadxScriptTemplate
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
import mu.KotlinLogging
import java.io.File
@@ -43,10 +43,10 @@ class ScriptEval {
processEvalResult(result, scriptFile)
}
fun buildCompileConf() = createJvmCompilationConfigurationFromTemplate<JadxScript>()
fun buildCompileConf() = createJvmCompilationConfigurationFromTemplate<JadxScriptTemplate>()
fun buildEvalConf(scriptData: JadxScriptData): ScriptEvaluationConfiguration {
return createJvmEvaluationConfigurationFromTemplate<JadxScript> {
return createJvmEvaluationConfigurationFromTemplate<JadxScriptTemplate> {
constructorArgs(scriptData)
}
}
@@ -5,12 +5,13 @@ plugins {
dependencies {
api(project(":jadx-core"))
implementation("org.jetbrains.kotlin:kotlin-scripting-common")
implementation("org.jetbrains.kotlin:kotlin-scripting-jvm")
implementation(kotlin("stdlib"))
implementation(kotlin("scripting-common"))
implementation(kotlin("scripting-jvm"))
// allow to use maven dependencies in scripts
implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies")
implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven")
implementation(kotlin("scripting-dependencies"))
implementation(kotlin("scripting-dependencies-maven"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("io.github.microutils:kotlin-logging-jvm:3.0.2")
@@ -1,6 +1,7 @@
package jadx.plugins.script.runtime
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import kotlin.script.experimental.annotations.KotlinScript
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptAcceptedLocation
@@ -9,11 +10,11 @@ import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptConfigurationRefinementContext
import kotlin.script.experimental.api.acceptedLocations
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.api.baseClass
import kotlin.script.experimental.api.collectedAnnotations
import kotlin.script.experimental.api.defaultImports
import kotlin.script.experimental.api.dependencies
import kotlin.script.experimental.api.ide
import kotlin.script.experimental.api.isStandalone
import kotlin.script.experimental.api.onSuccess
import kotlin.script.experimental.api.refineConfiguration
import kotlin.script.experimental.api.with
@@ -31,7 +32,22 @@ import kotlin.script.experimental.jvm.jvm
fileExtension = "jadx.kts",
compilationConfiguration = JadxScriptConfiguration::class
)
abstract class JadxScript
abstract class JadxScriptTemplate(
private val scriptData: JadxScriptData
) {
val scriptName = scriptData.scriptName
val log = KotlinLogging.logger("JadxScript:$scriptName")
fun getJadxInstance() = JadxScriptInstance(scriptData, log)
fun println(message: Any?) {
log.info(message?.toString())
}
fun print(message: Any?) {
log.info(message?.toString())
}
}
object JadxScriptConfiguration : ScriptCompilationConfiguration({
defaultImports(DependsOn::class, Repository::class)
@@ -45,11 +61,11 @@ object JadxScriptConfiguration : ScriptCompilationConfiguration({
acceptedLocations(ScriptAcceptedLocation.Everywhere)
}
baseClass(JadxScriptBaseClass::class)
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
isStandalone(false)
})
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
@@ -17,24 +17,8 @@ import jadx.plugins.script.runtime.data.Replace
import jadx.plugins.script.runtime.data.Search
import jadx.plugins.script.runtime.data.Stages
import mu.KLogger
import mu.KotlinLogging
import java.io.File
open class JadxScriptBaseClass(private val scriptData: JadxScriptData) {
val scriptName = scriptData.scriptName
val log = KotlinLogging.logger("JadxScript:$scriptName")
fun getJadxInstance() = JadxScriptInstance(scriptData, log)
fun println(message: Any?) {
log.info(message?.toString())
}
fun print(message: Any?) {
log.info(message?.toString())
}
}
class JadxScriptData(
val jadxInstance: JadxDecompiler,
val pluginContext: JadxPluginContext,
@@ -45,7 +45,6 @@ class JadxScriptOptions(
private val jadx: JadxScriptInstance,
private val options: JadxScriptAllOptions
) {
fun <T> register(
name: String,
desc: String,