Clean up KeyChordSerialization (#10654)

This commit is a preparation for upcoming changes to
KeyChordSerialization for #7539 and #10203.  It introduces several
string helpers to simplify key chord parsing and get rid of our implicit
dependency on locale sensitive functions, which are known to behave
erratically.

Additionally key chord serialization used to depend on iteration order
of a hashmap which caused different strings to be returned for the same
key chord. This commit fixes the iteration order and will always return
the same string.

## Validation Steps Performed

* Key bindings are correctly parsed ✔️
* Key bindings are correctly serialized 
This commit is contained in:
Leonard Hecker 2021-07-14 23:22:24 +02:00 committed by GitHub
parent c12835783d
commit fca87b2bb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 328 additions and 207 deletions

View file

@ -77,6 +77,7 @@ llu
localtime
lround
LSHIFT
memicmp
mov
msappx
MULTIPLEUSE
@ -141,6 +142,7 @@ THEMECHANGED
tlg
tmp
tolower
toupper
TTask
TVal
UChar

View file

@ -114,6 +114,7 @@ Backgrounder
backgrounding
backport
backstory
barbaz
Batang
baz
Bazz
@ -794,6 +795,7 @@ FONTTYPE
FONTWEIGHT
FONTWIDTH
FONTWINDOW
fooo
forceinline
FORCEOFFFEEDBACK
FORCEONFEEDBACK
@ -1644,6 +1646,7 @@ oss
ostream
ostringstream
ouicompat
OUnter
outdir
outfile
Outof
@ -1986,6 +1989,7 @@ rhs
RIGHTALIGN
RIGHTBUTTON
riid
Rike
RIPMSG
RIS
RMENU
@ -2208,6 +2212,7 @@ stoi
stol
stoul
stoutapot
Stri
strikethrough
stringstream
STRINGTABLE

View file

@ -5,95 +5,75 @@
#include "KeyChordSerialization.h"
#include "KeyChordSerialization.g.cpp"
#include <til/static_map.h>
using namespace winrt::Microsoft::Terminal::Control;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
using namespace Microsoft::Terminal::Settings::Model::JsonUtils;
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
static constexpr std::wstring_view CTRL_KEY{ L"ctrl" };
static constexpr std::wstring_view SHIFT_KEY{ L"shift" };
static constexpr std::wstring_view ALT_KEY{ L"alt" };
static constexpr std::wstring_view WIN_KEY{ L"win" };
constexpr std::wstring_view CTRL_KEY{ L"ctrl" };
constexpr std::wstring_view SHIFT_KEY{ L"shift" };
constexpr std::wstring_view ALT_KEY{ L"alt" };
constexpr std::wstring_view WIN_KEY{ L"win" };
static constexpr int MAX_CHORD_PARTS = 5; // win+ctrl+alt+shift+key
// clang-format off
static const std::unordered_map<std::wstring_view, int32_t> vkeyNamePairs {
{ L"app" , VK_APPS },
{ L"backspace" , VK_BACK },
{ L"tab" , VK_TAB },
{ L"enter" , VK_RETURN },
{ L"esc" , VK_ESCAPE },
{ L"escape" , VK_ESCAPE },
{ L"menu" , VK_APPS },
{ L"space" , VK_SPACE },
{ L"pgup" , VK_PRIOR },
{ L"pageup" , VK_PRIOR },
{ L"pgdn" , VK_NEXT },
{ L"pagedown" , VK_NEXT },
{ L"end" , VK_END },
{ L"home" , VK_HOME },
{ L"left" , VK_LEFT },
{ L"up" , VK_UP },
{ L"right" , VK_RIGHT },
{ L"down" , VK_DOWN },
{ L"insert" , VK_INSERT },
{ L"delete" , VK_DELETE },
{ L"numpad_0" , VK_NUMPAD0 },
{ L"numpad0" , VK_NUMPAD0 },
{ L"numpad_1" , VK_NUMPAD1 },
{ L"numpad1" , VK_NUMPAD1 },
{ L"numpad_2" , VK_NUMPAD2 },
{ L"numpad2" , VK_NUMPAD2 },
{ L"numpad_3" , VK_NUMPAD3 },
{ L"numpad3" , VK_NUMPAD3 },
{ L"numpad_4" , VK_NUMPAD4 },
{ L"numpad4" , VK_NUMPAD4 },
{ L"numpad_5" , VK_NUMPAD5 },
{ L"numpad5" , VK_NUMPAD5 },
{ L"numpad_6" , VK_NUMPAD6 },
{ L"numpad6" , VK_NUMPAD6 },
{ L"numpad_7" , VK_NUMPAD7 },
{ L"numpad7" , VK_NUMPAD7 },
{ L"numpad_8" , VK_NUMPAD8 },
{ L"numpad8" , VK_NUMPAD8 },
{ L"numpad_9" , VK_NUMPAD9 },
{ L"numpad9" , VK_NUMPAD9 },
{ L"numpad_multiply" , VK_MULTIPLY },
{ L"numpad_plus" , VK_ADD },
{ L"numpad_add" , VK_ADD },
{ L"numpad_minus" , VK_SUBTRACT },
{ L"numpad_subtract" , VK_SUBTRACT },
{ L"numpad_period" , VK_DECIMAL },
{ L"numpad_decimal" , VK_DECIMAL },
{ L"numpad_divide" , VK_DIVIDE },
{ L"f1" , VK_F1 },
{ L"f2" , VK_F2 },
{ L"f3" , VK_F3 },
{ L"f4" , VK_F4 },
{ L"f5" , VK_F5 },
{ L"f6" , VK_F6 },
{ L"f7" , VK_F7 },
{ L"f8" , VK_F8 },
{ L"f9" , VK_F9 },
{ L"f10" , VK_F10 },
{ L"f11" , VK_F11 },
{ L"f12" , VK_F12 },
{ L"f13" , VK_F13 },
{ L"f14" , VK_F14 },
{ L"f15" , VK_F15 },
{ L"f16" , VK_F16 },
{ L"f17" , VK_F17 },
{ L"f18" , VK_F18 },
{ L"f19" , VK_F19 },
{ L"f20" , VK_F20 },
{ L"f21" , VK_F21 },
{ L"f22" , VK_F22 },
{ L"f23" , VK_F23 },
{ L"f24" , VK_F24 },
{ L"plus" , VK_OEM_PLUS }
};
// clang-format on
#define VKEY_NAME_PAIRS(XX) \
XX(VK_RETURN, L"enter") \
XX(VK_TAB, L"tab") \
XX(VK_SPACE, L"space") \
XX(VK_BACK, L"backspace") \
XX(VK_APPS, L"menu", L"app") \
XX(VK_INSERT, L"insert") \
XX(VK_DELETE, L"delete") \
XX(VK_HOME, L"home") \
XX(VK_END, L"end") \
XX(VK_NEXT, L"pgdn", L"pagedown") \
XX(VK_PRIOR, L"pgup", L"pageup") \
XX(VK_ESCAPE, L"esc", L"escape") \
XX(VK_LEFT, L"left") \
XX(VK_RIGHT, L"right") \
XX(VK_UP, L"up") \
XX(VK_DOWN, L"down") \
XX(VK_F1, L"f1") \
XX(VK_F2, L"f2") \
XX(VK_F3, L"f3") \
XX(VK_F4, L"f4") \
XX(VK_F5, L"f5") \
XX(VK_F6, L"f6") \
XX(VK_F7, L"f7") \
XX(VK_F8, L"f8") \
XX(VK_F9, L"f9") \
XX(VK_F10, L"f10") \
XX(VK_F11, L"f11") \
XX(VK_F12, L"f12") \
XX(VK_F13, L"f13") \
XX(VK_F14, L"f14") \
XX(VK_F15, L"f15") \
XX(VK_F16, L"f16") \
XX(VK_F17, L"f17") \
XX(VK_F18, L"f18") \
XX(VK_F19, L"f19") \
XX(VK_F20, L"f20") \
XX(VK_F21, L"f21") \
XX(VK_F22, L"f22") \
XX(VK_F23, L"f23") \
XX(VK_F24, L"f24") \
XX(VK_ADD, L"numpad_plus", L"numpad_add") \
XX(VK_SUBTRACT, L"numpad_minus", L"numpad_subtract") \
XX(VK_MULTIPLY, L"numpad_multiply") \
XX(VK_DIVIDE, L"numpad_divide") \
XX(VK_DECIMAL, L"numpad_period", L"numpad_decimal") \
XX(VK_NUMPAD0, L"numpad0", L"numpad_0") \
XX(VK_NUMPAD1, L"numpad1", L"numpad_1") \
XX(VK_NUMPAD2, L"numpad2", L"numpad_2") \
XX(VK_NUMPAD3, L"numpad3", L"numpad_3") \
XX(VK_NUMPAD4, L"numpad4", L"numpad_4") \
XX(VK_NUMPAD5, L"numpad5", L"numpad_5") \
XX(VK_NUMPAD6, L"numpad6", L"numpad_6") \
XX(VK_NUMPAD7, L"numpad7", L"numpad_7") \
XX(VK_NUMPAD8, L"numpad8", L"numpad_8") \
XX(VK_NUMPAD9, L"numpad9", L"numpad_9") \
XX(VK_OEM_PLUS, L"plus")
// Function Description:
// - Deserializes the given string into a new KeyChord instance. If this
@ -106,108 +86,93 @@ static const std::unordered_map<std::wstring_view, int32_t> vkeyNamePairs {
// - hstr: the string to parse into a keychord.
// Return Value:
// - a newly constructed KeyChord
static KeyChord _fromString(const std::wstring_view& wstr)
static KeyChord _fromString(std::wstring_view wstr)
{
// Split the string on '+'
std::wstring temp;
std::vector<std::wstring> parts;
std::wstringstream wss;
wss << wstr;
while (std::getline(wss, temp, L'+'))
{
parts.push_back(temp);
// If we have > 4, something's wrong.
if (parts.size() > MAX_CHORD_PARTS)
{
throw winrt::hresult_invalid_argument();
}
}
using nameToVkeyPair = std::pair<std::wstring_view, int32_t>;
static const til::static_map nameToVkey{
// The above VKEY_NAME_PAIRS macro contains a list of key-binding names for each virtual key.
// This god-awful macro inverts VKEY_NAME_PAIRS and creates a static map of key-binding names to virtual keys.
// clang-format off
#define GENERATOR_1(vkey, name1) nameToVkeyPair{ name1, vkey },
#define GENERATOR_2(vkey, name1, name2) nameToVkeyPair{ name1, vkey }, nameToVkeyPair{ name2, vkey },
#define GENERATOR_3(vkey, name1, name2, name3) nameToVkeyPair{ name1, vkey }, nameToVkeyPair{ name2, vkey }, nameToVkeyPair{ name3, vkey },
#define GENERATOR_N(vkey, name1, name2, name3, MACRO, ...) MACRO
#define GENERATOR(...) GENERATOR_N(__VA_ARGS__, GENERATOR_3, GENERATOR_2, GENERATOR_1)(__VA_ARGS__)
VKEY_NAME_PAIRS(GENERATOR)
#undef GENERATOR_1
#undef GENERATOR_2
#undef GENERATOR_3
#undef GENERATOR_N
#undef GENERATOR
// clang-format on
};
VirtualKeyModifiers modifiers = VirtualKeyModifiers::None;
int32_t vkey = 0;
// Look for ctrl, shift, alt. Anything else might be a key
for (const auto& part : parts)
while (!wstr.empty())
{
std::wstring lowercase = part;
std::transform(lowercase.begin(), lowercase.end(), lowercase.begin(), std::towlower);
if (lowercase == CTRL_KEY)
const auto part = til::prefix_split(wstr, L"+");
if (til::equals_insensitive_ascii(part, CTRL_KEY))
{
modifiers |= VirtualKeyModifiers::Control;
}
else if (lowercase == ALT_KEY)
else if (til::equals_insensitive_ascii(part, ALT_KEY))
{
modifiers |= VirtualKeyModifiers::Menu;
}
else if (lowercase == SHIFT_KEY)
else if (til::equals_insensitive_ascii(part, SHIFT_KEY))
{
modifiers |= VirtualKeyModifiers::Shift;
}
else if (lowercase == WIN_KEY)
else if (til::equals_insensitive_ascii(part, WIN_KEY))
{
modifiers |= VirtualKeyModifiers::Windows;
}
else
{
bool foundKey = false;
// For potential keys, look through the pairs of strings and vkeys
if (vkey)
{
// Key bindings like Ctrl+A+B are not valid.
throw winrt::hresult_invalid_argument();
}
// Characters 0-9, a-z, A-Z directly map to virtual keys.
if (part.size() == 1)
{
const wchar_t wch = part.at(0);
// Quick lookup: ranges of vkeys that correlate directly to a key.
if (wch >= L'0' && wch <= L'9')
const auto wch = til::toupper_ascii(part[0]);
if ((wch >= L'0' && wch <= L'9') || (wch >= L'A' && wch <= L'Z'))
{
vkey = static_cast<int32_t>(wch);
foundKey = true;
}
else if (wch >= L'a' && wch <= L'z')
{
// subtract 0x20 to shift to uppercase
vkey = static_cast<int32_t>(wch - 0x20);
foundKey = true;
}
else if (wch >= L'A' && wch <= L'Z')
{
vkey = static_cast<int32_t>(wch);
foundKey = true;
continue;
}
}
// If we didn't find the key with a quick lookup, search the
// table to see if we have a matching name.
if (!foundKey && vkeyNamePairs.find(part) != vkeyNamePairs.end())
// nameToVkey contains a few more mappings like "F11".
if (const auto it = nameToVkey.find(part); it != nameToVkey.end())
{
vkey = vkeyNamePairs.at(part);
foundKey = true;
break;
vkey = it->second;
continue;
}
// If we haven't found a key, attempt a keyboard mapping
if (!foundKey && part.size() == 1)
if (part.size() == 1)
{
auto oemVk = VkKeyScanW(part[0]);
const auto oemVk = VkKeyScanW(part[0]);
if (oemVk != -1)
{
vkey = oemVk & 0xFF;
auto oemModifiers = (oemVk & 0xFF00) >> 8;
// We're using WI_SetFlagIf instead of WI_UpdateFlag because we want to be strictly additive
// to the user's specified modifiers. ctrl+| should be the same as ctrl+shift+\,
// but if we used WI_UpdateFlag, ctrl+shift+\ would turn _off_ Shift because \ doesn't
// require it.
vkey = oemVk & 0xff;
const auto oemModifiers = oemVk >> 8;
// NOTE: WI_UpdateFlag _replaces_ a bit. This code _adds_ a bit.
WI_SetFlagIf(modifiers, VirtualKeyModifiers::Shift, WI_IsFlagSet(oemModifiers, 1U));
WI_SetFlagIf(modifiers, VirtualKeyModifiers::Control, WI_IsFlagSet(oemModifiers, 2U));
WI_SetFlagIf(modifiers, VirtualKeyModifiers::Menu, WI_IsFlagSet(oemModifiers, 4U));
foundKey = true;
continue;
}
}
// If we weren't able to find a match, throw an exception.
if (!foundKey)
{
throw winrt::hresult_invalid_argument();
}
throw winrt::hresult_invalid_argument();
}
}
@ -223,82 +188,67 @@ static KeyChord _fromString(const std::wstring_view& wstr)
// - a string which is an equivalent serialization of this object.
static std::wstring _toString(const KeyChord& chord)
{
using vkeyToNamePair = std::pair<int32_t, std::wstring_view>;
static const til::static_map vkeyToName{
// The above VKEY_NAME_PAIRS macro contains a list of key-binding strings for each virtual key.
// This macro picks the first (most preferred) name and creates a static map of virtual keys to key-binding names.
#define GENERATOR(vkey, name1, ...) vkeyToNamePair{ vkey, name1 },
VKEY_NAME_PAIRS(GENERATOR)
#undef GENERATOR
};
if (!chord)
{
return {};
}
bool serializedSuccessfully = false;
const auto modifiers = chord.Modifiers();
const auto vkey = chord.Vkey();
std::wstring buffer{ L"" };
std::wstring buffer;
// Add modifiers
if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows))
{
buffer += WIN_KEY;
buffer += L"+";
buffer.append(WIN_KEY);
buffer.push_back(L'+');
}
if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control))
{
buffer += CTRL_KEY;
buffer += L"+";
buffer.append(CTRL_KEY);
buffer.push_back(L'+');
}
if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu))
{
buffer += ALT_KEY;
buffer += L"+";
buffer.append(ALT_KEY);
buffer.push_back(L'+');
}
if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift))
{
buffer += SHIFT_KEY;
buffer += L"+";
buffer.append(SHIFT_KEY);
buffer.push_back(L'+');
}
// Quick lookup: ranges of vkeys that correlate directly to a key.
if (vkey >= L'0' && vkey <= L'9')
if ((vkey >= L'0' && vkey <= L'9') || (vkey >= L'A' && vkey <= L'Z'))
{
buffer += std::wstring(1, static_cast<wchar_t>(vkey));
serializedSuccessfully = true;
}
else if (vkey >= L'A' && vkey <= L'Z')
{
// add 0x20 to shift to lowercase
buffer += std::wstring(1, static_cast<wchar_t>(vkey + 0x20));
serializedSuccessfully = true;
}
else
{
bool foundKey = false;
for (const auto& pair : vkeyNamePairs)
{
if (pair.second == vkey)
{
buffer += pair.first;
serializedSuccessfully = true;
foundKey = true;
break;
}
}
if (!foundKey)
{
auto mappedChar = MapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR);
if (mappedChar != 0)
{
wchar_t mappedWch = gsl::narrow_cast<wchar_t>(mappedChar);
buffer += std::wstring_view{ &mappedWch, 1 };
serializedSuccessfully = true;
}
}
buffer.push_back(til::tolower_ascii(gsl::narrow_cast<wchar_t>(vkey)));
return buffer;
}
if (!serializedSuccessfully)
if (const auto it = vkeyToName.find(vkey); it != vkeyToName.end())
{
buffer = L"";
buffer.append(it->second);
return buffer;
}
return buffer;
const auto mappedChar = MapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR);
if (mappedChar != 0)
{
buffer.push_back(gsl::narrow_cast<wchar_t>(mappedChar));
return buffer;
}
return {};
}
KeyChord KeyChordSerialization::FromString(const winrt::hstring& hstr)

