feat: make jadx-script-kotlin plugin external
This commit is contained in:
@@ -16,21 +16,12 @@ dependencies {
|
||||
// import mappings
|
||||
implementation(project(":jadx-plugins:jadx-rename-mappings"))
|
||||
|
||||
// jadx-script autocomplete support
|
||||
implementation(project(":jadx-plugins:jadx-script:jadx-script-ide"))
|
||||
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
|
||||
implementation(kotlin("scripting-common"))
|
||||
implementation("com.fifesoft:autocomplete:3.3.2")
|
||||
|
||||
// use KtLint for format and check jadx scripts
|
||||
implementation("com.pinterest.ktlint:ktlint-rule-engine:1.8.0")
|
||||
implementation("com.pinterest.ktlint:ktlint-ruleset-standard:1.8.0")
|
||||
|
||||
implementation("org.jcommander:jcommander:2.0")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.21")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
|
||||
|
||||
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
|
||||
implementation("com.fifesoft:rsyntaxtextarea:3.6.1")
|
||||
implementation("com.fifesoft:autocomplete:3.3.2")
|
||||
implementation("org.drjekyll:fontchooser:3.1.0")
|
||||
implementation("hu.kazocsaba:image-viewer:1.2.3")
|
||||
implementation("com.twelvemonkeys.imageio:imageio-webp:3.12.0") // WebP support for image viewer
|
||||
|
||||
@@ -21,6 +21,9 @@ import jadx.api.JavaPackage;
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.impl.InMemoryCodeCache;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.api.plugins.pass.JadxPassInfo;
|
||||
import jadx.api.plugins.pass.impl.SimpleJadxPassInfo;
|
||||
import jadx.api.plugins.pass.types.JadxPreparePass;
|
||||
import jadx.api.usage.impl.EmptyUsageInfoCache;
|
||||
import jadx.api.usage.impl.InMemoryUsageInfoCache;
|
||||
import jadx.cli.JadxAppCommon;
|
||||
@@ -30,6 +33,7 @@ import jadx.core.dex.nodes.ProcessState;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.plugins.AppContext;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.cache.code.CodeCacheMode;
|
||||
import jadx.gui.cache.code.CodeStringCache;
|
||||
import jadx.gui.cache.code.disk.BufferCodeCache;
|
||||
import jadx.gui.cache.code.disk.DiskCodeCache;
|
||||
@@ -73,10 +77,9 @@ public class JadxWrapper {
|
||||
decompiler = new JadxDecompiler(jadxArgs);
|
||||
guiPluginsContext = initGuiPluginsContext(decompiler, mainWindow);
|
||||
initUsageCache(jadxArgs);
|
||||
registerCodeCache(decompiler);
|
||||
decompiler.setEventsImpl(mainWindow.events());
|
||||
|
||||
decompiler.load();
|
||||
initCodeCache();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Jadx decompiler wrapper init error", e);
|
||||
@@ -114,22 +117,39 @@ public class JadxWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
private void initCodeCache() {
|
||||
switch (getSettings().getCodeCacheMode()) {
|
||||
case MEMORY:
|
||||
getArgs().setCodeCache(new InMemoryCodeCache());
|
||||
break;
|
||||
case DISK_WITH_CACHE:
|
||||
getArgs().setCodeCache(new CodeStringCache(buildBufferedDiskCache()));
|
||||
break;
|
||||
case DISK:
|
||||
getArgs().setCodeCache(buildBufferedDiskCache());
|
||||
break;
|
||||
/**
|
||||
* Disk cache require loaded classes to operate, but cache should be set before 'after load' event
|
||||
* to allow plugins decompile classes with cache enabled.
|
||||
* To resolve this, register last 'prepare' pass for cache initialization.
|
||||
*/
|
||||
private void registerCodeCache(JadxDecompiler jadxDecompiler) {
|
||||
CodeCacheMode codeCacheMode = getSettings().getCodeCacheMode();
|
||||
if (codeCacheMode == CodeCacheMode.MEMORY) {
|
||||
jadxDecompiler.getArgs().setCodeCache(new InMemoryCodeCache());
|
||||
return;
|
||||
}
|
||||
jadxDecompiler.addCustomPass(new JadxPreparePass() {
|
||||
@Override
|
||||
public JadxPassInfo getInfo() {
|
||||
return new SimpleJadxPassInfo("CacheInit");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(RootNode root) {
|
||||
switch (getSettings().getCodeCacheMode()) {
|
||||
case DISK_WITH_CACHE:
|
||||
root.getArgs().setCodeCache(new CodeStringCache(buildBufferedDiskCache(root)));
|
||||
break;
|
||||
case DISK:
|
||||
root.getArgs().setCodeCache(buildBufferedDiskCache(root));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private BufferCodeCache buildBufferedDiskCache() {
|
||||
DiskCodeCache diskCache = new DiskCodeCache(getDecompiler().getRoot(), getProject().getCacheDir());
|
||||
private BufferCodeCache buildBufferedDiskCache(RootNode root) {
|
||||
DiskCodeCache diskCache = new DiskCodeCache(root, getProject().getCacheDir());
|
||||
return new BufferCodeCache(diskCache);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
|
||||
|
||||
class LogAppender implements ILogListener {
|
||||
private final LogOptions options;
|
||||
private final RSyntaxTextArea textArea;
|
||||
@@ -39,7 +37,7 @@ class LogAppender implements ILogListener {
|
||||
return true;
|
||||
|
||||
case ALL_SCRIPTS:
|
||||
return logEvent.getLoggerName().startsWith(JADX_SCRIPT_LOG_PREFIX);
|
||||
return logEvent.getLoggerName().startsWith("JadxScript:");
|
||||
|
||||
case CURRENT_SCRIPT:
|
||||
return logEvent.getLoggerName().equals(options.getFilter());
|
||||
|
||||
@@ -6,8 +6,6 @@ import ch.qos.logback.classic.Level;
|
||||
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
|
||||
|
||||
public class LogOptions {
|
||||
|
||||
/**
|
||||
@@ -30,7 +28,7 @@ public class LogOptions {
|
||||
}
|
||||
|
||||
public static LogOptions forScript(String scriptName) {
|
||||
String filter = JADX_SCRIPT_LOG_PREFIX + scriptName;
|
||||
String filter = "JadxScript:" + scriptName;
|
||||
return store(new LogOptions(LogMode.CURRENT_SCRIPT, current.getLogLevel(), filter));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
import ch.qos.logback.classic.Level;
|
||||
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
@@ -144,7 +143,7 @@ public class LogPanel extends JPanel {
|
||||
TabBlueprint selectedTab = mainWindow.getTabsController().getSelectedTab();
|
||||
if (selectedTab != null) {
|
||||
JNode node = selectedTab.getNode();
|
||||
if (node instanceof JInputScript) {
|
||||
if (node.getClass().getSimpleName().equals("JInputScript")) { // TODO: register custom log filters
|
||||
return node.getName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.gui.settings.data.ITabStatePersist;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.codearea.CodeArea;
|
||||
import jadx.gui.ui.codearea.JNodePopupBuilder;
|
||||
@@ -23,6 +24,8 @@ public class CommonGuiPluginsContext {
|
||||
|
||||
private final List<CodePopupAction> codePopupActionList = new ArrayList<>();
|
||||
private final List<TreePopupMenuEntry> treePopupMenuEntries = new ArrayList<>();
|
||||
private final List<ITreeInputCategory> treeInputCategories = new ArrayList<>();
|
||||
private final List<ITabStatePersist> tabStatePersistAdapters = new ArrayList<>();
|
||||
|
||||
public CommonGuiPluginsContext(MainWindow mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
@@ -38,9 +41,19 @@ public class CommonGuiPluginsContext {
|
||||
return pluginsMap.get(pluginContext);
|
||||
}
|
||||
|
||||
public @Nullable GuiPluginContext getGuiPluginContextById(String pluginId) {
|
||||
for (GuiPluginContext guiPluginContext : pluginsMap.values()) {
|
||||
if (guiPluginContext.getPluginContext().getPluginId().equals(pluginId)) {
|
||||
return guiPluginContext;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
codePopupActionList.clear();
|
||||
treePopupMenuEntries.clear();
|
||||
treeInputCategories.clear();
|
||||
mainWindow.resetPluginsMenu();
|
||||
}
|
||||
|
||||
@@ -56,6 +69,14 @@ public class CommonGuiPluginsContext {
|
||||
return treePopupMenuEntries;
|
||||
}
|
||||
|
||||
public List<ITreeInputCategory> getTreeInputCategories() {
|
||||
return treeInputCategories;
|
||||
}
|
||||
|
||||
public List<ITabStatePersist> getTabStatePersistAdapters() {
|
||||
return tabStatePersistAdapters;
|
||||
}
|
||||
|
||||
public void addMenuAction(String name, Runnable action) {
|
||||
ActionHandler item = new ActionHandler(ev -> {
|
||||
try {
|
||||
|
||||
@@ -26,6 +26,7 @@ import jadx.api.plugins.gui.JadxGuiContext;
|
||||
import jadx.api.plugins.gui.JadxGuiSettings;
|
||||
import jadx.core.plugins.PluginContext;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.settings.data.ITabStatePersist;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
|
||||
@@ -82,6 +83,14 @@ public class GuiPluginContext implements JadxGuiContext {
|
||||
commonContext.getTreePopupMenuEntries().add(new TreePopupMenuEntry(name, addPredicate, action));
|
||||
}
|
||||
|
||||
public void registerTreeInputCategory(ITreeInputCategory inputCategory) {
|
||||
commonContext.getTreeInputCategories().add(inputCategory);
|
||||
}
|
||||
|
||||
public void registerTabStatePersistAdapter(ITabStatePersist tabStatePersist) {
|
||||
commonContext.getTabStatePersistAdapters().add(tabStatePersist);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action) {
|
||||
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyBinding);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package jadx.gui.plugins.context;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
|
||||
/**
|
||||
* Custom category for 'Inputs' tree section
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
public interface ITreeInputCategory {
|
||||
|
||||
/**
|
||||
* Check if file should be moved into this category
|
||||
*/
|
||||
boolean filesFilter(Path file);
|
||||
|
||||
/**
|
||||
* Build node for filtered files
|
||||
*/
|
||||
JNode buildInputNode(List<Path> files);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package jadx.gui.plugins.script
|
||||
|
||||
import com.pinterest.ktlint.rule.engine.api.Code
|
||||
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride
|
||||
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
|
||||
import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
|
||||
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY
|
||||
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue
|
||||
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
|
||||
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider
|
||||
import org.ec4j.core.model.PropertyType
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object KtLintUtils {
|
||||
|
||||
val LOG: Logger = LoggerFactory.getLogger(KtLintUtils::class.java)
|
||||
|
||||
private val ktLint by lazy {
|
||||
KtLintRuleEngine(
|
||||
ruleProviders = StandardRuleSetProvider().getRuleProviders(),
|
||||
editorConfigOverride = EditorConfigOverride.from(
|
||||
CODE_STYLE_PROPERTY to CodeStyleValue.intellij_idea,
|
||||
INDENT_STYLE_PROPERTY to PropertyType.IndentStyleValue.tab,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun format(content: String): String {
|
||||
val code = Code.fromSnippet(content, script = true)
|
||||
return ktLint.format(
|
||||
code,
|
||||
rerunAfterAutocorrect = true,
|
||||
defaultAutocorrect = true,
|
||||
) { AutocorrectDecision.ALLOW_AUTOCORRECT }
|
||||
}
|
||||
|
||||
fun lint(content: String): List<JadxLintError> {
|
||||
val code = Code.fromSnippet(content, script = true)
|
||||
val errors = mutableListOf<JadxLintError>()
|
||||
ktLint.lint(code) { lintError ->
|
||||
errors.add(JadxLintError(lintError.line, lintError.col, lintError.ruleId.value, lintError.detail))
|
||||
}
|
||||
return errors
|
||||
}
|
||||
}
|
||||
|
||||
data class JadxLintError(
|
||||
val line: Int,
|
||||
val col: Int,
|
||||
val ruleId: String,
|
||||
val detail: String,
|
||||
)
|
||||
@@ -1,90 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import org.fife.ui.autocomplete.AutoCompletion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.gui.jobs.IBackgroundTask;
|
||||
import jadx.gui.jobs.LoadTask;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.ui.action.JadxAutoCompletion;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
import jadx.gui.ui.panel.ContentPanel;
|
||||
import jadx.gui.utils.shortcut.ShortcutsController;
|
||||
|
||||
public class ScriptCodeArea extends AbstractCodeArea {
|
||||
|
||||
private final JInputScript scriptNode;
|
||||
private final AutoCompletion autoCompletion;
|
||||
private final ShortcutsController shortcutsController;
|
||||
|
||||
public ScriptCodeArea(ContentPanel contentPanel, JInputScript node) {
|
||||
super(contentPanel, node);
|
||||
scriptNode = node;
|
||||
|
||||
setSyntaxEditingStyle(node.getSyntaxName());
|
||||
setCodeFoldingEnabled(true);
|
||||
setCloseCurlyBraces(true);
|
||||
|
||||
shortcutsController = contentPanel.getMainWindow().getShortcutsController();
|
||||
JadxSettings settings = contentPanel.getMainWindow().getSettings();
|
||||
autoCompletion = addAutoComplete(settings);
|
||||
}
|
||||
|
||||
private AutoCompletion addAutoComplete(JadxSettings settings) {
|
||||
ScriptCompleteProvider provider = new ScriptCompleteProvider(this);
|
||||
provider.setAutoActivationRules(false, ".");
|
||||
JadxAutoCompletion ac = new JadxAutoCompletion(provider);
|
||||
ac.setListCellRenderer(new ScriptCompletionRenderer(settings));
|
||||
ac.setAutoActivationEnabled(true);
|
||||
ac.setAutoCompleteSingleChoices(true);
|
||||
ac.install(this);
|
||||
shortcutsController.bindImmediate(ac);
|
||||
return ac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ICodeInfo getCodeInfo() {
|
||||
return node.getCodeInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBackgroundTask getLoadTask() {
|
||||
return new LoadTask<>(
|
||||
() -> node.getCodeInfo().getCodeStr(),
|
||||
code -> {
|
||||
setText(code);
|
||||
setCaretPosition(0);
|
||||
setLoaded();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
setText(node.getCodeInfo().getCodeStr());
|
||||
}
|
||||
|
||||
public void updateCode(String newCode) {
|
||||
int caretPos = getCaretPosition();
|
||||
setText(newCode);
|
||||
setCaretPosition(caretPos);
|
||||
scriptNode.setChanged(true);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
scriptNode.save(getText());
|
||||
scriptNode.setChanged(false);
|
||||
}
|
||||
|
||||
public JInputScript getScriptNode() {
|
||||
return scriptNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
shortcutsController.unbindActionsForComponent(this);
|
||||
autoCompletion.uninstall();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.JTextComponent;
|
||||
|
||||
import org.fife.ui.autocomplete.Completion;
|
||||
import org.fife.ui.autocomplete.CompletionProviderBase;
|
||||
import org.fife.ui.autocomplete.ParameterizedCompletion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import kotlin.script.experimental.api.ScriptDiagnostic;
|
||||
import kotlin.script.experimental.api.SourceCode;
|
||||
import kotlin.script.experimental.api.SourceCodeCompletionVariant;
|
||||
|
||||
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.ScriptCompletionResult;
|
||||
import jadx.plugins.script.ide.ScriptServices;
|
||||
|
||||
import static jadx.plugins.script.ide.ScriptServicesKt.AUTO_COMPLETE_INSERT_STR;
|
||||
|
||||
public class ScriptCompleteProvider extends CompletionProviderBase {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ScriptCompleteProvider.class);
|
||||
|
||||
private static final Map<String, Icon> ICONS_MAP = buildIconsMap();
|
||||
|
||||
private static Map<String, Icon> buildIconsMap() {
|
||||
Map<String, Icon> map = new HashMap<>();
|
||||
map.put("class", Icons.CLASS);
|
||||
map.put("method", Icons.METHOD);
|
||||
map.put("field", Icons.FIELD);
|
||||
map.put("property", Icons.PROPERTY);
|
||||
map.put("parameter", Icons.PARAMETER);
|
||||
map.put("package", Icons.PACKAGE);
|
||||
return map;
|
||||
}
|
||||
|
||||
private final AbstractCodeArea codeArea;
|
||||
private ScriptServices scriptServices;
|
||||
|
||||
public ScriptCompleteProvider(AbstractCodeArea codeArea) {
|
||||
this.codeArea = codeArea;
|
||||
}
|
||||
|
||||
private List<Completion> getCompletions() {
|
||||
try {
|
||||
String code = codeArea.getText();
|
||||
int caretPos = codeArea.getCaretPosition();
|
||||
// TODO: resolve error after reusing ScriptCompiler
|
||||
scriptServices = new ScriptServices();
|
||||
String scriptName = codeArea.getNode().getName();
|
||||
ScriptCompletionResult result = scriptServices.complete(scriptName, code, caretPos);
|
||||
int replacePos = getReplacePos(caretPos, result);
|
||||
if (!result.getReports().isEmpty()) {
|
||||
LOG.debug("Script completion reports: {}", result.getReports());
|
||||
}
|
||||
return convertCompletions(result.getCompletions(), code, replacePos);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Code completion failed", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Completion> convertCompletions(List<SourceCodeCompletionVariant> completions, String code, int replacePos) {
|
||||
if (completions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (LOG.isDebugEnabled()) {
|
||||
String cmplStr = completions.stream().map(SourceCodeCompletionVariant::toString).collect(Collectors.joining("\n"));
|
||||
LOG.debug("Completions:\n{}", cmplStr);
|
||||
}
|
||||
int count = completions.size();
|
||||
List<Completion> list = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
SourceCodeCompletionVariant c = completions.get(i);
|
||||
if (Objects.equals(c.getIcon(), "keyword")) {
|
||||
// too many, not very useful
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
cmpl.setSummary(c.getDisplayText() + " " + c.getTail());
|
||||
} else {
|
||||
cmpl.setSummary(c.getTail());
|
||||
}
|
||||
cmpl.setToolTip(c.getDisplayText());
|
||||
Icon icon = ICONS_MAP.get(c.getIcon());
|
||||
cmpl.setIcon(icon != null ? icon : Icons.FILE);
|
||||
list.add(cmpl);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private int getReplacePos(int caretPos, ScriptCompletionResult result) throws BadLocationException {
|
||||
int lineRaw = codeArea.getLineOfOffset(caretPos);
|
||||
int lineStart = codeArea.getLineStartOffset(lineRaw);
|
||||
int line = lineRaw + 1;
|
||||
int col = caretPos - lineStart + 1;
|
||||
|
||||
List<ScriptDiagnostic> reports = result.getReports();
|
||||
ScriptDiagnostic cmplReport = ListUtils.filterOnlyOne(reports, r -> {
|
||||
if (r.getSeverity() == ScriptDiagnostic.Severity.ERROR && r.getLocation() != null) {
|
||||
SourceCode.Position start = r.getLocation().getStart();
|
||||
return start.getLine() == line && r.getMessage().endsWith(AUTO_COMPLETE_INSERT_STR);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (cmplReport == null) {
|
||||
LOG.warn("Failed to find completion report in: {}", reports);
|
||||
return caretPos;
|
||||
}
|
||||
reports.remove(cmplReport);
|
||||
int reportCol = Objects.requireNonNull(cmplReport.getLocation()).getStart().getCol();
|
||||
return caretPos - (col - reportCol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlreadyEnteredText(JTextComponent comp) {
|
||||
try {
|
||||
int pos = codeArea.getCaretPosition();
|
||||
return codeArea.getText(0, pos);
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to get text before caret", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Completion> getCompletionsAt(JTextComponent comp, Point p) {
|
||||
return getCompletions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Completion> getCompletionsImpl(JTextComponent comp) {
|
||||
return getCompletions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParameterizedCompletion> getParameterizedCompletions(JTextComponent tc) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.text.JTextComponent;
|
||||
|
||||
import org.fife.ui.autocomplete.Completion;
|
||||
import org.fife.ui.autocomplete.CompletionProvider;
|
||||
|
||||
public class ScriptCompletionData implements Completion {
|
||||
|
||||
private final CompletionProvider provider;
|
||||
private final int relevance;
|
||||
|
||||
private String input;
|
||||
private String code;
|
||||
private int replacePos;
|
||||
private Icon icon;
|
||||
private String summary;
|
||||
private String toolTip;
|
||||
|
||||
public ScriptCompletionData(CompletionProvider provider, int relevance) {
|
||||
this.provider = provider;
|
||||
this.relevance = relevance;
|
||||
}
|
||||
|
||||
public void setData(String input, String code, int replacePos) {
|
||||
this.input = input;
|
||||
this.code = code;
|
||||
this.replacePos = replacePos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInputText() {
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionProvider getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlreadyEntered(JTextComponent comp) {
|
||||
return provider.getAlreadyEnteredText(comp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRelevance() {
|
||||
return relevance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReplacementText() {
|
||||
return code.substring(0, replacePos) + input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(Icon icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTipText() {
|
||||
return toolTip;
|
||||
}
|
||||
|
||||
public void setToolTip(String toolTip) {
|
||||
this.toolTip = toolTip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Completion other) {
|
||||
return Integer.compare(relevance, other.getRelevance());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import javax.swing.JList;
|
||||
|
||||
import org.fife.ui.autocomplete.Completion;
|
||||
import org.fife.ui.autocomplete.CompletionCellRenderer;
|
||||
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
|
||||
import static jadx.gui.utils.UiUtils.escapeHtml;
|
||||
import static jadx.gui.utils.UiUtils.fadeHtml;
|
||||
import static jadx.gui.utils.UiUtils.wrapHtml;
|
||||
|
||||
public class ScriptCompletionRenderer extends CompletionCellRenderer {
|
||||
|
||||
public ScriptCompletionRenderer(JadxSettings settings) {
|
||||
setDisplayFont(settings.getCodeFont());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareForOtherCompletion(JList list, Completion c, int index, boolean selected, boolean hasFocus) {
|
||||
ScriptCompletionData cmpl = (ScriptCompletionData) c;
|
||||
setText(wrapHtml(escapeHtml(cmpl.getInputText()) + " "
|
||||
+ fadeHtml(escapeHtml(cmpl.getSummary()))));
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
package jadx.gui.plugins.script;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
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 kotlin.script.experimental.api.ScriptDiagnostic;
|
||||
import kotlin.script.experimental.api.ScriptDiagnostic.Severity;
|
||||
|
||||
import jadx.gui.logs.LogOptions;
|
||||
import jadx.gui.settings.JadxSettings;
|
||||
import jadx.gui.settings.LineNumbersMode;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.action.ActionModel;
|
||||
import jadx.gui.ui.action.JadxGuiAction;
|
||||
import jadx.gui.ui.codearea.AbstractCodeArea;
|
||||
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
|
||||
import jadx.gui.ui.codearea.SearchBar;
|
||||
import jadx.gui.ui.tab.TabbedPane;
|
||||
import jadx.gui.utils.Icons;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.ui.NodeLabel;
|
||||
import jadx.plugins.script.ide.ScriptAnalyzeResult;
|
||||
import jadx.plugins.script.ide.ScriptServices;
|
||||
|
||||
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
|
||||
|
||||
public class ScriptContentPanel extends AbstractCodeContentPanel {
|
||||
private static final long serialVersionUID = 6575696321112417513L;
|
||||
|
||||
private final ScriptCodeArea scriptArea;
|
||||
private final SearchBar searchBar;
|
||||
private final RTextScrollPane codeScrollPane;
|
||||
private final JPanel actionPanel;
|
||||
private final JLabel resultLabel;
|
||||
private final ScriptErrorService errorService;
|
||||
private final Logger scriptLog;
|
||||
|
||||
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);
|
||||
scriptLog = LoggerFactory.getLogger(JADX_SCRIPT_LOG_PREFIX + scriptNode.getName());
|
||||
|
||||
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() {
|
||||
JadxGuiAction runAction = new JadxGuiAction(ActionModel.SCRIPT_RUN, this::runScript);
|
||||
JadxGuiAction saveAction = new JadxGuiAction(ActionModel.SCRIPT_SAVE, scriptArea::save);
|
||||
|
||||
runAction.setShortcutComponent(scriptArea);
|
||||
saveAction.setShortcutComponent(scriptArea);
|
||||
|
||||
tabbedPane.getMainWindow().getShortcutsController().bindImmediate(runAction);
|
||||
tabbedPane.getMainWindow().getShortcutsController().bindImmediate(saveAction);
|
||||
|
||||
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());
|
||||
JButton scriptLog = new JButton(NLS.str("script.log"), Icons.FORMAT);
|
||||
scriptLog.addActionListener(ev -> showScriptLog());
|
||||
|
||||
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());
|
||||
panel.add(scriptLog);
|
||||
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().reloadPasses();
|
||||
} catch (Exception e) {
|
||||
scriptLog.error("Passes reload failed", e);
|
||||
}
|
||||
}, taskStatus -> {
|
||||
mainWindow.passesReloaded();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean checkScript() {
|
||||
try {
|
||||
resetResultLabel();
|
||||
String code = scriptArea.getText();
|
||||
String fileName = scriptArea.getNode().getName();
|
||||
|
||||
ScriptServices scriptServices = new ScriptServices();
|
||||
ScriptAnalyzeResult result = scriptServices.analyze(fileName, code);
|
||||
boolean success = result.getSuccess();
|
||||
List<ScriptDiagnostic> issues = result.getIssues();
|
||||
for (ScriptDiagnostic issue : issues) {
|
||||
Severity severity = issue.getSeverity();
|
||||
if (severity == Severity.ERROR || severity == Severity.FATAL) {
|
||||
scriptLog.error("{}", issue.render(false, true, true, true));
|
||||
success = false;
|
||||
} else if (severity == Severity.WARNING) {
|
||||
scriptLog.warn("Compile issue: {}", issue);
|
||||
}
|
||||
}
|
||||
List<JadxLintError> lintErrs = Collections.emptyList();
|
||||
if (success) {
|
||||
lintErrs = getLintIssues(code);
|
||||
}
|
||||
|
||||
errorService.clearErrors();
|
||||
errorService.addCompilerIssues(issues);
|
||||
errorService.addLintErrors(lintErrs);
|
||||
if (!success) {
|
||||
resultLabel.setText("Compile issues: " + issues.size());
|
||||
showScriptLog();
|
||||
} else if (!lintErrs.isEmpty()) {
|
||||
resultLabel.setText("Lint issues: " + lintErrs.size());
|
||||
} else {
|
||||
resultLabel.setText("OK");
|
||||
}
|
||||
errorService.apply();
|
||||
return success;
|
||||
} catch (Throwable e) {
|
||||
scriptLog.error("Failed to check code", e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private List<JadxLintError> getLintIssues(String code) {
|
||||
try {
|
||||
List<JadxLintError> lintErrs = KtLintUtils.INSTANCE.lint(code);
|
||||
for (JadxLintError error : lintErrs) {
|
||||
scriptLog.warn("Lint issue: {} ({}:{})(ruleId={})",
|
||||
error.getDetail(), error.getLine(), error.getCol(), error.getRuleId());
|
||||
}
|
||||
return lintErrs;
|
||||
} catch (Throwable e) { // can throw initialization error
|
||||
scriptLog.warn("KtLint failed", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private void reformatCode() {
|
||||
resetResultLabel();
|
||||
try {
|
||||
String code = scriptArea.getText();
|
||||
String formattedCode = KtLintUtils.INSTANCE.format(code);
|
||||
if (!code.equals(formattedCode)) {
|
||||
scriptArea.updateCode(formattedCode);
|
||||
resultLabel.setText("Code updated");
|
||||
errorService.clearErrors();
|
||||
}
|
||||
} catch (Throwable e) { // can throw initialization error
|
||||
scriptLog.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.getCodeFont());
|
||||
scriptArea.loadSettings();
|
||||
}
|
||||
|
||||
private void showScriptLog() {
|
||||
getMainWindow().showLogViewer(LogOptions.forScript(getNode().getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractCodeArea getCodeArea() {
|
||||
return scriptArea;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getChildrenComponent() {
|
||||
return getCodeArea();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSettings() {
|
||||
applySettings();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
scriptArea.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
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 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();
|
||||
scriptArea.requestFocus();
|
||||
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) {
|
||||
try {
|
||||
offset = scriptArea.getLineStartOffset(notice.getLine());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to jump to first error", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
scriptArea.scrollToPos(offset);
|
||||
}
|
||||
|
||||
public void addCompilerIssues(List<ScriptDiagnostic> issues) {
|
||||
for (ScriptDiagnostic issue : issues) {
|
||||
if (issue.getSeverity() == ScriptDiagnostic.Severity.DEBUG) {
|
||||
continue;
|
||||
}
|
||||
DefaultParserNotice notice;
|
||||
SourceCode.Location loc = issue.getLocation();
|
||||
if (loc == null) {
|
||||
notice = new DefaultParserNotice(this, issue.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, issue.getMessage(), line, offset - 1, len);
|
||||
notice.setLevel(convertLevel(issue.getSeverity()));
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to convert script issue", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
addNotice(notice);
|
||||
}
|
||||
}
|
||||
|
||||
private static ParserNotice.Level convertLevel(ScriptDiagnostic.Severity severity) {
|
||||
switch (severity) {
|
||||
case FATAL:
|
||||
case ERROR:
|
||||
return ParserNotice.Level.ERROR;
|
||||
case WARNING:
|
||||
return ParserNotice.Level.WARNING;
|
||||
case INFO:
|
||||
case DEBUG:
|
||||
return ParserNotice.Level.INFO;
|
||||
}
|
||||
return ParserNotice.Level.ERROR;
|
||||
}
|
||||
|
||||
public void addLintErrors(List<JadxLintError> errors) {
|
||||
for (JadxLintError 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);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ public class JadxProject {
|
||||
private static final int SEARCH_HISTORY_LIMIT = 30;
|
||||
|
||||
private final transient MainWindow mainWindow;
|
||||
private final transient TabStateViewAdapter tabStateViewAdapter = new TabStateViewAdapter();
|
||||
|
||||
private transient String name = "New Project";
|
||||
private transient @Nullable Path projectPath;
|
||||
@@ -155,7 +156,7 @@ public class JadxProject {
|
||||
|
||||
public void saveOpenTabs(List<EditorViewState> tabs) {
|
||||
List<TabViewState> tabStateList = tabs.stream()
|
||||
.map(TabStateViewAdapter::build)
|
||||
.map(tabStateViewAdapter::build)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
if (data.setOpenTabs(tabStateList)) {
|
||||
@@ -164,8 +165,9 @@ public class JadxProject {
|
||||
}
|
||||
|
||||
public List<EditorViewState> getOpenTabs(MainWindow mw) {
|
||||
tabStateViewAdapter.setCustomAdapters(mw.getWrapper().getGuiPluginsContext().getTabStatePersistAdapters());
|
||||
return data.getOpenTabs().stream()
|
||||
.map(s -> TabStateViewAdapter.load(mw, s))
|
||||
.map(s -> tabStateViewAdapter.load(mw, s))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package jadx.gui.settings;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.gui.plugins.mappings.JInputMapping;
|
||||
import jadx.gui.settings.data.ITabStatePersist;
|
||||
import jadx.gui.settings.data.TabViewState;
|
||||
import jadx.gui.settings.data.ViewPoint;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JInputScript;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
import jadx.gui.treemodel.JSubResource;
|
||||
@@ -20,8 +24,9 @@ import jadx.gui.utils.UiUtils;
|
||||
public class TabStateViewAdapter {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TabStateViewAdapter.class);
|
||||
|
||||
@Nullable
|
||||
public static TabViewState build(EditorViewState viewState) {
|
||||
private final Map<String, ITabStatePersist> customAdaptersMap = new HashMap<>();
|
||||
|
||||
public @Nullable TabViewState build(EditorViewState viewState) {
|
||||
TabViewState tvs = new TabViewState();
|
||||
tvs.setSubPath(viewState.getSubPath());
|
||||
if (!saveJNode(tvs, viewState.getNode())) {
|
||||
@@ -40,8 +45,7 @@ public class TabStateViewAdapter {
|
||||
return tvs;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EditorViewState load(MainWindow mw, TabViewState tvs) {
|
||||
public @Nullable EditorViewState load(MainWindow mw, TabViewState tvs) {
|
||||
try {
|
||||
JNode node = loadJNode(mw, tvs);
|
||||
if (node == null) {
|
||||
@@ -63,8 +67,15 @@ public class TabStateViewAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
public void setCustomAdapters(List<ITabStatePersist> customAdapters) {
|
||||
customAdaptersMap.clear();
|
||||
for (ITabStatePersist customAdapter : customAdapters) {
|
||||
customAdaptersMap.put(customAdapter.getNodeClass().getName(), customAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JNode loadJNode(MainWindow mw, TabViewState tvs) {
|
||||
private JNode loadJNode(MainWindow mw, TabViewState tvs) {
|
||||
switch (tvs.getType()) {
|
||||
case "class":
|
||||
JavaClass javaClass = mw.getWrapper().searchJavaClassByRawName(tvs.getTabPath());
|
||||
@@ -85,18 +96,21 @@ public class TabStateViewAdapter {
|
||||
}
|
||||
return null;
|
||||
|
||||
case "script":
|
||||
return mw.getTreeRoot()
|
||||
.followStaticPath("JInputs", "JInputScripts")
|
||||
.searchNode(node -> node instanceof JInputScript && node.getName().equals(tvs.getTabPath()));
|
||||
|
||||
case "mapping":
|
||||
return mw.getTreeRoot().followStaticPath("JInputs").searchNode(node -> node instanceof JInputMapping);
|
||||
}
|
||||
ITabStatePersist statePersist = customAdaptersMap.get(tvs.getType());
|
||||
if (statePersist != null) {
|
||||
try {
|
||||
return statePersist.load(tvs.getTabPath());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to restore tab for custom node adapter: {}", tvs.getType(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean saveJNode(TabViewState tvs, JNode node) {
|
||||
private boolean saveJNode(TabViewState tvs, JNode node) {
|
||||
if (node instanceof JClass) {
|
||||
tvs.setType("class");
|
||||
tvs.setTabPath(((JClass) node).getCls().getRawName());
|
||||
@@ -113,15 +127,22 @@ public class TabStateViewAdapter {
|
||||
tvs.setTabPath(node.getName());
|
||||
return true;
|
||||
}
|
||||
if (node instanceof JInputScript) {
|
||||
tvs.setType("script");
|
||||
tvs.setTabPath(node.getName());
|
||||
return true;
|
||||
}
|
||||
if (node instanceof JInputMapping) {
|
||||
tvs.setType("mapping");
|
||||
return true;
|
||||
}
|
||||
|
||||
String typeName = node.getClass().getName();
|
||||
ITabStatePersist statePersist = customAdaptersMap.get(typeName);
|
||||
if (statePersist != null) {
|
||||
try {
|
||||
tvs.setTabPath(statePersist.save(node));
|
||||
tvs.setType(statePersist.getNodeClass().getName());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to save state for custom node: {}", typeName, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package jadx.gui.settings.data;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
|
||||
/**
|
||||
* Adapter interface to allow save/load state of opened tabs
|
||||
*/
|
||||
public interface ITabStatePersist {
|
||||
|
||||
Class<? extends JNode> getNodeClass();
|
||||
|
||||
String save(JNode node);
|
||||
|
||||
@Nullable
|
||||
JNode load(String stateStr);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JPopupMenu;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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.panel.ContentPanel;
|
||||
import jadx.gui.ui.tab.TabbedPane;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.ui.SimpleMenuItem;
|
||||
|
||||
public class JInputScript extends JEditableNode {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JInputScript.class);
|
||||
|
||||
private static final ImageIcon SCRIPT_ICON = UiUtils.openSvgIcon("nodes/kotlin_script");
|
||||
|
||||
private final Path scriptPath;
|
||||
private final String name;
|
||||
|
||||
public JInputScript(Path scriptPath) {
|
||||
this.scriptPath = scriptPath;
|
||||
this.name = scriptPath.getFileName().toString().replace(".jadx.kts", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasContent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
|
||||
return new ScriptContentPanel(tabbedPane, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ICodeInfo getCodeInfo() {
|
||||
try {
|
||||
return new SimpleCodeInfo(FileUtils.readFile(scriptPath));
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to read script file: " + scriptPath.toAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(String newContent) {
|
||||
try {
|
||||
FileUtils.writeFile(scriptPath, newContent);
|
||||
LOG.debug("Script saved: {}", scriptPath.toAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to write script file: " + scriptPath.toAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.add_scripts"), mainWindow::addFiles));
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.new_script"), mainWindow::addNewScript));
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.remove"), () -> mainWindow.removeInput(scriptPath)));
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.rename"), () -> mainWindow.renameInput(scriptPath)));
|
||||
return menu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSyntaxName() {
|
||||
return SyntaxConstants.SYNTAX_STYLE_KOTLIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JClass getJParent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return SCRIPT_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String makeString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTooltip() {
|
||||
return scriptPath.normalize().toAbsolutePath().toString();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JPopupMenu;
|
||||
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.ui.SimpleMenuItem;
|
||||
|
||||
public class JInputScripts extends JNode {
|
||||
private static final ImageIcon INPUT_SCRIPTS_ICON = UiUtils.openSvgIcon("nodes/scriptsModel");
|
||||
|
||||
public JInputScripts(List<Path> scripts) {
|
||||
for (Path script : scripts) {
|
||||
add(new JInputScript(script));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.add_scripts"), mainWindow::addFiles));
|
||||
menu.add(new SimpleMenuItem(NLS.str("popup.new_script"), mainWindow::addNewScript));
|
||||
return menu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JClass getJParent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return INPUT_SCRIPTS_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID() {
|
||||
return "JInputScripts";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String makeString() {
|
||||
return NLS.str("tree.input_scripts");
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,29 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.plugins.TreeInputsHelper;
|
||||
|
||||
public class JInputs extends JNode {
|
||||
private static final ImageIcon INPUTS_ICON = UiUtils.openSvgIcon("nodes/projectStructure");
|
||||
|
||||
public JInputs(JadxWrapper wrapper) {
|
||||
JadxProject project = wrapper.getProject();
|
||||
public JInputs(MainWindow mainWindow) {
|
||||
JadxProject project = mainWindow.getProject();
|
||||
List<Path> inputs = project.getFilePaths();
|
||||
List<Path> files = FileUtils.expandDirs(inputs);
|
||||
List<Path> scripts = new ArrayList<>();
|
||||
Iterator<Path> it = files.iterator();
|
||||
while (it.hasNext()) {
|
||||
Path file = it.next();
|
||||
if (file.getFileName().toString().endsWith(".jadx.kts")) {
|
||||
scripts.add(file);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
add(new JInputFiles(files));
|
||||
add(new JInputScripts(scripts));
|
||||
TreeInputsHelper inputsHelper = new TreeInputsHelper(mainWindow);
|
||||
inputsHelper.processInputs(files);
|
||||
add(new JInputFiles(inputsHelper.getSimpleFiles()));
|
||||
inputsHelper.getCustomNodes().forEach(this::add);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,6 +19,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.settings.JadxProject;
|
||||
import jadx.gui.treemodel.JResource.JResType;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
@@ -28,18 +29,20 @@ public class JRoot extends JNode {
|
||||
private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/rootPackageFolder");
|
||||
|
||||
private final transient JadxWrapper wrapper;
|
||||
private final transient MainWindow mainWindow;
|
||||
|
||||
private transient boolean flatPackages = false;
|
||||
|
||||
private final List<JNode> customNodes = new ArrayList<>();
|
||||
private final transient List<JNode> customNodes = new ArrayList<>();
|
||||
|
||||
public JRoot(JadxWrapper wrapper) {
|
||||
this.wrapper = wrapper;
|
||||
public JRoot(MainWindow mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.wrapper = mainWindow.getWrapper();
|
||||
}
|
||||
|
||||
public final void update() {
|
||||
removeAllChildren();
|
||||
add(new JInputs(wrapper));
|
||||
add(new JInputs(mainWindow));
|
||||
add(new JSources(this, wrapper));
|
||||
|
||||
List<ResourceFile> resources = wrapper.getResources();
|
||||
|
||||
@@ -828,7 +828,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
public void initTree() {
|
||||
treeRoot = new JRoot(wrapper);
|
||||
treeRoot = new JRoot(this);
|
||||
treeRoot.setFlatPackages(isFlattenPackage);
|
||||
treeModel.setRoot(treeRoot);
|
||||
addTreeCustomNodes();
|
||||
|
||||
@@ -3,13 +3,13 @@ package jadx.gui.utils;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
public class IconsCache {
|
||||
|
||||
private static final Map<String, FlatSVGIcon> SVG_ICONS = new ConcurrentHashMap<>();
|
||||
private static final Map<String, ImageIcon> SVG_ICONS = new ConcurrentHashMap<>();
|
||||
|
||||
public static FlatSVGIcon getSVGIcon(String name) {
|
||||
public static ImageIcon getSVGIcon(String name) {
|
||||
return SVG_ICONS.computeIfAbsent(name, UiUtils::openSvgIcon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class UiUtils {
|
||||
private UiUtils() {
|
||||
}
|
||||
|
||||
public static FlatSVGIcon openSvgIcon(String name) {
|
||||
public static ImageIcon openSvgIcon(String name) {
|
||||
String iconPath = "icons/" + name + ".svg";
|
||||
FlatSVGIcon icon = new FlatSVGIcon(iconPath);
|
||||
boolean found;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package jadx.gui.utils.plugins;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.plugins.context.ITreeInputCategory;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
|
||||
public class TreeInputsHelper {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TreeInputsHelper.class);
|
||||
|
||||
private final List<CategoryData> categoryData;
|
||||
private List<Path> simpleFiles;
|
||||
|
||||
public TreeInputsHelper(MainWindow mainWindow) {
|
||||
categoryData = mainWindow.getWrapper().getGuiPluginsContext()
|
||||
.getTreeInputCategories()
|
||||
.stream()
|
||||
.map(CategoryData::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void processInputs(List<Path> files) {
|
||||
simpleFiles = new ArrayList<>(files.size());
|
||||
for (Path file : files) {
|
||||
boolean added = false;
|
||||
for (CategoryData data : categoryData) {
|
||||
if (data.filesFilter(file)) {
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
simpleFiles.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<JNode> getCustomNodes() {
|
||||
return categoryData.stream()
|
||||
.filter(CategoryData::notEmpty)
|
||||
.map(CategoryData::buildInputNode)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<Path> getSimpleFiles() {
|
||||
return simpleFiles;
|
||||
}
|
||||
|
||||
private static final class CategoryData {
|
||||
private final ITreeInputCategory provider;
|
||||
private final List<Path> collectedFiles = new ArrayList<>();
|
||||
|
||||
private CategoryData(ITreeInputCategory provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public boolean filesFilter(Path file) {
|
||||
try {
|
||||
if (provider.filesFilter(file)) {
|
||||
collectedFiles.add(file);
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to filter input files", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public @Nullable JNode buildInputNode() {
|
||||
try {
|
||||
return provider.buildInputNode(collectedFiles);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to build custom input node", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean notEmpty() {
|
||||
return !collectedFiles.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -116,12 +117,25 @@ public class TestI18n {
|
||||
fail("I18n file: " + path.getFileName() + " and " + DEFAULT_LANG_FILE + " differ in line " + line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary solution to allow use I18N strings in plugins until proper API implemented
|
||||
*/
|
||||
private static final List<String> EXCLUDED_KEYS = Arrays.asList(
|
||||
// keys from `jadx-script-kotlin`
|
||||
"tree.input_scripts",
|
||||
"popup.new_script",
|
||||
"popup.add_scripts",
|
||||
"script.log",
|
||||
"script.format",
|
||||
"script.check");
|
||||
|
||||
@Test
|
||||
public void keyIsUsed() throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (Reader reader = Files.newBufferedReader(i18nPath.resolve(DEFAULT_LANG_FILE))) {
|
||||
properties.load(reader);
|
||||
}
|
||||
EXCLUDED_KEYS.forEach(properties.keySet()::remove);
|
||||
Set<String> keys = new HashSet<>();
|
||||
for (Object key : properties.keySet()) {
|
||||
keys.add("\"" + key + '"');
|
||||
|
||||
Reference in New Issue
Block a user