Permalink
Browse files

LogDlg: Allow to use more complex search expressions

Signed-off-by: Sven Strickroth <email@cs-ware.de>
  • Loading branch information...
csware committed Sep 26, 2018
1 parent 9a3fc51 commit 9e826952e9fb7306ef31eef5eeca8260cdc6d014
@@ -609,10 +609,11 @@
<para>
Click on the search icon to select which information you want
to search in, and to choose <emphasis>regex</emphasis> mode.
Normally you will only need a simple text search, but if you
Normally you will only need a simple sub-string search, but if you
need to more flexible search terms, you can use regular expressions.
If you hover the mouse over the box, a tooltip will give hints
on how to use the regex functions.
To invert the results for the entire search expression, start the string with an exclamation mark ('!').
You can also find online documentation and a tutorial at
<ulink url="http://www.regular-expressions.info/">
<citetitle>http://www.regular-expressions.info/</citetitle>
@@ -622,10 +623,56 @@
the filter string are shown.
</para>
<para>
To make the filter show all log entries that do <emphasis>not</emphasis>
match the filter string, start the string with an exclamation mark ('!').
For example, a filter string <literal>!username</literal> will only
show those entries which were not committed by <literal>username</literal>.
Simple sub-string search works in a manner similar to a search engine.
Strings to search for are separated by spaces, and all strings must
match. You can use a leading <literal>-</literal> to specify that a
particular sub-string is not found (invert matching for that term),
and you can use <literal>!</literal> at the start of the expression
to invert matching for the entire expression.
You can use a leading <literal>+</literal> to specify that a sub-string
should be included, even if previously excluded with a <literal>-</literal>.
Note that the order of inclusion/exclusion is significant here.
You can use quotation marks to surround a string which must contain spaces,
and if you want to search for a literal quotation mark you can use two
quotation marks together as a self-escaping sequence.
Note that the backslash character is <emphasis>not</emphasis> used as
an escape character and has no special significance in simple sub-string
searches. Examples will make this easier:
<screen>
Alice Bob -Eve
</screen>
searches for strings containing both Alice and Bob but not Eve
<screen>
Alice -Bob +Eve
</screen>
searches for strings containing both Alice but not Bob, or strings
which contain Eve.
<screen>
-Case +SpecialCase
</screen>
searches for strings which do not contain Case, but still include
strings which contain SpecialCase.
<screen>
!Alice Bob
</screen>
searches for strings which do not contain both Alice and Bob
<screen>
!-Alice -Bob
</screen>
do you remember De Morgan's theorem? NOT(NOT Alice AND NOT Bob)
reduces to (Alice OR Bob).
<screen>
"Alice and Bob"
</screen>
searches for the literal expression <quote>Alice and Bob</quote>
<screen>
""
</screen>
searches for a double-quote anywhere in the text
<screen>
"Alice says ""hi"" to Bob"
</screen>
searches for the literal expression <quote>Alice says "hi" to Bob</quote>.
</para>
<para>
You can also filter the path names in the bottom pane using the
View
@@ -4,6 +4,7 @@ Released: unreleased
== Features ==
* Update libgit to 2.19.0 based on Git for Windows sources
* Fixed issue #2591: Enable accent coloring for search term matches in log messages
* LogDlg: Allow to use more complex search expressions, see documentation
== Bug Fixes ==
* Fixed issue #3250: GitWCRev: IsGitItem return true for item that had never been committed
@@ -29,7 +29,44 @@ bool CLogDlgFilter::Match(const std::wstring& text) const
return false;
if (m_patterns.empty())
return text.find(m_sFilterText) != std::string::npos;
{
// require all strings to be present
bool current_value = true;
for (size_t i = 0, count = subStringConditions.size(); i < count; ++i)
{
const SCondition& condition = subStringConditions[i];
bool found = text.find(condition.subString) != std::wstring::npos;
switch (condition.prefix)
{
case and_not:
found = !found;
// fallthrough
case and:
if (!found)
{
// not a match, so skip to the next "+"-prefixed item
if (condition.nextOrIndex == 0)
return false;
current_value = false;
i = condition.nextOrIndex - 1;
}
break;
case or:
current_value |= found;
if (!current_value)
{
// not a match, so skip to the next "+"-prefixed item
if (condition.nextOrIndex == 0)
return false;
i = condition.nextOrIndex - 1;
}
break;
}
}
}
for (const auto& pattern : m_patterns)
{
@@ -55,19 +92,23 @@ void CLogDlgFilter::GetMatchRanges(std::vector<CHARRANGE>& ranges, CString textU
if (m_patterns.empty())
{
ATLASSERT(!m_sFilterText.empty());
auto toScan = (LPCTSTR)textUTF16;
auto toFind = m_sFilterText.c_str();
size_t toFindLength = m_sFilterText.size();
auto pFound = wcsstr(toScan, toFind);
while (pFound)
for (auto iter = subStringConditions.cbegin(), end = subStringConditions.cend(); iter != end; ++iter)
{
CHARRANGE range;
range.cpMin = (LONG)(pFound - toScan) + offset;
range.cpMax = (LONG)(range.cpMin + toFindLength);
ranges.push_back(range);
pFound = wcsstr(pFound + 1, toFind);
if (iter->prefix == and_not)
continue;
auto toFind = iter->subString.c_str();
size_t toFindLength = iter->subString.size();
auto pFound = wcsstr(toScan, toFind);
while (pFound)
{
CHARRANGE range;
range.cpMin = (LONG)(pFound - toScan) + offset;
range.cpMax = (LONG)(range.cpMin + toFindLength);
ranges.push_back(range);
pFound = wcsstr(pFound + 1, toFind);
}
}
}
else
@@ -122,6 +163,33 @@ bool CLogDlgFilter::ValidateRegexp(const CString& regexp_str, std::vector<std::w
return false;
}
// construction utility
void CLogDlgFilter::AddSubString(CString token, Prefix prefix)
{
if (token.IsEmpty())
return;
if (!m_bCaseSensitive)
token.MakeLower();
// add condition to list
SCondition condition = { token, prefix, 0 };
subStringConditions.push_back(condition);
// update previous conditions
size_t newPos = subStringConditions.size() - 1;
if (prefix == or)
{
for (size_t i = newPos; i > 0; --i)
{
if (subStringConditions[i - 1].nextOrIndex > 0)
break;
subStringConditions[i - 1].nextOrIndex = newPos;
}
}
}
// construction
CLogDlgFilter::CLogDlgFilter()
: m_dwAttributeSelector(UINT_MAX)
@@ -154,15 +222,81 @@ CLogDlgFilter::CLogDlgFilter(const CString& filter, bool filterWithRegex, DWORD
if (useRegex)
useRegex = ValidateRegexp(filterText, m_patterns);
else if (!caseSensitive)
m_sFilterText = filterText.MakeLower();
else
m_sFilterText = filterText;
{
// now split the search string into words so we can search for each of them
int curPos = 0;
int length = filterText.GetLength();
while (curPos < length && curPos >= 0)
{
// skip spaces
for (; (curPos < length) && (filterText[curPos] == L' '); ++curPos)
{
}
// has it a prefix?
Prefix prefix = and;
if (curPos < length)
{
switch (filterText[curPos])
{
case L'-':
prefix = and_not;
++curPos;
break;
case L'+':
prefix = or;
++curPos;
break;
}
}
// escaped string?
if (curPos < length && filterText[curPos] == L'"')
{
CString subString;
while (++curPos < length)
{
if (filterText[curPos] == L'"')
{
// double double quotes?
if (++curPos < length && filterText[curPos] == L'"')
{
// keep one and continue within sub-string
subString.AppendChar(L'"');
}
else
{
// end of sub-string?
if (curPos >= length || filterText[curPos] == L' ')
break;
else
{
// add to sub-string & continue within it
subString.AppendChar(L'"');
subString.AppendChar(filterText[curPos]);
}
}
}
else
subString.AppendChar(filterText[curPos]);
}
AddSubString(subString, prefix);
++curPos;
}
// ordinary sub-string
AddSubString(filterText.Tokenize(L" ", curPos), prefix);
}
}
}
bool CLogDlgFilter::operator()(GitRevLoglist* pRev, CGitLogListBase* loglist) const
{
if (m_patterns.empty() && m_sFilterText.empty())
if (m_patterns.empty() && subStringConditions.empty())
return !m_bNegate;
// we need to perform expensive string / pattern matching
@@ -267,7 +401,7 @@ CLogDlgFilter& CLogDlgFilter::operator=(const CLogDlgFilter& rhs)
m_bCaseSensitive = rhs.m_bCaseSensitive;
m_bNegate = rhs.m_bNegate;
m_sFilterText = rhs.m_sFilterText;
subStringConditions = rhs.subStringConditions;
m_patterns = rhs.m_patterns;
scratch.clear();
@@ -290,9 +424,15 @@ bool CLogDlgFilter::operator==(const CLogDlgFilter& rhs) const
return false;
// compare sub-string defs
if (m_sFilterText != rhs.m_sFilterText)
if (subStringConditions.size() != rhs.subStringConditions.size())
return false;
for (auto lhsIt = subStringConditions.cbegin(), lhsEnd = subStringConditions.cend(), rhsIt = rhs.subStringConditions.cbegin(); lhsIt != lhsEnd; ++rhsIt, ++lhsIt)
{
if (lhsIt->subString != rhsIt->subString || lhsIt->prefix != rhsIt->prefix || lhsIt->nextOrIndex != rhsIt->nextOrIndex)
return false;
}
// no difference detected
return true;
}
@@ -304,5 +444,5 @@ bool CLogDlgFilter::operator!=(const CLogDlgFilter& rhs) const
bool CLogDlgFilter::IsFilterActive() const
{
return !(m_patterns.empty() && m_sFilterText.empty());
return !(m_patterns.empty() && subStringConditions.empty());
}
@@ -26,7 +26,32 @@ class CLogDlgFilter {
private:
std::vector<std::wregex> m_patterns;
std::wstring m_sFilterText;
/// sub-string matching info
enum Prefix
{
and,
or,
and_not,
};
struct SCondition
{
/// sub-strings to find; normalized to lower case
std::wstring subString;
/// depending on the presense of a prefix, indicate
/// how the sub-string match / mismatch gets combined
/// with the current match result
Prefix prefix;
/// index within @ref subStringConditions of the
/// next condition with prefix==or. 0, if no such
/// condition follows.
size_t nextOrIndex;
};
/// list of sub-strings to find; normalized to lower case
std::vector<SCondition> subStringConditions;
/// negate pattern matching result
bool m_bNegate;
@@ -42,6 +67,9 @@ class CLogDlgFilter {
/// allocation operations
mutable std::wstring scratch;
// construction utility
void AddSubString(CString token, Prefix prefix);
public:
/// construction
CLogDlgFilter();

0 comments on commit 9e82695

Please sign in to comment.