View file

@ -47,7 +47,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
if constexpr (!SortedInput::value)
{
const auto compareKeys = [&](const auto& p1, const auto& p2) { return _predicate(p1.first, p2.first); };
std::sort(_array.begin(), _array.end(), compareKeys); // compile-time sorting!
std::sort(_array.begin(), _array.end(), compareKeys); // compile-time sorting starting C++20
}
}

View file

@ -30,27 +30,29 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return visualize_control_codes(std::wstring{ str });
}
// std::string_view::starts_with support for C++17.
template<typename T, typename Traits>
constexpr bool starts_with(const std::basic_string_view<T, Traits> str, const std::basic_string_view<T, Traits> prefix) noexcept
constexpr bool starts_with(const std::basic_string_view<T, Traits>& str, const std::basic_string_view<T, Traits>& prefix) noexcept
{
#ifdef __cpp_lib_starts_ends_with
#error This code can be replaced in C++20, which natively supports .starts_with().
#endif
return str.size() >= prefix.size() && Traits::compare(str.data(), prefix.data(), prefix.size()) == 0;
};
}
constexpr bool starts_with(const std::string_view str, const std::string_view prefix) noexcept
constexpr bool starts_with(const std::string_view& str, const std::string_view& prefix) noexcept
{
return starts_with<>(str, prefix);
};
}
constexpr bool starts_with(const std::wstring_view str, const std::wstring_view prefix) noexcept
constexpr bool starts_with(const std::wstring_view& str, const std::wstring_view& prefix) noexcept
{
return starts_with<>(str, prefix);
};
}
// std::string_view::ends_with support for C++17.
template<typename T, typename Traits>
constexpr bool ends_with(const std::basic_string_view<T, Traits> str, const std::basic_string_view<T, Traits> prefix) noexcept
constexpr bool ends_with(const std::basic_string_view<T, Traits>& str, const std::basic_string_view<T, Traits>& prefix) noexcept
{
#ifdef __cpp_lib_ends_ends_with
#error This code can be replaced in C++20, which natively supports .ends_with().
@ -59,15 +61,110 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
return str.size() >= prefix.size() && Traits::compare(str.data() + (str.size() - prefix.size()), prefix.data(), prefix.size()) == 0;
#pragma warning(pop)
};
}
constexpr bool ends_with(const std::string_view str, const std::string_view prefix) noexcept
constexpr bool ends_with(const std::string_view& str, const std::string_view& prefix) noexcept
{
return ends_with<>(str, prefix);
};
}
constexpr bool ends_with(const std::wstring_view str, const std::wstring_view prefix) noexcept
constexpr bool ends_with(const std::wstring_view& str, const std::wstring_view& prefix) noexcept
{
return ends_with<>(str, prefix);
};
}
// Just like std::tolower, but without annoying locales.
template<typename T>
constexpr T tolower_ascii(T c)
{
if ((c >= 'A') && (c <= 'Z'))
{
c |= 0x20;
}
return c;
}
// Just like std::toupper, but without annoying locales.
template<typename T>
constexpr T toupper_ascii(T c)
{
if ((c >= 'a') && (c <= 'z'))
{
c &= ~0x20;
}
return c;
}
// Just like _memicmp, but without annoying locales.
template<typename T, typename Traits>
bool equals_insensitive_ascii(const std::basic_string_view<T, Traits>& str1, const std::basic_string_view<T, Traits>& str2) noexcept
{
if (str1.size() != str2.size())
{
return false;
}
#pragma warning(push)
#pragma warning(disable : 26429) // Symbol 'data1' is never tested for nullness, it can be marked as not_null
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead
auto remaining = str1.size();
auto data1 = str1.data();
auto data2 = str2.data();
for (; remaining; --remaining, ++data1, ++data2)
{
if (*data1 != *data2 && tolower_ascii(*data1) != tolower_ascii(*data2))
{
return false;
}
}
#pragma warning(pop)
return true;
}
inline bool equals_insensitive_ascii(const std::string_view& str1, const std::string_view& str2) noexcept
{
return equals_insensitive_ascii<>(str1, str2);
}
inline bool equals_insensitive_ascii(const std::wstring_view& str1, const std::wstring_view& str2) noexcept
{
return equals_insensitive_ascii<>(str1, str2);
}
// Give the arguments ("foo bar baz", " "), this method will
// * modify the first argument to "bar baz"
// * return "foo"
// If the needle cannot be found the "str" argument is returned as is.
template<typename T, typename Traits>
std::basic_string_view<T, Traits> prefix_split(std::basic_string_view<T, Traits>& str, const std::basic_string_view<T, Traits>& needle) noexcept
{
using view_type = std::basic_string_view<T, Traits>;
const auto idx = str.find(needle);
// > If the needle cannot be found the "str" argument is returned as is.
// ...but if needle is empty, idx will always be npos, forcing us to return str.
if (idx == view_type::npos || needle.empty())
{
return std::exchange(str, {});
}
const auto suffixIdx = idx + needle.size();
const view_type result{ str.data(), idx };
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead
str = { str.data() + suffixIdx, str.size() - suffixIdx };
return result;
}
inline std::string_view prefix_split(std::string_view& str, const std::string_view& needle) noexcept
{
return prefix_split<>(str, needle);
}
inline std::wstring_view prefix_split(std::wstring_view& str, const std::wstring_view& needle) noexcept
{
return prefix_split<>(str, needle);
}
}

