diff options
13 files changed, 341 insertions, 90 deletions
diff --git a/logcat/gen/com/android/tools/idea/logcat/filters/parser/LogcatFilterLexer.java b/logcat/gen/com/android/tools/idea/logcat/filters/parser/LogcatFilterLexer.java index b76bcc79611..ec65e4b4cf8 100644 --- a/logcat/gen/com/android/tools/idea/logcat/filters/parser/LogcatFilterLexer.java +++ b/logcat/gen/com/android/tools/idea/logcat/filters/parser/LogcatFilterLexer.java @@ -120,13 +120,13 @@ class LogcatFilterLexer implements FlexLexer { private static final String ZZ_ACTION_PACKED_0 = "\4\0\1\1\1\2\1\1\1\3\1\4\1\5\1\6"+ - "\2\7\6\1\1\10\3\7\1\11\2\7\1\12\2\1"+ - "\1\0\1\1\3\0\7\1\1\10\1\0\1\10\3\0"+ + "\2\7\7\1\1\10\3\7\1\11\2\7\1\12\2\1"+ + "\1\0\1\1\3\0\10\1\1\10\1\0\1\10\3\0"+ "\1\11\1\0\1\11\3\0\4\1\1\13\3\1\2\10"+ "\2\11\2\1\1\14\3\1\1\15"; private static int [] zzUnpackAction() { - int [] result = new int[72]; + int [] result = new int[74]; int offset = 0; offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); return result; @@ -153,16 +153,17 @@ class LogcatFilterLexer implements FlexLexer { private static final String ZZ_ROWMAP_PACKED_0 = "\0\0\0\33\0\66\0\121\0\154\0\207\0\242\0\154"+ "\0\154\0\275\0\275\0\330\0\363\0\u010e\0\u0129\0\u0144"+ - "\0\u015f\0\u017a\0\u0195\0\u01b0\0\275\0\u01cb\0\u01e6\0\u0201"+ - "\0\u021c\0\u0237\0\u0252\0\u026d\0\u0288\0\330\0\275\0\u02a3"+ - "\0\363\0\u02be\0\u02d9\0\u02f4\0\u030f\0\u032a\0\u0345\0\u0360"+ - "\0\u037b\0\u0396\0\u01cb\0\275\0\u03b1\0\u01e6\0\u03cc\0\u03e7"+ - "\0\u021c\0\275\0\u0402\0\u0237\0\u041d\0\330\0\363\0\u0438"+ - "\0\u0453\0\154\0\u046e\0\u0489\0\u04a4\0\u01cb\0\u01e6\0\u021c"+ - "\0\u0237\0\u04bf\0\u04da\0\154\0\u04f5\0\u0510\0\u052b\0\154"; + "\0\u015f\0\u017a\0\u0195\0\u01b0\0\u01cb\0\275\0\u01e6\0\u0201"+ + "\0\u021c\0\u0237\0\u0252\0\u026d\0\u0288\0\u02a3\0\330\0\275"+ + "\0\u02be\0\363\0\u02d9\0\u02f4\0\u030f\0\u032a\0\u0345\0\u0360"+ + "\0\u037b\0\u0396\0\u03b1\0\u03cc\0\u01e6\0\275\0\u03e7\0\u0201"+ + "\0\u0402\0\u041d\0\u0237\0\275\0\u0438\0\u0252\0\u0453\0\330"+ + "\0\363\0\u046e\0\u0489\0\154\0\u04a4\0\u04bf\0\u04da\0\u01e6"+ + "\0\u0201\0\u0237\0\u0252\0\u04f5\0\u0510\0\154\0\u052b\0\u0546"+ + "\0\u0561\0\154"; private static int [] zzUnpackRowMap() { - int [] result = new int[72]; + int [] result = new int[74]; int offset = 0; offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); return result; @@ -186,58 +187,60 @@ class LogcatFilterLexer implements FlexLexer { private static final String ZZ_TRANS_PACKED_0 = "\1\5\1\6\1\5\1\7\2\5\1\10\1\11\1\12"+ - "\1\13\1\14\1\5\1\6\1\15\1\16\1\17\2\5"+ - "\1\20\1\5\1\21\1\5\1\22\2\5\1\23\1\5"+ - "\1\24\1\6\6\24\2\25\1\26\1\24\1\6\1\27"+ - "\15\24\1\30\1\6\6\30\2\25\1\31\1\30\1\6"+ - "\1\32\15\30\1\33\1\6\12\33\1\6\16\33\1\5"+ - "\1\0\6\5\2\0\1\5\1\34\1\0\16\5\1\0"+ - "\1\6\12\0\1\6\16\0\1\5\1\0\6\5\2\0"+ - "\1\5\1\34\1\0\1\5\1\35\3\5\1\20\3\5"+ - "\1\22\2\5\1\23\1\5\33\0\12\36\1\37\1\40"+ - "\17\36\13\41\1\42\1\41\1\37\15\41\1\5\1\0"+ - "\6\5\2\0\1\5\1\34\1\0\2\5\1\43\1\5"+ - "\1\44\12\5\1\0\6\5\2\0\1\5\1\34\1\0"+ - "\6\5\1\45\10\5\1\0\6\5\2\0\1\5\1\34"+ - "\1\0\4\5\1\46\12\5\1\0\6\5\2\0\1\5"+ - "\1\34\1\0\10\5\1\47\6\5\1\0\6\5\2\0"+ - "\1\5\1\34\1\0\7\5\1\50\7\5\1\0\6\5"+ - "\2\0\1\5\1\34\1\0\7\5\1\51\6\5\1\24"+ - "\1\0\6\24\2\0\1\24\1\52\1\0\16\24\12\53"+ - "\1\54\1\55\17\53\13\56\1\57\1\56\1\54\15\56"+ - "\1\30\1\0\6\30\2\0\1\30\1\60\1\0\16\30"+ - "\12\61\1\62\1\63\17\61\13\64\1\65\1\64\1\62"+ - "\15\64\1\33\1\0\12\33\1\0\16\33\1\5\1\0"+ - "\6\5\2\0\1\5\1\34\20\5\1\0\6\5\2\0"+ - "\1\5\1\34\1\0\2\5\1\43\13\5\12\36\1\66"+ - "\1\40\17\36\13\41\1\42\1\41\1\67\15\41\1\5"+ - "\1\0\6\5\2\0\1\5\1\34\1\0\3\5\1\70"+ - "\13\5\1\0\6\5\2\0\1\5\1\34\1\0\15\5"+ - "\1\71\1\5\1\0\1\72\5\5\2\0\1\5\1\34"+ - "\1\0\17\5\1\0\6\5\2\0\1\5\1\34\1\0"+ - "\6\5\1\73\10\5\1\0\6\5\2\0\1\5\1\34"+ - "\1\0\4\5\1\45\12\5\1\0\6\5\2\0\1\5"+ - "\1\34\1\0\12\5\1\74\4\5\1\0\6\5\2\0"+ - "\1\5\1\34\1\0\10\5\1\75\5\5\1\24\1\0"+ - "\6\24\2\0\1\24\1\52\17\24\12\53\1\76\1\55"+ - "\17\53\13\56\1\57\1\56\1\77\15\56\1\30\1\0"+ - "\6\30\2\0\1\30\1\60\17\30\12\61\1\100\1\63"+ - "\17\61\13\64\1\65\1\64\1\101\15\64\1\5\1\0"+ - "\6\5\2\0\1\5\1\34\1\0\4\5\1\75\12\5"+ - "\1\0\6\5\2\0\1\5\1\34\1\0\4\5\1\102"+ - "\12\5\1\0\6\5\2\0\1\5\1\34\1\0\6\5"+ - "\1\103\10\5\1\0\6\5\2\0\1\5\1\34\1\0"+ - "\13\5\1\103\3\5\1\0\1\104\1\5\1\105\1\106"+ - "\2\5\2\0\1\5\1\34\1\0\17\5\1\0\6\5"+ - "\2\0\1\5\1\34\1\0\1\5\1\45\15\5\1\0"+ - "\6\5\2\0\1\5\1\34\1\0\7\5\1\107\7\5"+ - "\1\0\1\110\5\5\2\0\1\5\1\34\1\0\17\5"+ - "\1\0\1\104\5\5\2\0\1\5\1\34\1\0\17\5"+ - "\1\0\6\5\2\0\1\5\1\34\1\0\10\5\1\70"+ - "\5\5"; + "\1\13\1\14\1\5\1\6\1\15\1\16\1\17\1\20"+ + "\1\5\1\21\1\5\1\22\1\5\1\23\2\5\1\24"+ + "\1\5\1\25\1\6\6\25\2\26\1\27\1\25\1\6"+ + "\1\30\15\25\1\31\1\6\6\31\2\26\1\32\1\31"+ + "\1\6\1\33\15\31\1\34\1\6\12\34\1\6\16\34"+ + "\1\5\1\0\6\5\2\0\1\5\1\35\1\0\16\5"+ + "\1\0\1\6\12\0\1\6\16\0\1\5\1\0\6\5"+ + "\2\0\1\5\1\35\1\0\1\5\1\36\3\5\1\21"+ + "\3\5\1\23\2\5\1\24\1\5\33\0\12\37\1\40"+ + "\1\41\17\37\13\42\1\43\1\42\1\40\15\42\1\5"+ + "\1\0\6\5\2\0\1\5\1\35\1\0\2\5\1\44"+ + "\1\5\1\45\12\5\1\0\6\5\2\0\1\5\1\35"+ + "\1\0\6\5\1\46\10\5\1\0\6\5\2\0\1\5"+ + "\1\35\1\0\7\5\1\47\7\5\1\0\6\5\2\0"+ + "\1\5\1\35\1\0\4\5\1\50\12\5\1\0\6\5"+ + "\2\0\1\5\1\35\1\0\10\5\1\51\6\5\1\0"+ + "\6\5\2\0\1\5\1\35\1\0\7\5\1\52\7\5"+ + "\1\0\6\5\2\0\1\5\1\35\1\0\7\5\1\53"+ + "\6\5\1\25\1\0\6\25\2\0\1\25\1\54\1\0"+ + "\16\25\12\55\1\56\1\57\17\55\13\60\1\61\1\60"+ + "\1\56\15\60\1\31\1\0\6\31\2\0\1\31\1\62"+ + "\1\0\16\31\12\63\1\64\1\65\17\63\13\66\1\67"+ + "\1\66\1\64\15\66\1\34\1\0\12\34\1\0\16\34"+ + "\1\5\1\0\6\5\2\0\1\5\1\35\20\5\1\0"+ + "\6\5\2\0\1\5\1\35\1\0\2\5\1\44\13\5"+ + "\12\37\1\70\1\41\17\37\13\42\1\43\1\42\1\71"+ + "\15\42\1\5\1\0\6\5\2\0\1\5\1\35\1\0"+ + "\3\5\1\72\13\5\1\0\6\5\2\0\1\5\1\35"+ + "\1\0\15\5\1\73\1\5\1\0\1\74\5\5\2\0"+ + "\1\5\1\35\1\0\17\5\1\0\6\5\2\0\1\5"+ + "\1\35\1\0\5\5\1\51\11\5\1\0\6\5\2\0"+ + "\1\5\1\35\1\0\6\5\1\75\10\5\1\0\6\5"+ + "\2\0\1\5\1\35\1\0\4\5\1\46\12\5\1\0"+ + "\6\5\2\0\1\5\1\35\1\0\12\5\1\76\4\5"+ + "\1\0\6\5\2\0\1\5\1\35\1\0\10\5\1\77"+ + "\5\5\1\25\1\0\6\25\2\0\1\25\1\54\17\25"+ + "\12\55\1\100\1\57\17\55\13\60\1\61\1\60\1\101"+ + "\15\60\1\31\1\0\6\31\2\0\1\31\1\62\17\31"+ + "\12\63\1\102\1\65\17\63\13\66\1\67\1\66\1\103"+ + "\15\66\1\5\1\0\6\5\2\0\1\5\1\35\1\0"+ + "\4\5\1\77\12\5\1\0\6\5\2\0\1\5\1\35"+ + "\1\0\4\5\1\104\12\5\1\0\6\5\2\0\1\5"+ + "\1\35\1\0\6\5\1\105\10\5\1\0\6\5\2\0"+ + "\1\5\1\35\1\0\13\5\1\105\3\5\1\0\1\106"+ + "\1\5\1\107\1\110\2\5\2\0\1\5\1\35\1\0"+ + "\17\5\1\0\6\5\2\0\1\5\1\35\1\0\1\5"+ + "\1\46\15\5\1\0\6\5\2\0\1\5\1\35\1\0"+ + "\7\5\1\111\7\5\1\0\1\112\5\5\2\0\1\5"+ + "\1\35\1\0\17\5\1\0\1\106\5\5\2\0\1\5"+ + "\1\35\1\0\17\5\1\0\6\5\2\0\1\5\1\35"+ + "\1\0\10\5\1\72\5\5"; private static int [] zzUnpackTrans() { - int [] result = new int[1350]; + int [] result = new int[1404]; int offset = 0; offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); return result; @@ -275,12 +278,12 @@ class LogcatFilterLexer implements FlexLexer { private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute(); private static final String ZZ_ATTRIBUTE_PACKED_0 = - "\4\0\5\1\2\11\11\1\1\11\10\1\1\0\1\11"+ - "\3\0\10\1\1\0\1\11\3\0\1\1\1\0\1\11"+ + "\4\0\5\1\2\11\12\1\1\11\10\1\1\0\1\11"+ + "\3\0\11\1\1\0\1\11\3\0\1\1\1\0\1\11"+ "\3\0\23\1"; private static int [] zzUnpackAttribute() { - int [] result = new int[72]; + int [] result = new int[74]; int offset = 0; offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); return result; diff --git a/logcat/src/com/android/tools/idea/logcat/LogcatToolWindowFactory.kt b/logcat/src/com/android/tools/idea/logcat/LogcatToolWindowFactory.kt index 7664ebca2a4..b3b2c2779bc 100644 --- a/logcat/src/com/android/tools/idea/logcat/LogcatToolWindowFactory.kt +++ b/logcat/src/com/android/tools/idea/logcat/LogcatToolWindowFactory.kt @@ -57,6 +57,12 @@ internal class LogcatToolWindowFactory : SplittingTabsToolWindowFactory(), DumbA ProcessNameMonitor.getInstance(project).start() } + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + super.createToolWindowContent(project, toolWindow) + toolWindow.isAvailable = true + toolWindow.setToHideOnEmptyContent(true) + } + private fun showLogcat(toolWindow: ToolWindowEx, serialNumber: String) { EventQueue.invokeLater { toolWindow.activate { diff --git a/logcat/src/com/android/tools/idea/logcat/filters/FilterTextField.kt b/logcat/src/com/android/tools/idea/logcat/filters/FilterTextField.kt index 7ec3e8843d8..67defa4d5dc 100644 --- a/logcat/src/com/android/tools/idea/logcat/filters/FilterTextField.kt +++ b/logcat/src/com/android/tools/idea/logcat/filters/FilterTextField.kt @@ -22,6 +22,7 @@ import com.android.tools.idea.logcat.LogcatBundle import com.android.tools.idea.logcat.LogcatPresenter import com.android.tools.idea.logcat.PACKAGE_NAMES_PROVIDER_KEY import com.android.tools.idea.logcat.TAGS_PROVIDER_KEY +import com.android.tools.idea.logcat.filters.FilterTextField.FilterHistoryItem.Hint import com.android.tools.idea.logcat.filters.FilterTextField.FilterHistoryItem.Item import com.android.tools.idea.logcat.filters.FilterTextField.FilterHistoryItem.Separator import com.android.tools.idea.logcat.filters.parser.LogcatFilterFileType @@ -38,11 +39,17 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.asSequence +import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.ui.popup.PopupChooserBuilder import com.intellij.openapi.util.Disposer import com.intellij.ui.CollectionListModel import com.intellij.ui.EditorTextField +import com.intellij.ui.JBColor.BLUE +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.SimpleTextAttributes import com.intellij.ui.components.JBList import com.intellij.util.ui.EmptyIcon import com.intellij.util.ui.JBUI @@ -99,6 +106,8 @@ private val HISTORY_ICON_BORDER = JBUI.Borders.empty(0, 5, 0, 4) private val HISTORY_LIST_SEPARATOR_BORDER = JBUI.Borders.empty(3) +private val NAMED_FILTER_HISTORY_ITEM_COLOR = SimpleTextAttributes.fromTextAttributes(TextAttributes(BLUE, null, null, null, Font.PLAIN)) + /** * A text field for the filter. */ @@ -245,7 +254,9 @@ internal class FilterTextField( val popupDisposable = Disposer.newDisposable("popupDisposable") addToHistory() - val popup = PopupChooserBuilder(HistoryList(popupDisposable, logcatPresenter, filterHistory)) + val list: JList<FilterHistoryItem> = HistoryList(popupDisposable, logcatPresenter, filterHistory, filterParser) + JBPopupFactory.getInstance().createPopupChooserBuilder(listOf("foo", "bar")) + val popup = PopupChooserBuilder(list) .setMovable(false) .setRequestFocus(true) .setItemChosenCallback { @@ -254,7 +265,7 @@ internal class FilterTextField( isFavorite = item.isFavorite } } - .setSelectedValue(Item(text, isFavorite, count = null), true) + .setSelectedValue(Item(text, isFavorite, count = null, filterParser), true) .createPopup() Disposer.register(popup, popupDisposable) @@ -334,16 +345,19 @@ internal class FilterTextField( parentDisposable: Disposable, logcatPresenter: LogcatPresenter, filterHistory: AndroidLogcatFilterHistory, + filterParser: LogcatFilterParser, coroutineContext: CoroutineContext = EmptyCoroutineContext, ) : JBList<FilterHistoryItem>() { init { // The "count" field in FilterHistoryItem.Item takes time to calculate so initially, add all items with no count. val items = mutableListOf<FilterHistoryItem>().apply { - addAll(filterHistory.favorites.map { Item(filter = it, isFavorite = true, count = null) }) + addAll(filterHistory.favorites.map { Item(filter = it, isFavorite = true, count = null, filterParser) }) if (filterHistory.favorites.isNotEmpty() && filterHistory.nonFavorites.isNotEmpty()) { add(Separator) } - addAll(filterHistory.nonFavorites.map { Item(filter = it, isFavorite = false, count = null) }) + addAll(filterHistory.nonFavorites.map { Item(filter = it, isFavorite = false, count = null, filterParser) }) + add(Separator) + add(Hint) } val listModel = CollectionListModel(items) model = listModel @@ -370,7 +384,7 @@ internal class FilterTextField( // Replacing an item in the model will remove the selection. Save the selected index, so we can restore it after. withContext(uiThread) { val selected = selectedIndex - listModel.setElementAt(Item(item.filter, item.isFavorite, count), index) + listModel.setElementAt(Item(item.filter, item.isFavorite, count, filterParser), index) if (selected >= 0) { selectedIndex = selected } @@ -399,12 +413,25 @@ internal class FilterTextField( */ @VisibleForTesting internal sealed class FilterHistoryItem { - class Item(val filter: String, val isFavorite: Boolean, val count: Int?) + class Item(val filter: String, val isFavorite: Boolean, val count: Int?, private val filterParser: LogcatFilterParser) : FilterHistoryItem() { + private val filterName = filterParser.parse(filter)?.getFilterName() + override fun getComponent(isSelected: Boolean, list: JList<out FilterHistoryItem>): JComponent { + filterLabel.clear() + if (filterName != null) { + // If there is more than one Item with the same filterName, show the name and the filter. + val sameName = list.model.asSequence().filterIsInstance<Item>().count { it.filterName == filterName } + filterLabel.append(filterName, NAMED_FILTER_HISTORY_ITEM_COLOR) + if (sameName > 1) { + filterLabel.append(": ${filterParser.removeFilterNames(filter)}") + } + } + else { + filterLabel.append(filter) + } favoriteLabel.icon = if (isFavorite) FAVORITE_ON_ICON else FAVORITE_BLANK_ICON - filterLabel.text = filter countLabel.text = when (count) { null -> " ".repeat(3) in 0..99 -> "% 2d ".format(count) @@ -445,7 +472,7 @@ internal class FilterTextField( // The common pattern is to reuse the same component for all the items rather than allocate a new one for each item. companion object { private val favoriteLabel = JLabel() - private val filterLabel = JLabel().apply { + private val filterLabel = SimpleColoredComponent().apply { border = HISTORY_ITEM_LABEL_BORDER } @@ -478,6 +505,21 @@ internal class FilterTextField( } } + object Hint : FilterHistoryItem() { + // A standalone JLabel here will change the background of the separator when it is selected. Wrapping it with a JPanel + // suppresses that behavior for some reason. + private val component = JPanel().apply { + add(JLabel("Press Delete to remove an item").apply { + foreground = SimpleTextAttributes.GRAYED_ATTRIBUTES.fgColor + }) + } + + override fun getComponent(isSelected: Boolean, list: JList<out FilterHistoryItem>): JPanel { + component.background = list.background + return component + } + } + abstract fun getComponent(isSelected: Boolean, list: JList<out FilterHistoryItem>): JComponent } } diff --git a/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilter.kt b/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilter.kt index 9207547305b..27b7f3d74db 100644 --- a/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilter.kt +++ b/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilter.kt @@ -55,6 +55,8 @@ internal interface LogcatFilter { fun matches(message: LogcatMessageWrapper): Boolean + open fun getFilterName(): String? = null + companion object { const val MY_PACKAGE = "package:mine" } @@ -68,6 +70,8 @@ internal data class AndLogcatFilter(val filters: List<LogcatFilter>) : LogcatFil } override fun matches(message: LogcatMessageWrapper) = filters.all { it.matches(message) } + + override fun getFilterName(): String? = filters.mapNotNull { it.getFilterName() }.lastOrNull() } internal data class OrLogcatFilter(val filters: List<LogcatFilter>) : LogcatFilter { @@ -78,6 +82,8 @@ internal data class OrLogcatFilter(val filters: List<LogcatFilter>) : LogcatFilt } override fun matches(message: LogcatMessageWrapper) = filters.any { it.matches(message) } + + override fun getFilterName(): String? = filters.mapNotNull { it.getFilterName() }.lastOrNull() } internal enum class LogcatFilterField { @@ -200,6 +206,12 @@ internal object CrashFilter : LogcatFilter { } } +internal data class NameFilter(val name: String) : LogcatFilter { + override fun matches(message: LogcatMessageWrapper): Boolean = true + + override fun getFilterName(): String = name +} + private val EXCEPTION_LINE_PATTERN = Regex("\n\\s*at .+\\(.+\\)\n") internal object StackTraceFilter : LogcatFilter { diff --git a/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilterCompletionContributor.kt b/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilterCompletionContributor.kt index 4f08093875d..0252eab415b 100644 --- a/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilterCompletionContributor.kt +++ b/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilterCompletionContributor.kt @@ -64,11 +64,13 @@ private const val AGE_KEY = "age:" private const val IS_KEY = "is:" +private const val NAME_KEY = "name:" + // The following are getters so they can be tested. If they are consts, the value is fixed before we can override the flag private val KEYS - get() = STRING_KEYS.map { "$it:" } + LEVEL_KEY + AGE_KEY + maybeAddIsKey() + get() = STRING_KEYS.map { "$it:" } + LEVEL_KEY + AGE_KEY + NAME_KEY + maybeAddIsKey() private val ALL_KEYS - get() = STRING_KEYS.map(String::getKeyVariants).flatten() + LEVEL_KEY + AGE_KEY + maybeAddIsKey() + get() = STRING_KEYS.map(String::getKeyVariants).flatten() + LEVEL_KEY + AGE_KEY + NAME_KEY + maybeAddIsKey() private fun maybeAddIsKey() = if (StudioFlags.LOGCAT_IS_FILTER.get()) listOf(IS_KEY) else emptyList() diff --git a/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilterParser.kt b/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilterParser.kt index 30390296aeb..f16318519f9 100644 --- a/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilterParser.kt +++ b/logcat/src/com/android/tools/idea/logcat/filters/LogcatFilterParser.kt @@ -50,6 +50,8 @@ import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.impl.source.tree.PsiErrorElementImpl import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.elementType +import com.intellij.refactoring.suggested.endOffset +import com.intellij.refactoring.suggested.startOffset import java.text.ParseException import java.time.Clock import java.time.Duration @@ -101,6 +103,41 @@ internal class LogcatFilterParser( } } + /** + * Removes `name:` terms from a filter + * + * This method does a best-effort to remove `name:` terms from a filter. The resulting filter string may not be a valid filter under + * extreme circumstances. Therefore, this method should only be used for display purposes. The resulting filter should never actually be + * used for filtering. + * + * An example of a filter that will be broken by this method: `tag:Foo | name:Name` + * + * It's possible to improve this method to be able to handle these cases, but it's non-trivial. + * + * TODO(b/236844042): Improve to Handle Complex Expressions + */ + fun removeFilterNames(filterString: String): String { + return try { + val psi = psiFileFactory.createFileFromText("temp.lcf", LogcatFilterFileType, filterString) + val offsets = PsiTreeUtil.findChildrenOfType(psi, LogcatFilterLiteralExpression::class.java) + .filter { it.firstChild.text == "name:" } + .map { Pair(it.startOffset, it.endOffset) } + .sortedBy { it.first } + + val sb = StringBuilder(filterString) + offsets.reversed().forEach {(start, end) -> + val endWithSpace = if (end < sb.length && sb[end] in arrayOf(' ', '\t')) end + 1 else end + sb.replace(start, endWithSpace, "") + } + sb.toString().trim() + + } + catch (e: LogcatFilterParseException) { + filterString + } + } + + private fun parseInternal(filterString: String): LogcatFilter? { return when { filterString.isEmpty() -> null @@ -262,6 +299,7 @@ private fun LogcatFilterLiteralExpression.toKeyFilter( "level" -> LevelFilter(lastChild.asLogLevel()) "age" -> AgeFilter(lastChild.asDuration(), clock) "is" -> createIsFilter(lastChild.text) + "name" -> createNameFilter(lastChild.toText()) else -> { val value = lastChild.toText() val isNegated = firstChild.text.startsWith('-') @@ -301,6 +339,8 @@ private fun createIsFilter(text: String): LogcatFilter { } } +private fun createNameFilter(text: String): LogcatFilter = NameFilter(text) + private fun PsiElement.asLogLevel(): LogLevel = LogLevel.getByString(text.lowercase()) ?: throw LogcatFilterParseException(PsiErrorElementImpl("Invalid Log Level: $text")) diff --git a/logcat/src/com/android/tools/idea/logcat/filters/parser/LogcatFilter.flex b/logcat/src/com/android/tools/idea/logcat/filters/parser/LogcatFilter.flex index 11cbbfc16fa..9680fe25e25 100644 --- a/logcat/src/com/android/tools/idea/logcat/filters/parser/LogcatFilter.flex +++ b/logcat/src/com/android/tools/idea/logcat/filters/parser/LogcatFilter.flex @@ -93,6 +93,7 @@ KEY = "age" | "level" | "is" + | "name" %state STRING_KVALUE_STATE %state REGEX_KVALUE_STATE diff --git a/logcat/src/com/android/tools/idea/logcat/filters/parser/LogcatFilterLexerWrapper.kt b/logcat/src/com/android/tools/idea/logcat/filters/parser/LogcatFilterLexerWrapper.kt index 78264ce4e34..19feafd97e6 100644 --- a/logcat/src/com/android/tools/idea/logcat/filters/parser/LogcatFilterLexerWrapper.kt +++ b/logcat/src/com/android/tools/idea/logcat/filters/parser/LogcatFilterLexerWrapper.kt @@ -36,6 +36,7 @@ private val STRING_KEYS_REGEX = listOf( "line", "message", "msg", + "name", "package", "tag", ).joinToString("|") diff --git a/logcat/testSrc/com/android/tools/idea/logcat/filters/FilterTextFieldTest.kt b/logcat/testSrc/com/android/tools/idea/logcat/filters/FilterTextFieldTest.kt index d693fc7954b..6c776cab81c 100644 --- a/logcat/testSrc/com/android/tools/idea/logcat/filters/FilterTextFieldTest.kt +++ b/logcat/testSrc/com/android/tools/idea/logcat/filters/FilterTextFieldTest.kt @@ -19,7 +19,6 @@ import com.android.testutils.MockitoKt.any import com.android.testutils.MockitoKt.mock import com.android.tools.adtui.TreeWalker import com.android.tools.adtui.swing.FakeUi -import com.android.tools.adtui.swing.popup.JBPopupRule import com.android.tools.analytics.UsageTrackerRule import com.android.tools.idea.FakeAndroidProjectDetector import com.android.tools.idea.concurrency.waitForCondition @@ -49,6 +48,7 @@ import com.intellij.testFramework.RunsInEdt import com.intellij.testFramework.runInEdtAndGet import com.intellij.testFramework.runInEdtAndWait import com.intellij.ui.EditorTextField +import com.intellij.ui.SimpleColoredComponent import com.intellij.util.ui.components.BorderLayoutPanel import icons.StudioIcons import kotlinx.coroutines.runBlocking @@ -73,11 +73,10 @@ import javax.swing.JSeparator */ class FilterTextFieldTest { private val projectRule = ProjectRule() - private val popupRule = JBPopupRule() private val usageTrackerRule = UsageTrackerRule() @get:Rule - val rule = RuleChain(projectRule, EdtRule(), usageTrackerRule, popupRule) + val rule = RuleChain(projectRule, EdtRule(), usageTrackerRule) private val project get() = projectRule.project private val filterHistory by lazy { AndroidLogcatFilterHistory.getInstance() } @@ -236,14 +235,16 @@ class FilterTextFieldTest { filterHistory.add("bar", isFavorite = false) fakeLogcatPresenter.filterMatchesCount["foo"] = 1 fakeLogcatPresenter.filterMatchesCount["bar"] = 2 - val historyList = HistoryList(project, fakeLogcatPresenter, filterHistory, coroutineContext) + val historyList = HistoryList(project, fakeLogcatPresenter, filterHistory, logcatFilterParser, coroutineContext) historyList.waitForCounts(fakeLogcatPresenter) assertThat(historyList.renderToStrings()).containsExactly( "*: foo ( 1 )", "----------------------------------", " : bar ( 2 )", - ) + "----------------------------------", + "Press Delete to remove an item", + ).inOrder() // Order is reverse of the order added } @Suppress("OPT_IN_USAGE") // runBlockingTest is experimental @@ -254,13 +255,15 @@ class FilterTextFieldTest { filterHistory.add("bar", isFavorite = true) fakeLogcatPresenter.filterMatchesCount["foo"] = 1 fakeLogcatPresenter.filterMatchesCount["bar"] = 2 - val historyList = HistoryList(project, fakeLogcatPresenter, filterHistory, coroutineContext) + val historyList = HistoryList(project, fakeLogcatPresenter, filterHistory, logcatFilterParser, coroutineContext) historyList.waitForCounts(fakeLogcatPresenter) assertThat(historyList.renderToStrings()).containsExactly( - "*: foo ( 1 )", "*: bar ( 2 )", - ) + "*: foo ( 1 )", + "----------------------------------", + "Press Delete to remove an item", + ).inOrder() // Order is reverse of the order added } @Suppress("OPT_IN_USAGE") // runBlockingTest is experimental @@ -271,15 +274,51 @@ class FilterTextFieldTest { filterHistory.add("bar", isFavorite = false) fakeLogcatPresenter.filterMatchesCount["foo"] = 1 fakeLogcatPresenter.filterMatchesCount["bar"] = 2 - val historyList = HistoryList(project, fakeLogcatPresenter, filterHistory, coroutineContext) + val historyList = HistoryList(project, fakeLogcatPresenter, filterHistory,logcatFilterParser, coroutineContext) historyList.waitForCounts(fakeLogcatPresenter) assertThat(historyList.renderToStrings()).containsExactly( - " : foo ( 1 )", " : bar ( 2 )", - ) + " : foo ( 1 )", + "----------------------------------", + "Press Delete to remove an item", + ).inOrder() // Order is reverse of the order added + } + + @Suppress("OPT_IN_USAGE") // runBlockingTest is experimental + @Test + @RunsInEdt + fun historyList_renderNamedFilter() = runBlockingTest { + filterHistory.add("name:Foo tag:Foo", isFavorite = false) + fakeLogcatPresenter.filterMatchesCount["name:Foo tag:Foo"] = 1 + val historyList = HistoryList(project, fakeLogcatPresenter, filterHistory, logcatFilterParser, coroutineContext) + historyList.waitForCounts(fakeLogcatPresenter) + + assertThat(historyList.renderToStrings()).containsExactly( + " : Foo ( 1 )", + "----------------------------------", + "Press Delete to remove an item", + ).inOrder() } + @Suppress("OPT_IN_USAGE") // runBlockingTest is experimental + @Test + @RunsInEdt + fun historyList_renderNamedFilterWithSameName() = runBlockingTest { + filterHistory.add("name:Foo tag:Foo", isFavorite = false) + filterHistory.add("name:Foo tag:Foobar", isFavorite = false) + fakeLogcatPresenter.filterMatchesCount["name:Foo tag:Foo"] = 1 + fakeLogcatPresenter.filterMatchesCount["name:Foo tag:Foobar"] = 2 + val historyList = HistoryList(project, fakeLogcatPresenter, filterHistory, logcatFilterParser, coroutineContext) + historyList.waitForCounts(fakeLogcatPresenter) + + assertThat(historyList.renderToStrings()).containsExactly( + " : Foo: tag:Foobar ( 2 )", + " : Foo: tag:Foo ( 1 )", + "----------------------------------", + "Press Delete to remove an item", + ).inOrder() // Order is reverse of the order added + } @Test @RunsInEdt @@ -411,7 +450,7 @@ private fun Component.renderToString(): String { private fun BorderLayoutPanel.renderToString(): String { val favorite = if ((components[0] as JLabel).icon == StudioIcons.Logcat.Input.FAVORITE_FILLED) "*" else " " - val text = (components[1] as JLabel).text + val text = (components[1] as SimpleColoredComponent).toString() val count = (components[2] as JLabel).text return "$favorite: $text ($count)" } diff --git a/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterCompletionContributorTest.kt b/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterCompletionContributorTest.kt index 5e189f7dda0..b0af0646b6b 100644 --- a/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterCompletionContributorTest.kt +++ b/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterCompletionContributorTest.kt @@ -87,6 +87,7 @@ class LogcatFilterCompletionContributorTest { "level:", "line:", "message:", + "name:", "package:", "package:mine ", "tag:") @@ -107,6 +108,7 @@ class LogcatFilterCompletionContributorTest { "level:", "line:", "message:", + "name:", "package:", "package:mine ", "tag:", @@ -389,6 +391,7 @@ class LogcatFilterCompletionContributorTest { "level:", "line:", "message:", + "name:", "package:", "package:mine ", "tag:") diff --git a/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterParserTest.kt b/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterParserTest.kt index c640dc53cf8..90380ed083f 100644 --- a/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterParserTest.kt +++ b/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterParserTest.kt @@ -218,7 +218,6 @@ class LogcatFilterParserTest { assertThat(logcatFilterParser().parse(query)).isEqualTo(StringFilter(query, IMPLICIT_LINE)) } - @Test fun parse_topLevelExpressions_joinConsecutiveTopLevelValue_true() { @@ -382,11 +381,72 @@ class LogcatFilterParserTest { assertThat(logcatFilterParser().getUsageTrackingEvent("")?.build()).isEqualTo(LogcatFilterEvent.getDefaultInstance()) } + @Test + fun parse_name() { + val filter = logcatFilterParser().parse("level:INFO tag:bar package:foo name:Name") + + assertThat(filter).isEqualTo( + AndLogcatFilter( + LevelFilter(INFO), + StringFilter("bar", TAG), + StringFilter("foo", APP), + NameFilter("Name"), + ) + ) + } + + @Test + fun parse_name_quoted() { + val filter = logcatFilterParser().parse("""name:'Name1' name:"Name2"""") + + assertThat(filter).isEqualTo( + AndLogcatFilter( + NameFilter("Name1"), + NameFilter("Name2"), + ) + ) + } + + @Test + fun removeFilterNames_beginning() { + assertThat(logcatFilterParser().removeFilterNames("name:foo package:app")).isEqualTo("package:app") + } + + @Test + fun removeFilterNames_end() { + assertThat(logcatFilterParser().removeFilterNames("package:app name:foo")).isEqualTo("package:app") + } + + @Test + fun removeFilterNames_withWhitespace() { + assertThat(logcatFilterParser().removeFilterNames("package:app name: foo")).isEqualTo("package:app") + } + + @Test + fun removeFilterNames_withSingleQuotes() { + assertThat(logcatFilterParser().removeFilterNames("name:'foo' package:app")).isEqualTo("package:app") + } + + @Test + fun removeFilterNames_withDoubleQuotes() { + assertThat(logcatFilterParser().removeFilterNames("""name:"foo"'" package:app""")).isEqualTo("package:app") + } + + @Test + fun removeFilterNames_multiple() { + assertThat(logcatFilterParser().removeFilterNames("name:foo package:app name:foo")).isEqualTo("package:app") + } + private fun logcatFilterParser( androidProjectDetector: AndroidProjectDetector = FakeAndroidProjectDetector(true), joinConsecutiveTopLevelValue: Boolean = true, topLevelSameKeyTreatment: CombineWith = AND, clock: Clock = Clock.systemUTC(), - ) = LogcatFilterParser(project, fakePackageNamesProvider, androidProjectDetector, joinConsecutiveTopLevelValue, topLevelSameKeyTreatment, - clock) + ) = LogcatFilterParser( + project, + fakePackageNamesProvider, + androidProjectDetector, + joinConsecutiveTopLevelValue, + topLevelSameKeyTreatment, + clock) } diff --git a/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterTest.kt b/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterTest.kt index 84cdd8c4944..beee7584910 100644 --- a/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterTest.kt +++ b/logcat/testSrc/com/android/tools/idea/logcat/filters/LogcatFilterTest.kt @@ -278,6 +278,48 @@ class LogcatFilterTest { message2, ).inOrder() } + + @Test + fun nameFilter_matches() { + assertThat(NameFilter("name").matches(logcatMessage(message = "whatever"))).isTrue() + } + + @Test + fun getFilterName_nameFilter() { + assertThat(NameFilter("name").getFilterName()).isEqualTo("name") + } + + @Test + fun getFilterName_simpleFilters() { + assertThat(StringFilter("string", TAG).getFilterName()).isNull() + assertThat(NegatedStringFilter("string", TAG).getFilterName()).isNull() + assertThat(ExactStringFilter("string", TAG).getFilterName()).isNull() + assertThat(NegatedExactStringFilter("string", TAG).getFilterName()).isNull() + assertThat(RegexFilter("string", TAG).getFilterName()).isNull() + assertThat(NegatedRegexFilter("string", TAG).getFilterName()).isNull() + assertThat(LevelFilter(INFO).getFilterName()).isNull() + assertThat(AgeFilter(Duration.ofSeconds(60), Clock.systemDefaultZone()).getFilterName()).isNull() + assertThat(CrashFilter.getFilterName()).isNull() + assertThat(StackTraceFilter.getFilterName()).isNull() + } + + @Test + fun getFilterName_compoundFilter() { + assertThat(AndLogcatFilter(StringFilter("string", TAG), LevelFilter(INFO)).getFilterName()).isNull() + assertThat(OrLogcatFilter(StringFilter("string", TAG), LevelFilter(INFO)).getFilterName()).isNull() + assertThat(AndLogcatFilter( + NameFilter("name1"), + StringFilter("string", TAG), + LevelFilter(INFO), + NameFilter("name2"), + ).getFilterName()).isEqualTo("name2") + assertThat(OrLogcatFilter( + NameFilter("name1"), + StringFilter("string", TAG), + LevelFilter(INFO), + NameFilter("name2"), + ).getFilterName()).isEqualTo("name2") + } } private class TrueFilter : LogcatFilter { diff --git a/logcat/testSrc/com/android/tools/idea/logcat/filters/parser/LogcatFilterPsiTest.kt b/logcat/testSrc/com/android/tools/idea/logcat/filters/parser/LogcatFilterPsiTest.kt index 52b031b2a98..9887c3e9775 100644 --- a/logcat/testSrc/com/android/tools/idea/logcat/filters/parser/LogcatFilterPsiTest.kt +++ b/logcat/testSrc/com/android/tools/idea/logcat/filters/parser/LogcatFilterPsiTest.kt @@ -39,7 +39,7 @@ import org.junit.Test import java.text.ParseException private val STRING_KEYS = listOf("tag", "package", "message", "line") -private val NON_STRING_KEYS = listOf("level", "age", "is") +private val NON_STRING_KEYS = listOf("level", "age", "is", "name") @RunsInEdt class LogcatFilterPsiTest { |