Skip to content

Commit

Permalink
Match wildcards with menu-complete.
Browse files Browse the repository at this point in the history
The menu-complete family of commands (including clink-popup-complete)
now supports wildcards.

The match.wild setting controls whether wildcard support is enabled.

Also leading periods are skipped when matching the filename component of
a path, provided the filename component of the pattern has a wildcard.
This allows "bu" to match ".build" like CMD (and Win32 in general) does.
  • Loading branch information
chrisant996 committed Dec 3, 2020
1 parent 3637f3c commit a648688
Show file tree
Hide file tree
Showing 9 changed files with 519 additions and 23 deletions.
4 changes: 1 addition & 3 deletions TODO.md
Expand Up @@ -4,9 +4,6 @@ ChrisAnt Plans

# BETA

- `bu`**menu-complete** should match ".build\" because this is Windows.
- Popup should expand wildcards. Menu-complete should expand wildcards!

## Cmder, Powerline, Clink-Completions
- Update clink-completions to have better 0.4.9 implementations, and also to conditionally use the new API when available.
- Update clink-git-extensions to have better 0.4.9 implementations, and also to conditionally use the new API when available.
Expand Down Expand Up @@ -116,6 +113,7 @@ ChrisAnt Plans
**Miscellaneous**
- Allow to search the console output (not command history) with a RegExp [#166](https://github.com/mridgers/clink/issues/166). _[Unclear how that would work. Would it scroll the console? How would it highlight matches, etc, since that's really something the console host would need to do? I think this needs to be implemented by the console host, e.g. conhost or ConEmu or Terminal, etc.]_
- Add a Lua function that prints through Clink's VT emulation pipeline, so that e.g. the debugger.lua script can use colors.
- Include `wildmatch()` and an `fnmatch()` wrapper for it. But should first update it to support UTF8.

<br/>
<br/>
Expand Down
215 changes: 215 additions & 0 deletions clink/core/include/core/match_wild.h
@@ -0,0 +1,215 @@
// Copyright (c) 2020 Christopher Antos
// License: http://opensource.org/licenses/MIT

#pragma once

#include <core/path.h>
#include <core/str_compare.h>
#include <core/str_iter.h>

class str_base;

namespace path
{

//------------------------------------------------------------------------------
template <class T, int MODE>
bool match_char_impl(int pc, int fc)
{
if (MODE > 0)
{
pc = (pc > 0xffff) ? pc : int(uintptr_t(CharLowerW(LPWSTR(uintptr_t(pc)))));
fc = (fc > 0xffff) ? fc : int(uintptr_t(CharLowerW(LPWSTR(uintptr_t(fc)))));
}

if (MODE > 1)
{
pc = (pc == '-') ? '_' : pc;
fc = (fc == '-') ? '_' : fc;
}

if (pc == '\\') pc = '/';
if (fc == '\\') fc = '/';

return (pc == fc);
}

//------------------------------------------------------------------------------
template <class T, int MODE>
bool match_wild_impl(const str_iter_impl<T>& _pattern, const str_iter_impl<T>& _file)
{
str_iter_impl<T> pattern(_pattern);
str_iter_impl<T> file(_file);

unsigned depth = 0;
const T* pattern_stack[10];
const T* file_stack[10];
bool path_component_stack[10];

const T* final_pattern_component = nullptr;
const T* final_file_component = nullptr;
bool has_final_wildcard = false;

bool start_of_path_component = true;

while (true)
{
int c = pattern.peek();
int d = file.peek();
if (!c)
{
// Consumed pattern, so it's a match iff file was consumed.
return !d;
}

bool symbol_matched = false;
switch (c)
{
case '?':
// Any 1 character (or missing character), except slashes.
if (path::is_separator(d))
break;
if (d)
file.next();
pattern.next();
symbol_matched = true;
break;
case '*': {
const T* push_pattern = pattern.get_pointer();
while (c == '*' || c == '?')
{
pattern.next();
c = pattern.peek();
}
if (c != '?' &&
c != '*')
{
// Iterate until file char matches pattern char after wildcard.
const T* push_scout = file.get_pointer();
while (d &&
!path::is_separator(d) &&
!match_char_impl<T,MODE>(d, c))
{
file.next();
d = file.peek();
}
if (!match_char_impl<T,MODE>(d, c))
{
file.reset_pointer(push_scout);
break; // Next character has no possible match ahead.
}
file.next();
pattern.next();
}
if (path::is_separator(d))
{
// Wildcards don't match past a path separator.
depth = 0;
}
else
{
if (depth == _countof(pattern_stack))
return false; // Stack overflow.
pattern_stack[depth] = push_pattern;
file_stack[depth] = file.get_next_pointer();
path_component_stack[depth] = start_of_path_component;
depth++;
}
symbol_matched = true;
break; }
default: {
// Single character.
if (!d)
break;
if (path::is_separator(d))
start_of_path_component = true;
else if (d == '.' && start_of_path_component)
{
if (!final_file_component)
{
int x;
for (str_iter tmp(_pattern); x = tmp.peek(); tmp.next())
if (path::is_separator(x))
{
final_pattern_component = tmp.get_pointer() + 1;
has_final_wildcard = false;
}
else if (x == '?' || x == '*')
{
has_final_wildcard = true;
}
for (str_iter tmp(_file); x = tmp.peek(); tmp.next())
if (path::is_separator(x))
final_file_component = tmp.get_pointer() + 1;
if (!final_pattern_component)
final_pattern_component = _pattern.get_pointer();
if (!final_file_component)
final_file_component = _file.get_pointer();
}
if (has_final_wildcard &&
file.get_pointer() == final_file_component &&
pattern.get_pointer() == final_pattern_component &&
c != '.')
{
while (d == '.')
{
file.next();
d = file.peek();
}
}
}
if (!match_char_impl<T,MODE>(d, c))
break;
pattern.next();
file.next();
symbol_matched = true;
break; }
}

if (!symbol_matched)
{
if (depth)
{
// Backtrack.
depth--;
pattern.reset_pointer(pattern_stack[depth]);
file.reset_pointer(file_stack[depth]);
start_of_path_component = path_component_stack[depth];
continue;
}
return false;
}
}
}

//------------------------------------------------------------------------------
template <class T>
bool match_wild(const str_iter_impl<T>& pattern, const str_iter_impl<T>& file)
{
switch (str_compare_scope::current())
{
case str_compare_scope::relaxed: return match_wild_impl<T, 2>(pattern, file);
case str_compare_scope::caseless: return match_wild_impl<T, 1>(pattern, file);
default: return match_wild_impl<T, 0>(pattern, file);
}
}

//------------------------------------------------------------------------------
template <class T>
bool match_wild(const T* pattern, const T* file)
{
str_iter_impl<T> pattern_iter(pattern);
str_iter_impl<T> file_iter(file);
return match_wild(pattern_iter, file_iter);
}

//------------------------------------------------------------------------------
template <class T>
bool match_wild(const str_impl<T>& pattern, const str_impl<T>& file)
{
str_iter_impl<T> pattern_iter(pattern);
str_iter_impl<T> file_iter(file);
return match_wild(pattern_iter, file_iter);
}

}; // namespace path
26 changes: 26 additions & 0 deletions clink/core/include/core/str_iter.h
Expand Up @@ -12,7 +12,10 @@ class str_iter_impl
public:
explicit str_iter_impl(const T* s=(const T*)L"", int len=-1);
explicit str_iter_impl(const str_impl<T>& s, int len=-1);
str_iter_impl(const str_iter_impl<T>& i);
const T* get_pointer() const;
const T* get_next_pointer();
void reset_pointer(const T* ptr);
int peek();
int next();
bool more() const;
Expand All @@ -37,12 +40,35 @@ template <typename T> str_iter_impl<T>::str_iter_impl(const str_impl<T>& s, int
{
}

//------------------------------------------------------------------------------
template <typename T> str_iter_impl<T>::str_iter_impl(const str_iter_impl<T>& i)
: m_ptr(i.m_ptr)
, m_end(i.m_end)
{
}

//------------------------------------------------------------------------------
template <typename T> const T* str_iter_impl<T>::get_pointer() const
{
return m_ptr;
};

//------------------------------------------------------------------------------
template <typename T> const T* str_iter_impl<T>::get_next_pointer()
{
const T* ptr = m_ptr;
next();
const T* ret = m_ptr;
m_ptr = ptr;
return ret;
};

//------------------------------------------------------------------------------
template <typename T> void str_iter_impl<T>::reset_pointer(const T* ptr)
{
m_ptr = ptr;
}

//------------------------------------------------------------------------------
template <typename T> int str_iter_impl<T>::peek()
{
Expand Down
71 changes: 71 additions & 0 deletions clink/core/test/match_wild.cpp
@@ -0,0 +1,71 @@
// Copyright (c) 2020 Christopher Antos
// License: http://opensource.org/licenses/MIT

#include "pch.h"

#include <core/match_wild.h>

//------------------------------------------------------------------------------
TEST_CASE("path::match_wild()")
{
SECTION("Basic")
{
REQUIRE(!path::match_wild("a*/ghi", "abc/def/ghi"));

REQUIRE(path::match_wild("*foo*", "food"));
REQUIRE(path::match_wild("*foo*", "qfood"));
REQUIRE(path::match_wild("*foo*", "qfoo"));
REQUIRE(path::match_wild("*foo*bar", "foobar"));
REQUIRE(path::match_wild("*foo*bar", "foodbar"));
REQUIRE(path::match_wild("*foo*bar", "foodbar"));
REQUIRE(!path::match_wild("*foo*bar", "foodbard"));
REQUIRE(path::match_wild("*foo*bar", "build.foobar"));
REQUIRE(path::match_wild("*foo*bar", "build.foo123bar"));
REQUIRE(!path::match_wild("*foo*bar", "build.foo123bard"));
REQUIRE(!path::match_wild("*foo*bar", "build.fo123bar"));
REQUIRE(path::match_wild("build*.log", "build.foo.bar.log"));
REQUIRE(!path::match_wild("build*.log", "wmbuild.foo.bar.log"));
REQUIRE(path::match_wild("wmbuild*.log", "wmbuild.foo.bar.log"));
}

SECTION("Period")
{
REQUIRE(path::match_wild("bu*", "build"));
REQUIRE(path::match_wild("bu*", ".build"));
REQUIRE(path::match_wild("bu*", "..build"));

REQUIRE(!path::match_wild(".bu*", "build"));
REQUIRE(path::match_wild(".bu*", ".build"));
REQUIRE(!path::match_wild(".bu*", "..build"));

REQUIRE(!path::match_wild("abc/bu*", "build"));
REQUIRE(path::match_wild("abc/bu*", "abc/build"));
REQUIRE(!path::match_wild("abc/bu*", ".build"));
REQUIRE(path::match_wild("abc/bu*", "abc/.build"));
}

SECTION("Components")
{
REQUIRE(path::match_wild("abc/def/ghi", "abc/def/ghi"));
REQUIRE(path::match_wild("a*/def/ghi", "abc/def/ghi"));
REQUIRE(path::match_wild("a*/d?f/*i", "abc/def/ghi"));

REQUIRE(!path::match_wild("abc/def", "abc/def/build"));
REQUIRE(!path::match_wild("abc/def/?i*", "abc/def/build"));
REQUIRE(path::match_wild("abc/def/??i*", "abc/def/build"));
}

SECTION("Slashes")
{
REQUIRE(path::match_wild("abc/def/ghi", "abc\\def\\ghi"));

REQUIRE(!path::match_wild("a*/ghi", "abc/def/ghi"));
REQUIRE(path::match_wild("a*/*/ghi", "abc/def/ghi"));
REQUIRE(!path::match_wild("abc/*", "abc/def/ghi"));
REQUIRE(path::match_wild("abc/*", "abc/def"));

REQUIRE(!path::match_wild("abc/def", "abc/def/build"));
REQUIRE(!path::match_wild("abc/def/?i*", "abc/def/build"));
REQUIRE(path::match_wild("abc/def/??i*", "abc/def/build"));
}
}

0 comments on commit a648688

Please sign in to comment.