View file

@ -19,7 +19,7 @@ class StringTests
VERIFY_ARE_EQUAL(expected, actual);
}
TEST_METHOD(StartsWith)
TEST_METHOD(starts_with)
{
VERIFY_IS_TRUE(til::starts_with("", ""));
@ -36,7 +36,7 @@ class StringTests
VERIFY_IS_TRUE(til::starts_with("abcd", "abc"));
}
TEST_METHOD(EndsWith)
TEST_METHOD(ends_with)
{
VERIFY_IS_TRUE(til::ends_with("", ""));
@ -52,4 +52,71 @@ class StringTests
VERIFY_IS_TRUE(til::ends_with("abc", "abc"));
VERIFY_IS_TRUE(til::ends_with("0abc", "abc"));
}
TEST_METHOD(tolower_ascii)
{
for (wchar_t ch = 0; ch < 128; ++ch)
{
VERIFY_ARE_EQUAL(std::towlower(ch), til::tolower_ascii(ch));
}
}
TEST_METHOD(toupper_ascii)
{
for (wchar_t ch = 0; ch < 128; ++ch)
{
VERIFY_ARE_EQUAL(std::towupper(ch), til::toupper_ascii(ch));
}
}
TEST_METHOD(equals_insensitive_ascii)
{
VERIFY_IS_TRUE(til::equals_insensitive_ascii("", ""));
VERIFY_IS_FALSE(til::equals_insensitive_ascii("", "foo"));
VERIFY_IS_FALSE(til::equals_insensitive_ascii("foo", "fo"));
VERIFY_IS_FALSE(til::equals_insensitive_ascii("fooo", "foo"));
VERIFY_IS_TRUE(til::equals_insensitive_ascii("cOUnterStriKE", "COuntERStRike"));
}
TEST_METHOD(prefix_split)
{
{
std::string_view s{ "" };
VERIFY_ARE_EQUAL("", til::prefix_split(s, ""));
VERIFY_ARE_EQUAL("", s);
}
{
std::string_view s{ "" };
VERIFY_ARE_EQUAL("", til::prefix_split(s, " "));
VERIFY_ARE_EQUAL("", s);
}
{
std::string_view s{ " " };
VERIFY_ARE_EQUAL(" ", til::prefix_split(s, ""));
VERIFY_ARE_EQUAL("", s);
}
{
std::string_view s{ "foo" };
VERIFY_ARE_EQUAL("foo", til::prefix_split(s, ""));
VERIFY_ARE_EQUAL("", s);
}
{
std::string_view s{ "foo bar baz" };
VERIFY_ARE_EQUAL("foo", til::prefix_split(s, " "));
VERIFY_ARE_EQUAL("bar baz", s);
VERIFY_ARE_EQUAL("bar", til::prefix_split(s, " "));
VERIFY_ARE_EQUAL("baz", s);
VERIFY_ARE_EQUAL("baz", til::prefix_split(s, " "));
VERIFY_ARE_EQUAL("", s);
}
{
std::string_view s{ "foo123barbaz123" };
VERIFY_ARE_EQUAL("foo", til::prefix_split(s, "123"));
VERIFY_ARE_EQUAL("barbaz123", s);
VERIFY_ARE_EQUAL("barbaz", til::prefix_split(s, "123"));
VERIFY_ARE_EQUAL("", s);
VERIFY_ARE_EQUAL("", til::prefix_split(s, ""));
VERIFY_ARE_EQUAL("", s);
}
}
};