diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 53f2cb001b939dae2e3066fddaa93ddea557fda5..c3ca4d6dd5826126b90901ec1f6b344cf1a1024b 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -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') diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java index 37881af19ce5d64580efc6ce5a84972610213159..8a7e76fbb287681df0c81d2a993a9c6a13a23197 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java @@ -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; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java index c2717d2aea29b5952f264b24a3a1b5af0af59d99..e839724d7ee62468786c428f9249ddf9ad9ae95e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -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 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 diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 4954d232035fc72f1de54e8fbcabef8685b7bb37..24303881baa0abb0168e9adde4e1d0621374de2b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -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")); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java index a8cfb322859b8c60ccbfabfa6d07c22139c971d9..5cc81129988875eb82c9e9d542bcdcbce52d2aa9 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java @@ -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 options; + private transient Set options; + + private transient JTextField searchField; - private JTextField searchField; - private JCheckBox caseChBox; + private transient Disposable searchDisposable; + private transient SearchEventEmitter searchEmitter; - public SearchDialog(MainWindow mainWindow, Set options) { + public SearchDialog(MainWindow mainWindow, boolean textSearch) { super(mainWindow); - this.options = options; + this.textSearch = textSearch; + if (textSearch) { + Set 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 flowable; + private Emitter emitter; + + public SearchEventEmitter() { + flowable = Flowable.create(this::saveEmitter, BackpressureStrategy.LATEST); + } + + public Flowable getFlowable() { + return flowable; + } + + private void saveEmitter(Emitter emitter) { + this.emitter = emitter; + } + + public synchronized void emitSearch() { + this.emitter.onNext(searchField.getText()); + } + } + + private void searchFieldSubscribe() { + searchEmitter = new SearchEventEmitter(); + + Flowable textChanges = onTextFieldChanges(searchField); + Flowable 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 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 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 onTextFieldChanges(final JTextField textField) { + return Flowable.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; } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index e394f513bf0007c1957b86c0738e6cec896cf641..fc47dd81c3af48125383e8681c0fe66e06a6037e 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -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 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 lastSearchOptions) { + this.lastSearchOptions = lastSearchOptions; + } + + public Set getLastSearchOptions() { + return lastSearchOptions; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java index fa85e804eb9abb56a540cabf7ac44ceaed1a8837..2c7328972be8c265c64129061c43c8daa32e9ed6 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/CodeIndex.java @@ -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 implements SearchIndex { + private static final Logger LOG = LoggerFactory.getLogger(CodeIndex.class); + private final List keys = new ArrayList<>(); private final List values = new ArrayList<>(); @@ -28,23 +36,27 @@ public class CodeIndex implements SearchIndex { return true; } + private boolean isMatched(StringRef key, String str, boolean caseInsensitive) { + return key.indexOf(str, caseInsensitive) != -1; + } + @Override - public List getValuesForKeysContaining(String str, boolean caseInsensitive) { - int size = size(); - if (size == 0) { - return Collections.emptyList(); - } - if (caseInsensitive) { - str = str.toLowerCase(); - } - List 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 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 diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java index 9b3fed0b5ff8c867522bff2cc15aa72318002aa1..75edf104bce9b1e07350ff923ba7b95bd26cbbe3 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/SearchIndex.java @@ -1,6 +1,6 @@ package jadx.gui.utils.search; -import java.util.List; +import io.reactivex.Flowable; public interface SearchIndex { @@ -10,7 +10,7 @@ public interface SearchIndex { boolean isStringRefSupported(); - List getValuesForKeysContaining(String str, boolean caseInsensitive); + Flowable search(String searchStr, boolean caseInsensitive); int size(); } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java index 31584578910cedf1aebb242858dd48533cc62fb2..81a3f72f4a4f0424c45cab41397710ee20f9407b 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/SimpleIndex.java @@ -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 implements SearchIndex { private final List keys = new ArrayList<>(); @@ -25,26 +28,28 @@ public class SimpleIndex implements SearchIndex { return false; } - @Override - public List 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 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 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 diff --git a/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java index ccc13af29238845ec7dad3736de4b2a23c394e97..8e577f6dad67eb211c99648e77223a02a807375a 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/search/TextSearchIndex.java @@ -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 searchClsName(String text, boolean caseInsensitive) { - return clsNamesIndex.getValuesForKeysContaining(text, caseInsensitive); - } - - public List searchMthName(String text, boolean caseInsensitive) { - return mthNamesIndex.getValuesForKeysContaining(text, caseInsensitive); - } - - public List searchFldName(String text, boolean caseInsensitive) { - return fldNamesIndex.getValuesForKeysContaining(text, caseInsensitive); - } + public Flowable buildSearch(String text, Set options) { + boolean ignoreCase = options.contains(IGNORE_CASE); + LOG.debug("Building search, ignoreCase: {}", ignoreCase); - public List searchCode(String text, boolean caseInsensitive) { - List items; - if (codeIndex.size() > 0) { - items = codeIndex.getValuesForKeysContaining(text, caseInsensitive); - if (skippedClasses.isEmpty()) { - return items; + Flowable 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 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 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 list, String text, JavaNode javaClass, String code, int startPos) { - int pos = code.indexOf(text, startPos); + private int searchNext(FlowableEmitter 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 getSkippedClasses() { - return skippedClasses; - } - public int getSkippedCount() { return skippedClasses.size(); }