gui: run text search in background thread (#269)
This commit is contained in:
@@ -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,18 +287,17 @@ 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);
|
||||
rows.addAll(nodes);
|
||||
if (!addDescColumn) {
|
||||
for (JNode row : rows) {
|
||||
if (row.hasDescString()) {
|
||||
addDescColumn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void add(JNode node) {
|
||||
if (node.hasDescString()) {
|
||||
addDescColumn = true;
|
||||
}
|
||||
rows.add(node);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
start = 0;
|
||||
addDescColumn = false;
|
||||
@@ -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 JTextField searchField;
|
||||
private JCheckBox caseChBox;
|
||||
private transient JTextField searchField;
|
||||
|
||||
public SearchDialog(MainWindow mainWindow, Set<SearchOptions> options) {
|
||||
private transient Disposable searchDisposable;
|
||||
private transient SearchEventEmitter searchEmitter;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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();
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
if (key.contains(str)) {
|
||||
results.add(values.get(i));
|
||||
}
|
||||
}
|
||||
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 Flowable<JNode> buildSearch(String text, Set<SearchDialog.SearchOptions> options) {
|
||||
boolean ignoreCase = options.contains(IGNORE_CASE);
|
||||
LOG.debug("Building search, ignoreCase: {}", ignoreCase);
|
||||
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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(FlowableEmitter<CodeNode> emitter, String text, JavaNode javaClass, String code,
|
||||
int startPos, boolean ignoreCase) {
|
||||
int pos;
|
||||
if (ignoreCase) {
|
||||
pos = StringUtils.indexOfIgnoreCase(code, text, startPos);
|
||||
} else {
|
||||
items = new ArrayList<>();
|
||||
pos = code.indexOf(text, startPos);
|
||||
}
|
||||
addSkippedClasses(items, text);
|
||||
return items;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int searchNext(List<CodeNode> list, String text, JavaNode javaClass, String code, int startPos) {
|
||||
int 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user