提交 7216635d 编写于 作者: S Skylot

gui: run text search in background thread (#269)

上级 98ef7c39
......@@ -14,6 +14,11 @@ dependencies {
compile 'com.google.code.gson:gson:2.8.2'
compile files('libs/jfontchooser-1.0.5.jar')
compile 'hu.kazocsaba:image-viewer:1.2.3'
compile 'org.apache.commons:commons-lang3:3.7'
compile 'io.reactivex.rxjava2:rxjava:2.1.13'
compile "com.github.akarnokd:rxjava2-swing:0.2.12"
}
applicationDistribution.with {
......@@ -35,7 +40,7 @@ jar {
}
startScripts {
defaultJvmOpts = [ '-Xms128M', '-Xmx4g' ]
defaultJvmOpts = ['-Xms128M', '-Xmx4g']
doLast {
def str = windowsScript.text
str = str.replaceAll('java.exe', 'javaw.exe')
......
......@@ -12,12 +12,12 @@ public class CodeNode extends JNode {
private final transient JNode jNode;
private final transient JClass jParent;
private final transient StringRef line;
private final int lineNum;
private final transient int lineNum;
public CodeNode(JNode jNode, int lineNum, StringRef line) {
public CodeNode(JNode jNode, int lineNum, StringRef lineStr) {
this.jNode = jNode;
this.jParent = this.jNode.getJParent();
this.line = line;
this.line = lineStr;
this.lineNum = lineNum;
}
......
......@@ -215,8 +215,12 @@ public abstract class CommonSearchDialog extends JDialog {
}
protected void updateProgressLabel() {
String statusText = String.format(NLS.str("search_dialog.info_label"), resultsModel.getDisplayedResultsStart(),
resultsModel.getDisplayedResultsEnd(), resultsModel.getResultCount());
String statusText = String.format(
NLS.str("search_dialog.info_label"),
resultsModel.getDisplayedResultsStart(),
resultsModel.getDisplayedResultsEnd(),
resultsModel.getResultCount()
);
resultsInfoLabel.setText(statusText);
}
......@@ -283,16 +287,15 @@ public abstract class CommonSearchDialog extends JDialog {
protected void addAll(Collection<? extends JNode> nodes) {
rows.ensureCapacity(rows.size() + nodes.size());
for (JNode node : nodes) {
add(node);
}
}
private void add(JNode node) {
if (node.hasDescString()) {
addDescColumn = true;
rows.addAll(nodes);
if (!addDescColumn) {
for (JNode row : rows) {
if (row.hasDescString()) {
addDescColumn = true;
break;
}
}
}
rows.add(node);
}
public void clear() {
......@@ -339,7 +342,10 @@ public abstract class CommonSearchDialog extends JDialog {
@Override
public int getRowCount() {
return rows.size() - start;
if (rows.isEmpty()) {
return 0;
}
return getDisplayedResultsEnd() - getDisplayedResultsStart();
}
@Override
......
......@@ -24,7 +24,6 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Timer;
import java.util.TimerTask;
......@@ -43,7 +42,6 @@ import jadx.gui.treemodel.JLoadableNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.treemodel.JRoot;
import jadx.gui.ui.SearchDialog.SearchOptions;
import jadx.gui.update.JadxUpdate;
import jadx.gui.update.JadxUpdate.IUpdateCallback;
import jadx.gui.update.data.Release;
......@@ -385,7 +383,7 @@ public class MainWindow extends JFrame {
Action textSearchAction = new AbstractAction(NLS.str("menu.text_search"), ICON_SEARCH) {
@Override
public void actionPerformed(ActionEvent e) {
new SearchDialog(MainWindow.this, EnumSet.of(SearchOptions.CODE)).setVisible(true);
new SearchDialog(MainWindow.this, true).setVisible(true);
}
};
textSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.text_search"));
......@@ -395,7 +393,7 @@ public class MainWindow extends JFrame {
Action clsSearchAction = new AbstractAction(NLS.str("menu.class_search"), ICON_FIND) {
@Override
public void actionPerformed(ActionEvent e) {
new SearchDialog(MainWindow.this, EnumSet.of(SearchOptions.CLASS)).setVisible(true);
new SearchDialog(MainWindow.this, false).setVisible(true);
}
};
clsSearchAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.class_search"));
......
......@@ -4,35 +4,60 @@ import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import hu.akarnokd.rxjava2.swing.SwingSchedulers;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Emitter;
import io.reactivex.Flowable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.treemodel.JNode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.search.TextSearchIndex;
public class SearchDialog extends CommonSearchDialog {
private static final Logger LOG = LoggerFactory.getLogger(SearchDialog.class);
private static final long serialVersionUID = -5105405456969134105L;
private final boolean textSearch;
enum SearchOptions {
public enum SearchOptions {
CLASS,
METHOD,
FIELD,
CODE
CODE,
IGNORE_CASE
}
private Set<SearchOptions> options;
private transient Set<SearchOptions> options;
private transient JTextField searchField;
private JTextField searchField;
private JCheckBox caseChBox;
private transient Disposable searchDisposable;
private transient SearchEventEmitter searchEmitter;
public SearchDialog(MainWindow mainWindow, Set<SearchOptions> options) {
public SearchDialog(MainWindow mainWindow, boolean textSearch) {
super(mainWindow);
this.options = options;
this.textSearch = textSearch;
if (textSearch) {
Set<SearchOptions> lastSearchOptions = cache.getLastSearchOptions();
if (!lastSearchOptions.isEmpty()) {
this.options = lastSearchOptions;
} else {
this.options = EnumSet.of(SearchOptions.CODE, SearchOptions.IGNORE_CASE);
}
} else {
this.options = EnumSet.of(SearchOptions.CLASS);
}
initUI();
registerInitOnOpen();
......@@ -46,83 +71,19 @@ public class SearchDialog extends CommonSearchDialog {
if (lastSearch != null) {
searchField.setText(lastSearch);
searchField.selectAll();
searchEmitter.emitSearch();
}
searchField.requestFocus();
}
@Override
protected synchronized void performSearch() {
resultsModel.clear();
String text = searchField.getText();
if (text == null || text.isEmpty() || options.isEmpty()) {
return;
}
try {
cache.setLastSearch(text);
TextSearchIndex index = cache.getTextIndex();
if (index == null) {
return;
}
boolean caseInsensitive = caseChBox.isSelected();
if (options.contains(SearchOptions.CLASS)) {
resultsModel.addAll(index.searchClsName(text, caseInsensitive));
}
if (options.contains(SearchOptions.METHOD)) {
resultsModel.addAll(index.searchMthName(text, caseInsensitive));
}
if (options.contains(SearchOptions.FIELD)) {
resultsModel.addAll(index.searchFldName(text, caseInsensitive));
}
if (options.contains(SearchOptions.CODE)) {
resultsModel.addAll(index.searchCode(text, caseInsensitive));
}
highlightText = text;
highlightTextCaseInsensitive = caseInsensitive;
} finally {
super.performSearch();
}
}
private class SearchFieldListener implements DocumentListener, ActionListener {
private Timer timer;
private synchronized void change() {
if (timer != null) {
timer.restart();
} else {
timer = new Timer(400, this);
timer.setRepeats(false);
timer.start();
}
}
@Override
public void actionPerformed(ActionEvent e) {
performSearch();
}
public void changedUpdate(DocumentEvent e) {
change();
}
public void removeUpdate(DocumentEvent e) {
change();
}
public void insertUpdate(DocumentEvent e) {
change();
}
}
private void initUI() {
JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name"));
searchField = new JTextField();
searchField.setAlignmentX(LEFT_ALIGNMENT);
searchField.getDocument().addDocumentListener(new SearchFieldListener());
new TextStandardActions(searchField);
searchFieldSubscribe();
caseChBox = new JCheckBox(NLS.str("search_dialog.ignorecase"));
caseChBox.addItemListener(e -> performSearch());
JCheckBox caseChBox = makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), SearchOptions.IGNORE_CASE);
JCheckBox clsChBox = makeOptionsCheckBox(NLS.str("search_dialog.class"), SearchOptions.CLASS);
JCheckBox mthChBox = makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD);
......@@ -184,6 +145,122 @@ public class SearchDialog extends CommonSearchDialog {
setModalityType(ModalityType.MODELESS);
}
private class SearchEventEmitter {
private final Flowable<String> flowable;
private Emitter<String> emitter;
public SearchEventEmitter() {
flowable = Flowable.create(this::saveEmitter, BackpressureStrategy.LATEST);
}
public Flowable<String> getFlowable() {
return flowable;
}
private void saveEmitter(Emitter<String> emitter) {
this.emitter = emitter;
}
public synchronized void emitSearch() {
this.emitter.onNext(searchField.getText());
}
}
private void searchFieldSubscribe() {
searchEmitter = new SearchEventEmitter();
Flowable<String> textChanges = onTextFieldChanges(searchField);
Flowable<String> searchEvents = Flowable.merge(textChanges, searchEmitter.getFlowable());
searchDisposable = searchEvents
.filter(text -> text.length() > 0)
.subscribeOn(Schedulers.single())
.doOnNext(r -> LOG.debug("search event: {}", r))
.switchMap(text -> prepareSearch(text)
.subscribeOn(Schedulers.single())
.toList()
.toFlowable(), 1)
.observeOn(SwingSchedulers.edt())
.subscribe(this::processSearchResults);
}
private Flowable<JNode> prepareSearch(String text) {
if (text == null || text.isEmpty() || options.isEmpty()) {
return Flowable.empty();
}
TextSearchIndex index = cache.getTextIndex();
if (index == null) {
return Flowable.empty();
}
return index.buildSearch(text, options);
}
private void processSearchResults(java.util.List<JNode> results) {
LOG.debug("search result size: {}", results.size());
String text = searchField.getText();
highlightText = text;
highlightTextCaseInsensitive = options.contains(SearchOptions.IGNORE_CASE);
cache.setLastSearch(text);
if (textSearch) {
cache.setLastSearchOptions(options);
}
resultsModel.clear();
resultsModel.addAll(results);
super.performSearch();
}
private static Flowable<String> onTextFieldChanges(final JTextField textField) {
return Flowable.<String>create(emitter -> {
DocumentListener listener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
change();
}
@Override
public void removeUpdate(DocumentEvent e) {
change();
}
@Override
public void changedUpdate(DocumentEvent e) {
change();
}
public void change() {
emitter.onNext(textField.getText());
}
};
textField.getDocument().addDocumentListener(listener);
emitter.setDisposable(new Disposable() {
private boolean disposed = false;
@Override
public void dispose() {
textField.getDocument().removeDocumentListener(listener);
disposed = true;
}
@Override
public boolean isDisposed() {
return disposed;
}
});
}, BackpressureStrategy.LATEST)
.debounce(300, TimeUnit.MILLISECONDS)
.distinctUntilChanged();
}
@Override
public void dispose() {
if (searchDisposable != null && !searchDisposable.isDisposed()) {
searchDisposable.dispose();
}
super.dispose();
}
private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) {
final JCheckBox chBox = new JCheckBox(name);
chBox.setAlignmentX(LEFT_ALIGNMENT);
......@@ -194,7 +271,7 @@ public class SearchDialog extends CommonSearchDialog {
} else {
options.remove(opt);
}
performSearch();
searchEmitter.emitSearch();
});
return chBox;
}
......
package jadx.gui.utils;
import java.util.EnumSet;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.gui.jobs.DecompileJob;
import jadx.gui.jobs.IndexJob;
import jadx.gui.ui.SearchDialog;
import jadx.gui.utils.search.TextSearchIndex;
public class CacheObject {
......@@ -14,13 +18,21 @@ public class CacheObject {
private TextSearchIndex textIndex;
private CodeUsageInfo usageInfo;
private String lastSearch;
private JNodeCache jNodeCache = new JNodeCache();
private JNodeCache jNodeCache;
private Set<SearchDialog.SearchOptions> lastSearchOptions;
public CacheObject() {
reset();
}
public void reset() {
decompileJob = null;
indexJob = null;
textIndex = null;
lastSearch = null;
jNodeCache = new JNodeCache();
usageInfo = null;
lastSearchOptions = EnumSet.noneOf(SearchDialog.SearchOptions.class);
}
public DecompileJob getDecompileJob() {
......@@ -69,4 +81,12 @@ public class CacheObject {
public JNodeCache getNodeCache() {
return jNodeCache;
}
public void setLastSearchOptions(Set<SearchDialog.SearchOptions> lastSearchOptions) {
this.lastSearchOptions = lastSearchOptions;
}
public Set<SearchDialog.SearchOptions> getLastSearchOptions() {
return lastSearchOptions;
}
}
package jadx.gui.utils.search;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.utils.Utils;
public class CodeIndex<T> implements SearchIndex<T> {
private static final Logger LOG = LoggerFactory.getLogger(CodeIndex.class);
private final List<StringRef> keys = new ArrayList<>();
private final List<T> values = new ArrayList<>();
......@@ -28,23 +36,27 @@ public class CodeIndex<T> implements SearchIndex<T> {
return true;
}
private boolean isMatched(StringRef key, String str, boolean caseInsensitive) {
return key.indexOf(str, caseInsensitive) != -1;
}
@Override
public List<T> getValuesForKeysContaining(String str, boolean caseInsensitive) {
int size = size();
if (size == 0) {
return Collections.emptyList();
}
if (caseInsensitive) {
str = str.toLowerCase();
}
List<T> results = new ArrayList<>();
for (int i = 0; i < size; i++) {
StringRef key = keys.get(i);
if (key.indexOf(str, caseInsensitive) != -1) {
results.add(values.get(i));
public Flowable<T> search(final String searchStr, final boolean caseInsensitive) {
return Flowable.create(emitter -> {
int size = size();
LOG.debug("Code search started: {} ...", searchStr);
for (int i = 0; i < size; i++) {
if (isMatched(keys.get(i), searchStr, caseInsensitive)) {
emitter.onNext(values.get(i));
}
if (emitter.isCancelled()) {
LOG.debug("Code search canceled: {}", searchStr);
return;
}
}
}
return results;
LOG.debug("Code search complete: {}, memory usage: {}", searchStr, Utils.memoryInfo());
emitter.onComplete();
}, BackpressureStrategy.LATEST);
}
@Override
......
package jadx.gui.utils.search;
import java.util.List;
import io.reactivex.Flowable;
public interface SearchIndex<V> {
......@@ -10,7 +10,7 @@ public interface SearchIndex<V> {
boolean isStringRefSupported();
List<V> getValuesForKeysContaining(String str, boolean caseInsensitive);
Flowable<V> search(String searchStr, boolean caseInsensitive);
int size();
}
package jadx.gui.utils.search;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import org.apache.commons.lang3.StringUtils;
public class SimpleIndex<T> implements SearchIndex<T> {
private final List<String> keys = new ArrayList<>();
......@@ -25,26 +28,28 @@ public class SimpleIndex<T> implements SearchIndex<T> {
return false;
}
@Override
public List<T> getValuesForKeysContaining(String str, boolean caseInsensitive) {
int size = size();
if (size == 0) {
return Collections.emptyList();
}
private boolean isMatched(String str, String searchStr, boolean caseInsensitive) {
if (caseInsensitive) {
str = str.toLowerCase();
return StringUtils.containsIgnoreCase(str, searchStr);
} else {
return str.contains(searchStr);
}
List<T> results = new ArrayList<>();
for (int i = 0; i < size; i++) {
String key = keys.get(i);
if (caseInsensitive) {
key = key.toLowerCase();
}
if (key.contains(str)) {
results.add(values.get(i));
}
@Override
public Flowable<T> search(final String searchStr, final boolean caseInsensitive) {
return Flowable.create(emitter -> {
int size = size();
for (int i = 0; i < size; i++) {
if (isMatched(keys.get(i), searchStr, caseInsensitive)) {
emitter.onNext(values.get(i));
}
if (emitter.isCancelled()) {
return;
}
}
}
return results;
emitter.onComplete();
}, BackpressureStrategy.LATEST);
}
@Override
......
......@@ -2,7 +2,12 @@ package jadx.gui.utils.search;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.FlowableEmitter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -13,9 +18,16 @@ import jadx.api.JavaNode;
import jadx.core.codegen.CodeWriter;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.CommonSearchDialog;
import jadx.gui.ui.SearchDialog;
import jadx.gui.utils.CodeLinesInfo;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.Utils;
import static jadx.gui.ui.SearchDialog.SearchOptions.CLASS;
import static jadx.gui.ui.SearchDialog.SearchOptions.CODE;
import static jadx.gui.ui.SearchDialog.SearchOptions.FIELD;
import static jadx.gui.ui.SearchDialog.SearchOptions.IGNORE_CASE;
import static jadx.gui.ui.SearchDialog.SearchOptions.METHOD;
public class TextSearchIndex {
......@@ -41,7 +53,7 @@ public class TextSearchIndex {
public void indexNames(JavaClass cls) {
clsNamesIndex.put(cls.getFullName(), nodeCache.makeFrom(cls));
for (JavaMethod mth : cls.getMethods()) {
mthNamesIndex.put(mth.getFullName(), this.nodeCache.makeFrom(mth));
mthNamesIndex.put(mth.getFullName(), nodeCache.makeFrom(mth));
}
for (JavaField fld : cls.getFields()) {
fldNamesIndex.put(fld.getFullName(), nodeCache.makeFrom(fld));
......@@ -73,54 +85,70 @@ public class TextSearchIndex {
}
}
public List<JNode> searchClsName(String text, boolean caseInsensitive) {
return clsNamesIndex.getValuesForKeysContaining(text, caseInsensitive);
}
public List<JNode> searchMthName(String text, boolean caseInsensitive) {
return mthNamesIndex.getValuesForKeysContaining(text, caseInsensitive);
}
public List<JNode> searchFldName(String text, boolean caseInsensitive) {
return fldNamesIndex.getValuesForKeysContaining(text, caseInsensitive);
}
public Flowable<JNode> buildSearch(String text, Set<SearchDialog.SearchOptions> options) {
boolean ignoreCase = options.contains(IGNORE_CASE);
LOG.debug("Building search, ignoreCase: {}", ignoreCase);
public List<CodeNode> searchCode(String text, boolean caseInsensitive) {
List<CodeNode> items;
if (codeIndex.size() > 0) {
items = codeIndex.getValuesForKeysContaining(text, caseInsensitive);
if (skippedClasses.isEmpty()) {
return items;
Flowable<JNode> result = Flowable.empty();
if (options.contains(CLASS)) {
result = Flowable.concat(result, clsNamesIndex.search(text, ignoreCase));
}
if (options.contains(METHOD)) {
result = Flowable.concat(result, mthNamesIndex.search(text, ignoreCase));
}
if (options.contains(FIELD)) {
result = Flowable.concat(result, fldNamesIndex.search(text, ignoreCase));
}
if (options.contains(CODE)) {
if (codeIndex.size() > 0) {
result = Flowable.concat(result, codeIndex.search(text, ignoreCase));
}
if (!skippedClasses.isEmpty()) {
result = Flowable.concat(result, searchInSkippedClasses(text, ignoreCase));
}
} else {
items = new ArrayList<>();
}
addSkippedClasses(items, text);
return items;
return result;
}
private void addSkippedClasses(List<CodeNode> list, String text) {
for (JavaClass javaClass : skippedClasses) {
String code = javaClass.getCode();
int pos = 0;
while (pos != -1) {
pos = searchNext(list, text, javaClass, code, pos);
}
if (list.size() > CommonSearchDialog.RESULTS_PER_PAGE) {
return;
public Flowable<CodeNode> searchInSkippedClasses(final String searchStr, final boolean caseInsensitive) {
return Flowable.create(emitter -> {
LOG.debug("Skipped code search started: {} ...", searchStr);
for (JavaClass javaClass : skippedClasses) {
String code = javaClass.getCode();
int pos = 0;
while (pos != -1) {
pos = searchNext(emitter, searchStr, javaClass, code, pos, caseInsensitive);
if (emitter.isCancelled()) {
LOG.debug("Skipped Code search canceled: {}", searchStr);
return;
}
}
if (!Utils.isFreeMemoryAvailable()) {
LOG.warn("Skipped code search stopped due to memory limit: {}", Utils.memoryInfo());
emitter.onComplete();
return;
}
}
}
LOG.debug("Skipped code search complete: {}, memory usage: {}", searchStr, Utils.memoryInfo());
emitter.onComplete();
}, BackpressureStrategy.LATEST);
}
private int searchNext(List<CodeNode> list, String text, JavaNode javaClass, String code, int startPos) {
int pos = code.indexOf(text, startPos);
private int searchNext(FlowableEmitter<CodeNode> emitter, String text, JavaNode javaClass, String code,
int startPos, boolean ignoreCase) {
int pos;
if (ignoreCase) {
pos = StringUtils.indexOfIgnoreCase(code, text, startPos);
} else {
pos = code.indexOf(text, startPos);
}
if (pos == -1) {
return -1;
}
int lineStart = 1 + code.lastIndexOf(CodeWriter.NL, pos);
int lineEnd = code.indexOf(CodeWriter.NL, pos + text.length());
StringRef line = StringRef.subString(code, lineStart, lineEnd == -1 ? code.length() : lineEnd);
list.add(new CodeNode(nodeCache.makeFrom(javaClass), -pos, line.trim()));
emitter.onNext(new CodeNode(nodeCache.makeFrom(javaClass), -pos, line.trim()));
return lineEnd;
}
......@@ -128,10 +156,6 @@ public class TextSearchIndex {
this.skippedClasses.add(cls);
}
public List<JavaClass> getSkippedClasses() {
return skippedClasses;
}
public int getSkippedCount() {
return skippedClasses.size();
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册