From 9a6a8736af1979a547bcf04dca4367bbb9b73294 Mon Sep 17 00:00:00 2001 From: Julia Beliaeva Date: Fri, 21 Apr 2023 21:34:55 +0200 Subject: [PATCH] [vcs-log] highlight text matches in the table GitOrigin-RevId: 87239c349bfbad159803eef9296691adadc4022a --- .../util/resources/misc/registry.properties | 2 + .../ui/render/GraphCommitCellRenderer.java | 11 +++++ .../visible/filters/VcsLogTextFilterImpl.kt | 18 ++++++-- .../log/visible/filters/VcsLogTextFilters.kt | 46 ++++++++++++++++++- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index b54d7688a671..c09309d1c1bc 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -758,6 +758,8 @@ vcs.log.copy.filters.to.new.tab=true vcs.log.copy.filters.to.new.tab.description=If enabled, "Open New Vcs Log Tab" action will copy active filters to the new tab. vcs.log.filter.text.on.the.fly=false vcs.log.filter.text.on.the.fly.description=If enabled, applies text filter to the Log while typing +vcs.log.filter.text.highlight.matches=false +vcs.log.filter.text.highlight.matches.description=Highlight text filter matches in the Log table vcs.log.max.changes.shown=50000 vcs.log.max.changes.shown.description=Limit for showing commits in the changes view. vcs.log.max.branches.shown=100 diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/render/GraphCommitCellRenderer.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/render/GraphCommitCellRenderer.java index 1bdb374002fc..214070fddc35 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/render/GraphCommitCellRenderer.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/render/GraphCommitCellRenderer.java @@ -1,6 +1,7 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.vcs.log.ui.render; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkRenderer; import com.intellij.openapi.vcs.changes.ui.CurrentBranchComponent; @@ -10,6 +11,8 @@ import com.intellij.ui.scale.JBUIScale; import com.intellij.ui.speedSearch.SpeedSearchUtil; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.StartupUiUtil; +import com.intellij.vcs.log.VcsLogFilterCollection; +import com.intellij.vcs.log.VcsLogTextFilter; import com.intellij.vcs.log.VcsRef; import com.intellij.vcs.log.data.VcsLogData; import com.intellij.vcs.log.graph.EdgePrintElement; @@ -21,6 +24,7 @@ import com.intellij.vcs.log.ui.table.VcsLogCellRenderer; import com.intellij.vcs.log.ui.table.VcsLogGraphTable; import com.intellij.vcs.log.ui.table.column.Commit; import com.intellij.vcs.log.ui.table.column.VcsLogColumnManager; +import com.intellij.vcs.log.visible.filters.VcsLogTextFilterWithMatches; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -239,6 +243,13 @@ public class GraphCommitCellRenderer extends TypeSafeTableCellRenderer { + return generateSequence({ findMatchingRange(message, null) }) { + lastRange -> findMatchingRange(message, lastRange) + }.asIterable() + } + + private fun findMatchingRange(message: String, previousRange: TextRange?): TextRange? { + val startIndex = previousRange?.endOffset ?: 0 + val startOffset = message.indexOf(text, startIndex, !isMatchCase).takeIf { it >= 0 } ?: return null + return TextRange(startOffset, startOffset + text.length) + } + @NonNls override fun toString(): String { return "containing '$text' ${caseSensitiveText()}" diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/visible/filters/VcsLogTextFilters.kt b/platform/vcs-log/impl/src/com/intellij/vcs/log/visible/filters/VcsLogTextFilters.kt index b5770074f17a..9194340db842 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/visible/filters/VcsLogTextFilters.kt +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/visible/filters/VcsLogTextFilters.kt @@ -2,16 +2,33 @@ package com.intellij.vcs.log.visible.filters import com.intellij.openapi.util.Comparing +import com.intellij.openapi.util.TextRange import com.intellij.vcs.log.VcsLogDetailsFilter import com.intellij.vcs.log.VcsLogTextFilter +import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.NonNls import java.util.* import java.util.regex.Pattern +@ApiStatus.Experimental +interface VcsLogTextFilterWithMatches : VcsLogTextFilter { + override fun matches(message: String): Boolean { + return matchingRanges(message).iterator().hasNext() + } + + /** + * Returns text ranges for matches in the specified commit message. + * + * @param message a commit message to match + * @return an Iterable containing text ranges for matches + */ + fun matchingRanges(message: String): Iterable +} + /** * @see VcsLogFilterObject.fromPattern */ -internal data class VcsLogRegexTextFilter(private val pattern: Pattern) : VcsLogDetailsFilter, VcsLogTextFilter { +internal data class VcsLogRegexTextFilter(private val pattern: Pattern) : VcsLogDetailsFilter, VcsLogTextFilterWithMatches { override fun matches(message: String): Boolean = pattern.matcher(message).find() override fun getText(): String = pattern.pattern() @@ -20,6 +37,10 @@ internal data class VcsLogRegexTextFilter(private val pattern: Pattern) : VcsLog override fun matchesCase(): Boolean = (pattern.flags() and Pattern.CASE_INSENSITIVE) == 0 + override fun matchingRanges(message: String): Iterable { + return Iterable { pattern.matcher(message).results().map { TextRange(it.start(), it.end()) }.iterator() } + } + @NonNls override fun toString(): String { return "matching '$text' ${caseSensitiveText()}" @@ -30,7 +51,7 @@ internal data class VcsLogRegexTextFilter(private val pattern: Pattern) : VcsLog * @see VcsLogFilterObject.fromPatternsList */ internal class VcsLogMultiplePatternsTextFilter(val patterns: List, - private val isMatchCase: Boolean) : VcsLogDetailsFilter, VcsLogTextFilter { + private val isMatchCase: Boolean) : VcsLogDetailsFilter, VcsLogTextFilterWithMatches { override fun getText(): String = if (patterns.size == 1) patterns.single() else patterns.joinToString("|") { Pattern.quote(it) } override fun isRegex(): Boolean = patterns.size > 1 @@ -39,6 +60,27 @@ internal class VcsLogMultiplePatternsTextFilter(val patterns: List, override fun matches(message: String): Boolean = patterns.any { message.contains(it, !isMatchCase) } + override fun matchingRanges(message: String): Iterable { + return generateSequence({ findNextMatch(message, null) }) { previousRange -> + findNextMatch(message, previousRange) + }.asIterable() + } + + private fun findNextMatch(message: String, previousRange: TextRange?): TextRange? { + val startIndex = previousRange?.endOffset ?: 0 + + var match: TextRange? = null + for (pattern in patterns) { + val patternIndex = message.indexOf(pattern, startIndex, !isMatchCase) + if (patternIndex < 0) continue + + if (match == null || patternIndex <= match.startOffset) { + match = TextRange(patternIndex, patternIndex + pattern.length) + } + } + return match + } + @NonNls override fun toString(): String { return "containing at least one of the ${patterns.joinToString(", ") { s -> "'$s'" }} ${caseSensitiveText()}"