diff --git a/src/buffer/out/UTextAdapter.cpp b/src/buffer/out/UTextAdapter.cpp index 7dec8d2dc04..6f07ce9c534 100644 --- a/src/buffer/out/UTextAdapter.cpp +++ b/src/buffer/out/UTextAdapter.cpp @@ -327,3 +327,18 @@ til::point_span Microsoft::Console::ICU::BufferRangeFromMatch(UText* ut, URegula return ret; } + +UText Microsoft::Console::ICU::UTextForWrappableRow(const TextBuffer& textBuffer, til::CoordType& row) noexcept +{ + const auto startRow = row; + auto length = 0; + while (textBuffer.GetRowByOffset(row).WasWrapForced()) + { + row++; + length += textBuffer.GetRowByOffset(row).size(); + } + length += textBuffer.GetRowByOffset(row).size(); + const auto ut = UTextFromTextBuffer(textBuffer, startRow, row + 1); + + return ut; +} diff --git a/src/buffer/out/UTextAdapter.h b/src/buffer/out/UTextAdapter.h index c8c325143ef..73bf4048c4b 100644 --- a/src/buffer/out/UTextAdapter.h +++ b/src/buffer/out/UTextAdapter.h @@ -12,6 +12,7 @@ namespace Microsoft::Console::ICU using unique_uregex = wistd::unique_ptr>; UText UTextFromTextBuffer(const TextBuffer& textBuffer, til::CoordType rowBeg, til::CoordType rowEnd) noexcept; + UText UTextForWrappableRow(const TextBuffer& textBuffer, til::CoordType& row) noexcept; unique_uregex CreateRegex(const std::wstring_view& pattern, uint32_t flags, UErrorCode* status) noexcept; til::point_span BufferRangeFromMatch(UText* ut, URegularExpression* re); } diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index d044cbe13f2..f82684ca723 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -6,6 +6,7 @@ #include "TerminalPage.h" #include "ScratchpadContent.h" +#include "FuzzySearchPane.h" #include "../WinRTUtils/inc/WtExeUtils.h" #include "../../types/inc/utils.hpp" #include "Utils.h" @@ -1467,6 +1468,25 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleOpenFuzzySearch(const IInspectable& sender, + const ActionEventArgs& args) + { + if (Feature_FuzzySearch::IsEnabled()) + { + const auto& fuzzySearchPane{ winrt::make_self(_GetActiveControl()) }; + + // This is maybe a little wacky - add our key event handler to the pane + // we made. So that we can get actions for keys that the content didn't + // handle. + fuzzySearchPane->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); + + const auto resultPane = std::make_shared(*fuzzySearchPane); + _SplitPane(_senderOrFocusedTab(sender), SplitDirection::Automatic, 0.5f, resultPane); + fuzzySearchPane->Focus(); + args.Handled(true); + } + } + void TerminalPage::_HandleOpenAbout(const IInspectable& /*sender*/, const ActionEventArgs& args) { diff --git a/src/cascadia/TerminalApp/FuzzySearchPane.cpp b/src/cascadia/TerminalApp/FuzzySearchPane.cpp new file mode 100644 index 00000000000..5c1d4eb6df8 --- /dev/null +++ b/src/cascadia/TerminalApp/FuzzySearchPane.cpp @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "FuzzySearchPane.h" + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Microsoft::Terminal::Settings::Model; + +namespace winrt::TerminalApp::implementation +{ + FuzzySearchPane::FuzzySearchPane(const winrt::Microsoft::Terminal::Control::TermControl& control) + { + _control = control; + _root = winrt::Windows::UI::Xaml::Controls::Grid{}; + // Vertical and HorizontalAlignment are Stretch by default + + auto res = Windows::UI::Xaml::Application::Current().Resources(); + auto bg = res.Lookup(winrt::box_value(L"UnfocusedBorderBrush")); + _root.Background(bg.try_as()); + + const Controls::RowDefinition row1; + row1.Height(GridLengthHelper::FromValueAndType(1, GridUnitType::Star)); + _root.RowDefinitions().Append(row1); + + const Controls::RowDefinition row2; + row2.Height(GridLengthHelper::Auto()); + _root.RowDefinitions().Append(row2); + + _listBox = Controls::ListBox{}; + _listBox.Margin({ 10, 10, 10, 10 }); + _root.Children().Append(_listBox); + + _searchBox = Controls::TextBox{}; + _root.Children().Append(_searchBox); + + _searchBox.TextChanged({ this, &FuzzySearchPane::OnTextChanged }); + _searchBox.KeyDown({ this, &FuzzySearchPane::OnKeyUp }); + + Controls::Grid::SetRow(_listBox, 0); + Controls::Grid::SetRow(_searchBox, 1); + } + + void FuzzySearchPane::OnKeyUp(Windows::Foundation::IInspectable const&, Input::KeyRoutedEventArgs const& e) + { + if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Down || e.OriginalKey() == winrt::Windows::System::VirtualKey::Up) + { + auto selectedIndex = _listBox.SelectedIndex(); + + if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Down) + { + selectedIndex++; + } + else if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Up) + { + selectedIndex--; + } + + if (selectedIndex >= 0 && selectedIndex < static_cast(_listBox.Items().Size())) + { + _listBox.SelectedIndex(selectedIndex); + _listBox.ScrollIntoView(Controls::ListBox().SelectedItem()); + } + + e.Handled(true); + } + else if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Enter) + { + if (const auto selectedItem = _listBox.SelectedItem()) + { + if (const auto listBoxItem = selectedItem.try_as()) + { + if (const auto fuzzyMatch = listBoxItem.DataContext().try_as()) + { + _control.SelectChar(fuzzyMatch.FirstPosition()); + _control.Focus(FocusState::Programmatic); + e.Handled(true); + } + } + } + } + else if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Escape) + { + Close(); + e.Handled(true); + } + } + + void FuzzySearchPane::OnTextChanged(Windows::Foundation::IInspectable const&, Controls::TextChangedEventArgs const&) const + { + _listBox.Items().Clear(); + const auto needle = _searchBox.Text(); + const auto fuzzySearchResult = _control.FuzzySearch(needle); + if (fuzzySearchResult.NumberOfResults() > 0) + { + for (auto fuzzyMatch : fuzzySearchResult.Results()) + { + auto control = Controls::TextBlock{}; + const auto inlinesCollection = control.Inlines(); + inlinesCollection.Clear(); + + for (const auto& match : fuzzyMatch.Segments()) + { + const auto matchText = match.TextSegment(); + const auto fontWeight = match.IsHighlighted() ? Windows::UI::Text::FontWeights::Bold() : Windows::UI::Text::FontWeights::Normal(); + + Media::SolidColorBrush foregroundBrush; + + if (match.IsHighlighted()) + { + foregroundBrush.Color(Windows::UI::Colors::OrangeRed()); + } + else + { + foregroundBrush.Color(Windows::UI::Colors::White()); + } + + Documents::Run run; + run.Text(matchText); + run.FontWeight(fontWeight); + run.Foreground(foregroundBrush); + inlinesCollection.Append(run); + } + auto lbi = Controls::ListBoxItem(); + lbi.DataContext(fuzzyMatch); + lbi.Content(control); + _listBox.Items().Append(lbi); + } + _listBox.SelectedIndex(0); + } + } + + void FuzzySearchPane::UpdateSettings(const CascadiaSettings& /*settings*/) + { + // Nothing to do. + } + + winrt::Windows::UI::Xaml::FrameworkElement FuzzySearchPane::GetRoot() + { + return _root; + } + winrt::Windows::Foundation::Size FuzzySearchPane::MinimumSize() + { + return { 1, 1 }; + } + void FuzzySearchPane::Focus(winrt::Windows::UI::Xaml::FocusState reason) + { + _searchBox.Focus(reason); + } + void FuzzySearchPane::Close() + { + CloseRequested.raise(*this, nullptr); + } + + INewContentArgs FuzzySearchPane::GetNewTerminalArgs(const BuildStartupKind /* kind */) const + { + return BaseContentArgs(L"scratchpad"); + } + + winrt::hstring FuzzySearchPane::Icon() const + { + static constexpr std::wstring_view glyph{ L"\xe70b" }; // QuickNote + return winrt::hstring{ glyph }; + } + + winrt::Windows::UI::Xaml::Media::Brush FuzzySearchPane::BackgroundBrush() + { + return _root.Background(); + } +} diff --git a/src/cascadia/TerminalApp/FuzzySearchPane.h b/src/cascadia/TerminalApp/FuzzySearchPane.h new file mode 100644 index 00000000000..be177ed064d --- /dev/null +++ b/src/cascadia/TerminalApp/FuzzySearchPane.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +#include "winrt/TerminalApp.h" + +namespace winrt::TerminalApp::implementation +{ + class FuzzySearchPane : public winrt::implements + { + public: + FuzzySearchPane(const winrt::Microsoft::Terminal::Control::TermControl& control); + void TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/); + void OnTextChanged(Windows::Foundation::IInspectable const&, Windows::UI::Xaml::Controls::TextChangedEventArgs const&) const; + void OnKeyUp(Windows::Foundation::IInspectable const&, Windows::UI::Xaml::Input::KeyRoutedEventArgs const&); + + winrt::Windows::UI::Xaml::FrameworkElement GetRoot(); + + void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); + + winrt::Windows::Foundation::Size MinimumSize(); + + void Focus(winrt::Windows::UI::Xaml::FocusState reason = winrt::Windows::UI::Xaml::FocusState::Programmatic); + void Close(); + winrt::Microsoft::Terminal::Settings::Model::INewContentArgs GetNewTerminalArgs(BuildStartupKind kind) const; + + winrt::hstring Title() { return L"FuzzySearch"; } + uint64_t TaskbarState() { return 0; } + uint64_t TaskbarProgress() { return 0; } + bool ReadOnly() { return false; } + winrt::hstring Icon() const; + Windows::Foundation::IReference TabColor() const noexcept { return nullptr; } + winrt::Windows::UI::Xaml::Media::Brush BackgroundBrush(); + + til::typed_event<> ConnectionStateChanged; + til::typed_event CloseRequested; + til::typed_event BellRequested; + til::typed_event TitleChanged; + til::typed_event TabColorChanged; + til::typed_event TaskbarProgressChanged; + til::typed_event ReadOnlyChanged; + til::typed_event FocusRequested; + + private: + winrt::Windows::UI::Xaml::Controls::Grid _root{ nullptr }; + winrt::Windows::UI::Xaml::Controls::ListBox _listBox{ nullptr }; + winrt::Windows::UI::Xaml::Controls::TextBox _searchBox{ nullptr }; + winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr }; + }; +} diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index d0629da8036..c576991e237 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -161,6 +161,9 @@ TerminalPaneContent.idl + + TerminalPaneContent.idl + TerminalPaneContent.idl @@ -274,6 +277,9 @@ TerminalPaneContent.idl + + TerminalPaneContent.idl + TerminalPaneContent.idl @@ -400,7 +406,6 @@ {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F} - - - 3 nested - - true true - - + + + FuzzySearchTextSegment.idl + + ControlCore.idl @@ -71,6 +71,13 @@ + + + FuzzySearchTextSegment.idl + + + NotUsing + Create @@ -114,6 +121,7 @@ + @@ -183,9 +191,7 @@ - - diff --git a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj.filters b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj.filters new file mode 100644 index 00000000000..4b590d8c8ec --- /dev/null +++ b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj.filters @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + fzf + + + + + + + + + + + fzf + + + + + + + + + + + + + + + + + + + + + + + + + + + {39e45a3e-ceef-4fcf-96cd-bed5bfdacb8b} + + + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/fzf/LICENSE b/src/cascadia/TerminalControl/fzf/LICENSE new file mode 100644 index 00000000000..57a10051e01 --- /dev/null +++ b/src/cascadia/TerminalControl/fzf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Simon Hauser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/cascadia/TerminalControl/fzf/fzf.c b/src/cascadia/TerminalControl/fzf/fzf.c new file mode 100644 index 00000000000..ee1058e2c74 --- /dev/null +++ b/src/cascadia/TerminalControl/fzf/fzf.c @@ -0,0 +1,1428 @@ +#include "fzf.h" + +#include +#include +#include +#include + +#pragma warning(push) +#pragma warning(disable : 4100) + +// TODO(conni2461): UNICODE HEADER +#define UNICODE_MAXASCII 0x7f + +#define SFREE(x) \ + if (x) { \ + free(x); \ + } + +/* Helpers */ +#define free_alloc(obj) \ + if ((obj).allocated) { \ + free((obj).data); \ + } + +#define gen_simple_slice(name, type) \ + typedef struct { \ + type *data; \ + size_t size; \ + } name##_slice_t; \ + static name##_slice_t slice_##name(type *input, size_t from, size_t to) { \ + return (name##_slice_t){.data = input + from, .size = to - from}; \ + } + +#define gen_slice(name, type) \ + gen_simple_slice(name, type); \ + static name##_slice_t slice_##name##_right(type *input, size_t to) { \ + return slice_##name(input, 0, to); \ + } + +gen_slice(i16, int16_t); +gen_simple_slice(i32, int32_t); +gen_slice(str, const char); +gen_slice(ustr, const UChar); +#undef gen_slice +#undef gen_simple_slice + +/* TODO(conni2461): additional types (utf8) */ +typedef int32_t char_class; +//typedef char byte; + +typedef enum { + ScoreMatch = 16, + ScoreGapStart = -3, + ScoreGapExtention = -1, + BonusBoundary = ScoreMatch / 2, + BonusNonWord = ScoreMatch / 2, + BonusCamel123 = BonusBoundary + ScoreGapExtention, + BonusConsecutive = -(ScoreGapStart + ScoreGapExtention), + BonusFirstCharMultiplier = 2, +} score_t; + +typedef enum { + UCharNonWord = 0, + UCharLower, + UCharUpper, + UCharLetter, + UCharNumber +} char_types; + + +typedef struct +{ + UText* utext; + int32_t start; + int32_t end; // Exclusive + size_t size; +} UTextSlice; + +void u_text_slice_init(UTextSlice* dest, UText* uText, int32_t start, int32_t end) +{ + dest->utext = uText; + dest->start = start; + dest->end = end; + dest->size = end - start; +} + +void u_text_slice(UTextSlice* dest, UTextSlice* slice, int32_t start, int32_t end) +{ + dest->utext = slice->utext; + dest->start = slice->start + start; + dest->end = slice->start + end; + dest->size = dest->end - dest->start; +} + +void u_text_slice_right(UTextSlice* dest, UTextSlice* slice, int32_t end) +{ + u_text_slice(dest, slice, 0, end); +} + +UChar32 utextSlice_charAt(const UTextSlice* slice, int64_t index) +{ + if (index < 0 || index >= (slice->end - slice->start)) + { + return U_SENTINEL; + } + UChar32 result = utext_char32At(slice->utext, slice->start + index); + return result; +} + +static int32_t uindex_byte(UTextSlice *string, UChar32 b) { + for (size_t i = 0; i < string->size; i++) { + if (utextSlice_charAt(string, i) == b) { + return (int32_t)i; + } + } + return -1; +} + +static size_t uleading_whitespaces(UText* utext) +{ + size_t whitespaces = 0; + //UErrorCode status = U_ZERO_ERROR; + + // Ensure the UText is properly initialized and ready for use + // This includes opening it on the specific text if not already done + + int64_t length = utext_nativeLength(utext); + + for (int64_t i = 0; i < length; ++i) + { + UChar32 ch = utext_char32At(utext, i); + if (!u_isUWhiteSpace(ch)) + { + break; + } + whitespaces++; + // Advance i to skip over code points that are represented by more than one unit in UTF-16 + if (U_IS_SUPPLEMENTARY(ch)) + { + i++; + } + } + + return whitespaces; +} + +static size_t utrailing_whitespaces(UText* utext) +{ + size_t whitespaces = 0; + + int64_t length = utext_nativeLength(utext); + + for (int64_t i = length - 1; i >= 0; --i) + { + UChar32 ch = utext_char32At(utext, i); + if (!u_isUWhiteSpace(ch)) + { + break; + } + whitespaces++; + // When a supplementary character is encountered, adjust the index accordingly + if (U_IS_SUPPLEMENTARY(ch)) + { + // Prevents skipping an extra code unit since the loop will decrement i again + if (i > 0) + i--; + } + } + + return whitespaces; +} + +static void copy_into_i16(i16_slice_t *src, fzf_i16_t *dest) { + for (size_t i = 0; i < src->size; i++) { + dest->data[i] = src->data[i]; + } +} + +static UChar *utrim_whitespace_left(UChar *str, size_t *len) { + for (size_t i = 0; i < *len; i++) { + if (str[0] == 0x0020) { // Check for space character in UTF-16 + (*len)--; + str++; + } else { + break; + } + } + return str; +} + +static bool uhas_prefix(const UChar* str, const UChar *prefix, int32_t prefix_len) +{ + return u_strncmp(prefix, str, prefix_len) == 0; +} + +static bool uhas_suffix(const UChar *str, size_t len, const UChar *suffix, size_t suffix_len) { + if (len < suffix_len) { + return false; + } + + const UChar *str_suffix_start = str + (len - suffix_len); + return u_memcmp(str_suffix_start, suffix, (int32_t)suffix_len) == 0; +} + +static UChar* ustr_replace_char(UChar* str, UChar find, UChar replace) +{ + UChar* current_pos = str; + while (*current_pos != 0) + { + if (*current_pos == find) + { + *current_pos = replace; + } + ++current_pos; + } + return str; +} + +static UChar *ustr_replace(const UChar *orig, const UChar *rep, const UChar *with) { + if (!orig || !rep || !with) { + return NULL; + } + + int32_t len_rep = u_strlen(rep); + int32_t len_with = u_strlen(with); + int32_t len_orig = u_strlen(orig); + + // Temporary variables for the replacement process + const UChar *ins = orig; // Current position in the original string + const UChar *tmp; + int32_t count = 0; // Number of replacements + + // First pass: count the number of replacements needed + while ((tmp = u_strstr(ins, rep)) != NULL) { + count++; + ins = tmp + len_rep; + } + + // Allocate memory for the new string + int32_t new_len = len_orig + (len_with - len_rep) * count; + UChar* result = (UChar*)malloc((new_len + 1) * sizeof(UChar)); + if (!result) { + return NULL; + } + + UChar *current_pos = result; + while (count--) { + tmp = u_strstr(orig, rep); // Find next occurrence of rep + int32_t len_front = (int32_t)(tmp - orig); // Length before the rep + u_strncpy(current_pos, orig, len_front); // Copy the part before rep + current_pos += len_front; + u_strcpy(current_pos, with); // Replace rep with with + current_pos += len_with; + orig += len_front + len_rep; // Move past the replaced part + } + + // Copy the remaining part of the original string + u_strcpy(current_pos, orig); + + return result; +} + +static int16_t max16(int16_t a, int16_t b) { + return (a > b) ? a : b; +} + +static size_t min64u(size_t a, size_t b) { + return (a < b) ? a : b; +} + +fzf_position_t *fzf_pos_array(size_t len) { + fzf_position_t *pos = (fzf_position_t *)malloc(sizeof(fzf_position_t)); + pos->size = 0; + pos->cap = len; + if (len > 0) { + pos->data = (uint32_t *)malloc(len * sizeof(uint32_t)); + } else { + pos->data = NULL; + } + return pos; +} + +static void resize_pos(fzf_position_t *pos, size_t add_len, size_t comp) { + if (!pos) { + return; + } + if (pos->size + comp > pos->cap) { + pos->cap += add_len > 0 ? add_len : 1; + pos->data = (uint32_t *)realloc(pos->data, sizeof(uint32_t) * pos->cap); + } +} + +static void unsafe_append_pos(fzf_position_t *pos, size_t value) { + resize_pos(pos, pos->cap, 1); + pos->data[pos->size] = (int32_t)value; + pos->size++; +} + +static void append_pos(fzf_position_t *pos, size_t value) { + if (pos) { + unsafe_append_pos(pos, value); + } +} + +static void insert_range(fzf_position_t *pos, size_t start, size_t end) { + if (!pos) { + return; + } + + int32_t diff = ((int32_t)end - (int32_t)start); + if (diff <= 0) { + return; + } + + resize_pos(pos, end - start, end - start); + for (size_t k = start; k < end; k++) { + pos->data[pos->size] = (uint32_t)k; + pos->size++; + } +} + +static fzf_i16_t alloc16(size_t *offset, fzf_slab_t *slab, size_t size) { + if (slab != NULL && slab->I16.cap > *offset + size) { + i16_slice_t slice = slice_i16(slab->I16.data, *offset, (*offset) + size); + *offset = *offset + size; + return (fzf_i16_t){.data = slice.data, + .size = slice.size, + .cap = slice.size, + .allocated = false}; + } + int16_t *data = (int16_t *)malloc(size * sizeof(int16_t)); + memset(data, 0, size * sizeof(int16_t)); + return (fzf_i16_t){ + .data = data, .size = size, .cap = size, .allocated = true}; +} + +static fzf_i32_t alloc32(size_t *offset, fzf_slab_t *slab, size_t size) { + if (slab != NULL && slab->I32.cap > *offset + size) { + i32_slice_t slice = slice_i32(slab->I32.data, *offset, (*offset) + size); + *offset = *offset + size; + return (fzf_i32_t){.data = slice.data, + .size = slice.size, + .cap = slice.size, + .allocated = false}; + } + int32_t *data = (int32_t *)malloc(size * sizeof(int32_t)); + memset(data, 0, size * sizeof(int32_t)); + return (fzf_i32_t){ + .data = data, .size = size, .cap = size, .allocated = true}; +} + +static char_class UText_class_of_ascii(UChar32 ch) { + if (u_islower(ch)) { + return UCharLower; + } + if (u_isupper(ch)) { + return UCharUpper; + } + if (u_isdigit(ch)) { + return UCharNumber; + } + return UCharNonWord; +} + +// static char_class char_class_of_non_ascii(char ch) { +// return 0; +// } + +static char_class UText_class_of(UChar32 ch) { + return UText_class_of_ascii(ch); + // if (ch <= 0x7f) { + // return char_class_of_ascii(ch); + // } + // return char_class_of_non_ascii(ch); +} + +static int16_t bonus_for(char_class prev_class, char_class class) { + if (prev_class == UCharNonWord && class != UCharNonWord) { + return BonusBoundary; + } + if ((prev_class == UCharLower && class == UCharUpper) || + (prev_class != UCharNumber && class == UCharNumber)) { + return BonusCamel123; + } + if (class == UCharNonWord) { + return BonusNonWord; + } + return 0; +} + +static int16_t ubonus_at(ufzf_string_t *input, size_t idx) { + if (idx == 0) { + return BonusBoundary; + } + return bonus_for(UText_class_of(utext_char32At(input->data, idx - 1)), + UText_class_of(utext_char32At(input->data, idx))); +} + +/* TODO(conni2461): maybe just not do this */ +static UChar32 unormalize_rune(UChar32 r) { + // TODO(conni2461) + /* if (r < 0x00C0 || r > 0x2184) { */ + /* return r; */ + /* } */ + /* rune n = normalized[r]; */ + /* if n > 0 { */ + /* return n; */ + /* } */ + return r; +} + +static int32_t utry_skip(ufzf_string_t *input, bool case_sensitive, UChar32 b, + int32_t from) { + UTextSlice slice; + u_text_slice_init(&slice, input->data, from, (int32_t)input->size); + int32_t idx = uindex_byte(&slice, b); + if (idx == 0) { + return from; + } + + if (!case_sensitive) { + if (idx > 0) { + UTextSlice tmp; + u_text_slice_right(&tmp, &slice, idx); + slice.start = tmp.start; + slice.end = tmp.end; + slice.size = tmp.size; + } + UChar32 upperCaseB = u_toupper(b); + int32_t uidx = uindex_byte(&slice, upperCaseB); + if (uidx >= 0) { + idx = uidx; + } + } + if (idx < 0) { + return -1; + } + + return from + idx; +} + +static bool uis_ascii(const UText *runes, size_t size) { + // TODO(conni2461): future use + /* for (size_t i = 0; i < size; i++) { */ + /* if (runes[i] >= 256) { */ + /* return false; */ + /* } */ + /* } */ + return true; +} + +static int32_t uascii_fuzzy_index(ufzf_string_t *input, UText *pattern, + size_t size, bool case_sensitive) { + if (!uis_ascii(pattern, size)) { + return -1; + } + + int32_t first_idx = 0; + int32_t idx = 0; + for (size_t pidx = 0; pidx < size; pidx++) { + idx = utry_skip(input, case_sensitive, utext_char32At(pattern, pidx), idx); + if (idx < 0) { + return -1; + } + if (pidx == 0 && idx > 0) { + first_idx = idx - 1; + } + idx++; + } + + return first_idx; +} + +static int32_t ucalculate_score(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + size_t sidx, size_t eidx, fzf_position_t *pos) { + const size_t M = pattern->size; + + size_t pidx = 0; + int32_t score = 0; + int32_t consecutive = 0; + bool in_gap = false; + int16_t first_bonus = 0; + + resize_pos(pos, M, M); + int32_t prev_class = UCharNonWord; + if (sidx > 0) { + prev_class = UText_class_of(utext_char32At(text->data, sidx - 1)); + } + for (size_t idx = sidx; idx < eidx; idx++) { + UChar32 c = utext_char32At(text->data, idx); + int32_t class = UText_class_of(c); + if (!case_sensitive) { + /* TODO(conni2461): He does some unicode stuff here, investigate */ + c = u_tolower(c); + } + if (normalize) { + c = unormalize_rune(c); + } + if (c == utext_char32At(pattern->data, pidx)) { + append_pos(pos, idx); + score += ScoreMatch; + int16_t bonus = bonus_for(prev_class, class); + if (consecutive == 0) { + first_bonus = bonus; + } else { + if (bonus == BonusBoundary) { + first_bonus = bonus; + } + bonus = max16(max16(bonus, first_bonus), BonusConsecutive); + } + if (pidx == 0) { + score += (int32_t)(bonus * BonusFirstCharMultiplier); + } else { + score += (int32_t)bonus; + } + in_gap = false; + consecutive++; + pidx++; + } else { + if (in_gap) { + score += ScoreGapExtention; + } else { + score += ScoreGapStart; + } + in_gap = true; + consecutive = 0; + first_bonus = 0; + } + prev_class = class; + } + return score; +} + +fzf_result_t ufzf_fuzzy_match_v1(bool case_sensitive, bool normalize, ufzf_string_t* text, ufzf_string_t* pattern, fzf_position_t* pos, fzf_slab_t* slab) +{ + const size_t M = pattern->size; + const size_t N = text->size; + if (M == 0) + { + return (fzf_result_t){ 0, 0, 0 }; + } + if (uascii_fuzzy_index(text, pattern->data, M, case_sensitive) < 0) + { + return (fzf_result_t){ -1, -1, 0 }; + } + + int32_t pidx = 0; + int32_t sidx = -1; + int32_t eidx = -1; + for (size_t idx = 0; idx < N; idx++) + { + UChar32 c = utext_char32At(text->data, idx); + /* TODO(conni2461): Common pattern maybe a macro would be good here */ + if (!case_sensitive) + { + /* TODO(conni2461): He does some unicode stuff here, investigate */ + c = u_tolower(c); + } + if (normalize) + { + c = unormalize_rune(c); + } + if (c == utext_char32At(pattern->data, pidx)) + { + if (sidx < 0) + { + sidx = (int32_t)idx; + } + pidx++; + if (pidx == M) + { + eidx = (int32_t)idx + 1; + break; + } + } + } + if (sidx >= 0 && eidx >= 0) + { + size_t start = (size_t)sidx; + size_t end = (size_t)eidx; + pidx--; + for (size_t idx = end - 1; idx >= start; idx--) + { + UChar32 c = utext_char32At(text->data, idx); + if (!case_sensitive) + { + /* TODO(conni2461): He does some unicode stuff here, investigate */ + c = u_tolower(c); + } + if (c == utext_char32At(pattern->data, pidx)) + { + pidx--; + if (pidx < 0) + { + start = idx; + break; + } + } + } + + int32_t score = ucalculate_score(case_sensitive, normalize, text, pattern, start, end, pos); + return (fzf_result_t){ (int32_t)start, (int32_t)end, score }; + } + return (fzf_result_t){ -1, -1, 0 }; +} + +fzf_result_t ufzf_fuzzy_match_v2(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab) { + const size_t M = pattern->size; + const size_t N = text->size; + if (M == 0) { + return (fzf_result_t){0, 0, 0}; + } + if (slab != NULL && N * M > slab->I16.cap) { + return ufzf_fuzzy_match_v1(case_sensitive, normalize, text, pattern, pos, + slab); + } + + size_t idx; + { + int32_t tmp_idx = uascii_fuzzy_index(text, pattern->data, M, case_sensitive); + if (tmp_idx < 0) { + return (fzf_result_t){-1, -1, 0}; + } + idx = (size_t)tmp_idx; + } + + size_t offset16 = 0; + size_t offset32 = 0; + + fzf_i16_t h0 = alloc16(&offset16, slab, N); + fzf_i16_t c0 = alloc16(&offset16, slab, N); + // Bonus point for each positions + fzf_i16_t bo = alloc16(&offset16, slab, N); + // The first occurrence of each character in the pattern + fzf_i32_t f = alloc32(&offset32, slab, M); + + // Phase 2. Calculate bonus for each point + int16_t max_score = 0; + size_t max_score_pos = 0; + + size_t pidx = 0; + size_t last_idx = 0; + + UChar32 pchar0 = utext_char32At(pattern->data, 0); + UChar32 pchar = utext_char32At(pattern->data, 0); + int16_t prev_h0 = 0; + int32_t prev_class = UCharNonWord; + bool in_gap = false; + + UTextSlice t_sub; + u_text_slice_init(&t_sub, text->data, (int32_t)idx, (int32_t)text->size); + i16_slice_t h0_sub = + slice_i16_right(slice_i16(h0.data, idx, h0.size).data, t_sub.size); + i16_slice_t c0_sub = + slice_i16_right(slice_i16(c0.data, idx, c0.size).data, t_sub.size); + i16_slice_t b_sub = + slice_i16_right(slice_i16(bo.data, idx, bo.size).data, t_sub.size); + + for (size_t off = 0; off < t_sub.size; off++) { + char_class class; + UChar32 c = utextSlice_charAt(&t_sub, off); + class = UText_class_of_ascii(c); + if (!case_sensitive && class == UCharUpper) { + /* TODO(conni2461): unicode support */ + c = u_tolower(c); + } + if (normalize) { + c = unormalize_rune(c); + } + + int16_t bonus = bonus_for(prev_class, class); + b_sub.data[off] = bonus; + prev_class = class; + if (c == pchar) { + if (pidx < M) { + f.data[pidx] = (int32_t)(idx + off); + pidx++; + pchar = utext_char32At(pattern->data, min64u(pidx, M - 1)); + } + last_idx = idx + off; + } + + if (c == pchar0) { + int16_t score = ScoreMatch + bonus * BonusFirstCharMultiplier; + h0_sub.data[off] = score; + c0_sub.data[off] = 1; + if (M == 1 && (score > max_score)) { + max_score = score; + max_score_pos = idx + off; + if (bonus == BonusBoundary) { + break; + } + } + in_gap = false; + } else { + if (in_gap) { + h0_sub.data[off] = max16(prev_h0 + ScoreGapExtention, 0); + } else { + h0_sub.data[off] = max16(prev_h0 + ScoreGapStart, 0); + } + c0_sub.data[off] = 0; + in_gap = true; + } + prev_h0 = h0_sub.data[off]; + } + if (pidx != M) { + free_alloc(f); + free_alloc(bo); + free_alloc(c0); + free_alloc(h0); + return (fzf_result_t){-1, -1, 0}; + } + if (M == 1) { + free_alloc(f); + free_alloc(bo); + free_alloc(c0); + free_alloc(h0); + fzf_result_t res = {(int32_t)max_score_pos, (int32_t)max_score_pos + 1, + max_score}; + append_pos(pos, max_score_pos); + return res; + } + + size_t f0 = (size_t)f.data[0]; + size_t width = last_idx - f0 + 1; + fzf_i16_t h = alloc16(&offset16, slab, width * M); + { + i16_slice_t h0_tmp_slice = slice_i16(h0.data, f0, last_idx + 1); + copy_into_i16(&h0_tmp_slice, &h); + } + + fzf_i16_t c = alloc16(&offset16, slab, width * M); + { + i16_slice_t c0_tmp_slice = slice_i16(c0.data, f0, last_idx + 1); + copy_into_i16(&c0_tmp_slice, &c); + } + + i32_slice_t f_sub = slice_i32(f.data, 1, f.size); + UTextSlice p_subTmp; + UTextSlice p_sub; + u_text_slice_init(&p_subTmp, pattern->data, 1, (int32_t)M); + u_text_slice_right(&p_sub, &p_subTmp, (int32_t)f_sub.size); + for (size_t off = 0; off < f_sub.size; off++) { + size_t f = (size_t)f_sub.data[off]; + pchar = utextSlice_charAt(&p_sub, off); + pidx = off + 1; + size_t row = pidx * width; + in_gap = false; + UTextSlice t_sub2; + u_text_slice_init(&t_sub2, text->data, (int32_t)f, (int32_t)last_idx + 1); + t_sub = t_sub2; + b_sub = slice_i16_right(slice_i16(bo.data, (int32_t)f, bo.size).data, (int32_t)t_sub.size); + i16_slice_t c_sub = slice_i16_right( + slice_i16(c.data, row + f - f0, c.size).data, t_sub.size); + i16_slice_t c_diag = slice_i16_right( + slice_i16(c.data, row + f - f0 - 1 - width, c.size).data, t_sub.size); + i16_slice_t h_sub = slice_i16_right( + slice_i16(h.data, row + f - f0, h.size).data, t_sub.size); + i16_slice_t h_diag = slice_i16_right( + slice_i16(h.data, row + f - f0 - 1 - width, h.size).data, t_sub.size); + i16_slice_t h_left = slice_i16_right( + slice_i16(h.data, row + f - f0 - 1, h.size).data, t_sub.size); + h_left.data[0] = 0; + for (size_t j = 0; j < t_sub.size; j++) { + UChar32 ch = utextSlice_charAt(&t_sub, j); + + char_class class = UText_class_of_ascii(ch); + if (!case_sensitive && class == UCharUpper) + { + /* TODO(conni2461): unicode support */ + ch = u_tolower(ch); + } + + size_t col = j + f; + int16_t s1 = 0; + int16_t s2 = 0; + int16_t consecutive = 0; + + if (in_gap) { + s2 = h_left.data[j] + ScoreGapExtention; + } else { + s2 = h_left.data[j] + ScoreGapStart; + } + + if (pchar == ch) { + s1 = h_diag.data[j] + ScoreMatch; + int16_t b = b_sub.data[j]; + consecutive = c_diag.data[j] + 1; + if (b == BonusBoundary) { + consecutive = 1; + } else if (consecutive > 1) { + b = max16(b, max16(BonusConsecutive, + bo.data[col - ((size_t)consecutive) + 1])); + } + if (s1 + b < s2) { + s1 += b_sub.data[j]; + consecutive = 0; + } else { + s1 += b; + } + } + c_sub.data[j] = consecutive; + in_gap = s1 < s2; + int16_t score = max16(max16(s1, s2), 0); + if (pidx == M - 1 && (score > max_score)) { + max_score = score; + max_score_pos = col; + } + h_sub.data[j] = score; + } + } + + resize_pos(pos, M, M); + size_t j = max_score_pos; + if (pos) { + size_t i = M - 1; + bool prefer_match = true; + for (;;) { + size_t ii = i * width; + size_t j0 = j - f0; + int16_t s = h.data[ii + j0]; + + int16_t s1 = 0; + int16_t s2 = 0; + if (i > 0 && j >= f.data[i]) { + s1 = h.data[ii - width + j0 - 1]; + } + if (j > f.data[i]) { + s2 = h.data[ii + j0 - 1]; + } + + if (s > s1 && (s > s2 || (s == s2 && prefer_match))) { + unsafe_append_pos(pos, j); + if (i == 0) { + break; + } + i--; + } + prefer_match = c.data[ii + j0] > 1 || (ii + width + j0 + 1 < c.size && + c.data[ii + width + j0 + 1] > 0); + j--; + } + } + + free_alloc(h); + free_alloc(c); + free_alloc(f); + free_alloc(bo); + free_alloc(c0); + free_alloc(h0); + return (fzf_result_t){(int32_t)j, (int32_t)max_score_pos + 1, + (int32_t)max_score}; +} + +fzf_result_t ufzf_exact_match_naive(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab) { + const size_t M = pattern->size; + const size_t N = text->size; + + if (M == 0) { + return (fzf_result_t){0, 0, 0}; + } + if (N < M) { + return (fzf_result_t){-1, -1, 0}; + } + if (uascii_fuzzy_index(text, pattern->data, M, case_sensitive) < 0) { + return (fzf_result_t){-1, -1, 0}; + } + + size_t pidx = 0; + int32_t best_pos = -1; + int16_t bonus = 0; + int16_t best_bonus = -1; + for (size_t idx = 0; idx < N; idx++) { + UChar32 c = utext_char32At(text->data, idx); + if (!case_sensitive) { + /* TODO(conni2461): He does some unicode stuff here, investigate */ + c = u_tolower(c); + } + if (normalize) { + c = unormalize_rune(c); + } + if (c == utext_char32At(pattern->data, pidx)) + { + if (pidx == 0) { + bonus = ubonus_at(text, idx); + } + pidx++; + if (pidx == M) { + if (bonus > best_bonus) { + best_pos = (int32_t)idx; + best_bonus = bonus; + } + if (bonus == BonusBoundary) { + break; + } + idx -= pidx - 1; + pidx = 0; + bonus = 0; + } + } else { + idx -= pidx; + pidx = 0; + bonus = 0; + } + } + if (best_pos >= 0) { + size_t bp = (size_t)best_pos; + size_t sidx = bp - M + 1; + size_t eidx = bp + 1; + int32_t score = ucalculate_score(case_sensitive, normalize, text, pattern, + sidx, eidx, NULL); + insert_range(pos, sidx, eidx); + return (fzf_result_t){(int32_t)sidx, (int32_t)eidx, score}; + } + return (fzf_result_t){-1, -1, 0}; +} + +fzf_result_t ufzf_prefix_match(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab) { + const size_t M = pattern->size; + if (M == 0) { + return (fzf_result_t){0, 0, 0}; + } + size_t trimmed_len = 0; + /* TODO(conni2461): i feel this is wrong */ + if (!u_isWhitespace(utext_char32At(pattern->data, 0))) { + trimmed_len = uleading_whitespaces(text->data); + } + if (text->size - trimmed_len < M) { + return (fzf_result_t){-1, -1, 0}; + } + for (size_t i = 0; i < M; i++) { + UChar32 c = utext_char32At(text->data, trimmed_len + i); + if (!case_sensitive) { + c = u_tolower(c); + } + if (normalize) { + c = unormalize_rune(c); + } + if (c != utext_char32At(pattern->data, i)) { + return (fzf_result_t){-1, -1, 0}; + } + } + size_t start = trimmed_len; + size_t end = trimmed_len + M; + int32_t score = ucalculate_score(case_sensitive, normalize, text, pattern, + start, end, NULL); + insert_range(pos, start, end); + return (fzf_result_t){(int32_t)start, (int32_t)end, score}; +} + +fzf_result_t ufzf_suffix_match(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab) { + size_t trimmed_len = text->size; + const size_t M = pattern->size; + /* TODO(conni2461): i think this is wrong */ + if (M == 0 || !u_isWhitespace(utext_char32At(pattern->data, M - 1))) { + trimmed_len -= utrailing_whitespaces(text->data); + } + if (M == 0) { + return (fzf_result_t){(int32_t)trimmed_len, (int32_t)trimmed_len, 0}; + } + size_t diff = trimmed_len - M; + if (diff < 0) { + return (fzf_result_t){-1, -1, 0}; + } + + for (size_t idx = 0; idx < M; idx++) { + UChar32 c = utext_char32At(text->data, idx + diff); + if (!case_sensitive) { + c = u_tolower(c); + } + if (normalize) { + c = unormalize_rune(c); + } + if (c != utext_char32At(pattern->data, idx)) { + return (fzf_result_t){-1, -1, 0}; + } + } + size_t start = trimmed_len - M; + size_t end = trimmed_len; + int32_t score = ucalculate_score(case_sensitive, normalize, text, pattern, + start, end, NULL); + insert_range(pos, start, end); + return (fzf_result_t){(int32_t)start, (int32_t)end, score}; +} + +fzf_result_t ufzf_equal_match(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab) { + const size_t M = pattern->size; + if (M == 0) { + return (fzf_result_t){-1, -1, 0}; + } + + size_t trimmed_len = uleading_whitespaces(text->data); + size_t trimmed_end_len = utrailing_whitespaces(text->data); + + if ((text->size - trimmed_len - trimmed_end_len) != M) { + return (fzf_result_t){-1, -1, 0}; + } + + bool match = true; + if (normalize) { + // TODO(conni2461): to rune + for (size_t idx = 0; idx < M; idx++) { + UChar32 pchar = utext_char32At(pattern->data, idx); + UChar32 c = utext_char32At(text->data, trimmed_len + idx); + if (!case_sensitive) { + c = u_tolower(c); + } + if (unormalize_rune(c) != unormalize_rune(pchar)) { + match = false; + break; + } + } + } else { + // TODO(conni2461): to rune + for (size_t idx = 0; idx < M; idx++) { + UChar32 pchar = utext_char32At(pattern->data, idx); + UChar32 c = utext_char32At(text->data, trimmed_len + idx); + if (!case_sensitive) { + c = u_tolower(c); + } + if (c != pchar) { + match = false; + break; + } + } + } + if (match) { + insert_range(pos, trimmed_len, trimmed_len + M); + return (fzf_result_t){(int32_t)trimmed_len, + ((int32_t)trimmed_len + (int32_t)M), + (ScoreMatch + BonusBoundary) * (int32_t)M + + (BonusFirstCharMultiplier - 1) * BonusBoundary}; + } + return (fzf_result_t){-1, -1, 0}; +} + +static void uappend_set(ufzf_term_set_t *set, ufzf_term_t value) { + if (set->cap == 0) { + set->cap = 1; + set->ptr = (ufzf_term_t *)malloc(sizeof(ufzf_term_t)); + } else if (set->size + 1 > set->cap) { + set->cap *= 2; + set->ptr = realloc(set->ptr, sizeof(ufzf_term_t) * set->cap); + } + set->ptr[set->size] = value; + set->size++; +} + +static void uappend_pattern(ufzf_pattern_t *pattern, ufzf_term_set_t *value) { + if (pattern->cap == 0) { + pattern->cap = 1; + pattern->ptr = (ufzf_term_set_t **)malloc(sizeof(ufzf_term_set_t *)); + } else if (pattern->size + 1 > pattern->cap) { + pattern->cap *= 2; + pattern->ptr = + realloc(pattern->ptr, sizeof(ufzf_term_set_t *) * pattern->cap); + } + pattern->ptr[pattern->size] = value; + pattern->size++; +} + +#define UCALL_ALG(term, normalize, input, pos, slab) \ + term->fn((term)->case_sensitive, normalize, &(input), \ + (ufzf_string_t *)(term)->text, pos, slab) + +UChar* ustrtok_r(UChar* src, const UChar delim, UChar** context, size_t length) +{ + if (src == NULL && (src = *context) == NULL) + { + return NULL; // No more tokens + } + + UChar* end = src; + UChar* src_end = src + length; // Calculate the end of the src based on the length + + while (end < src_end && *end && *end != delim) + { + ++end; + } + + if (end >= src_end || *end == 0) + { // End of string or reaching the length limit + *context = NULL; + } + else + { + *end = 0; // Replace delim with null terminator to end token + *context = end + 1; + } + + return src; +} + +UChar* u_strdup(const UChar* src) { + if (src == NULL) { + return NULL; + } + + int32_t len = u_strlen(src); + UChar* dup = malloc((len + 1) * sizeof(UChar)); + if (dup == NULL) { + return NULL; + } + + u_memcpy(dup, src, len + 1); + + return dup; +} + +ufzf_pattern_t *ufzf_parse_pattern(fzf_case_types case_mode, bool normalize, + const UChar *pattern, bool fuzzy) { + ufzf_pattern_t *pat_obj = (ufzf_pattern_t *)malloc(sizeof(ufzf_pattern_t)); + memset(pat_obj, 0, sizeof(*pat_obj)); + + size_t pat_len = u_strlen(pattern); + if (pat_len == 0) { + return pat_obj; + } + + UChar* patternDup = u_strdup(pattern); + + patternDup = utrim_whitespace_left(patternDup, &pat_len); + + UChar spaceChar = 0x0020; + UChar space_suffix[] = { 0x0020, 0x0000 }; + UChar escaped_space_suffix[] = { 0x005C, 0x0020, 0 }; + UChar tabChar = 0x0009; + UChar tab[] = { tabChar, 0x0000 }; + while (uhas_suffix(patternDup, pat_len, space_suffix, 1) && + !uhas_suffix(patternDup, pat_len, escaped_space_suffix, 2)) { + patternDup[pat_len - 1] = 0; + pat_len--; + } + + UChar* pattern_copy = ustr_replace(patternDup, escaped_space_suffix, tab); + UChar* context = NULL; + UChar udelim = 0x0020; + UChar* ptr = ustrtok_r(pattern_copy, udelim, &context, pat_len); + + ufzf_term_set_t *set = (ufzf_term_set_t *)malloc(sizeof(ufzf_term_set_t)); + memset(set, 0, sizeof(*set)); + + bool switch_set = false; + bool after_bar = false; + while (ptr != NULL) { + ufzf_algo_t fn = ufzf_fuzzy_match_v2; + bool inv = false; + + size_t len = u_strlen(ptr); + ustr_replace_char(ptr, tabChar, spaceChar); + UChar* text = u_strdup(ptr); + + UChar* og_str = text; + int32_t bufferLength = (int32_t)len + 1; + UChar* lower_text = malloc(bufferLength * sizeof(UChar)); + UErrorCode status = U_ZERO_ERROR; + u_strToLower(lower_text, bufferLength, text, (int32_t)len, "", &status); + bool case_sensitive = + case_mode == CaseRespect || + (case_mode == CaseSmart && u_strcmp(text, lower_text) != 0); + if (!case_sensitive) { + SFREE(text); + text = lower_text; + og_str = lower_text; + } else { + SFREE(lower_text); + } + if (!fuzzy) { + fn = ufzf_exact_match_naive; + } + UChar pipeChar[] = { 0x007C, 0x0000 }; + if (set->size > 0 && !after_bar && u_strcmp(text, pipeChar) == 0) { + switch_set = false; + after_bar = true; + ptr = ustrtok_r(NULL, udelim, &context, pat_len); + SFREE(og_str); + continue; + } + after_bar = false; + UChar exclamationPrefix[] = { 0x0021, 0 }; + if (uhas_prefix(text, exclamationPrefix, 1)) { + inv = true; + fn = ufzf_exact_match_naive; + text++; + len--; + } + + UChar dollarSignChar[] = { 0x0024, 0 }; + if (u_strcmp(text, dollarSignChar) != 0 && uhas_suffix(text, len, dollarSignChar, 1)) { + fn = ufzf_suffix_match; + text[len - 1] = 0; + len--; + } + + UChar singleQuoteChar[] = { 0x0027, 0 }; + UChar caretChar[] = { 0x005E, 0 }; + if (uhas_prefix(text, singleQuoteChar, 1)) { + if (fuzzy && !inv) { + fn = ufzf_exact_match_naive; + text++; + len--; + } else { + fn = ufzf_fuzzy_match_v2; + text++; + len--; + } + } else if (uhas_prefix(text, caretChar, 1)) { + if (fn == ufzf_suffix_match) { + fn = ufzf_equal_match; + } else { + fn = ufzf_prefix_match; + } + text++; + len--; + } + + if (len > 0) { + if (switch_set) { + uappend_pattern(pat_obj, set); + set = (ufzf_term_set_t *)malloc(sizeof(ufzf_term_set_t)); + set->cap = 0; + set->size = 0; + } + UErrorCode status = U_ZERO_ERROR; + UText* utextPattern = utext_openUChars(NULL, og_str, u_strlen(og_str), &status); + UText* utextText = utext_openUChars(NULL, text, u_strlen(text), &status); + ufzf_string_t* text_ptr = (ufzf_string_t*)malloc(sizeof(ufzf_string_t)); + text_ptr->data = utextText; + text_ptr->size = len; + uappend_set(set, (ufzf_term_t){.fn = fn, + .inv = inv, + .ptr = utextPattern, + .text = text_ptr, + .case_sensitive = case_sensitive}); + switch_set = true; + } else { + SFREE(og_str); + } + + ptr = ustrtok_r(NULL, udelim, &context, pat_len); + } + if (set->size > 0) { + uappend_pattern(pat_obj, set); + } else { + SFREE(set->ptr); + SFREE(set); + } + bool only = true; + for (size_t i = 0; i < pat_obj->size; i++) { + ufzf_term_set_t *term_set = pat_obj->ptr[i]; + if (term_set->size > 1) { + only = false; + break; + } + if (term_set->ptr[0].inv == false) { + only = false; + break; + } + } + pat_obj->only_inv = only; + SFREE(pattern_copy); + SFREE(patternDup); + return pat_obj; +} + +void ufzf_free_pattern(ufzf_pattern_t* pattern) +{ + if (pattern->ptr) + { + for (size_t i = 0; i < pattern->size; i++) + { + ufzf_term_set_t* term_set = pattern->ptr[i]; + for (size_t j = 0; j < term_set->size; j++) + { + ufzf_term_t* term = &term_set->ptr[j]; + utext_close(term->ptr); + utext_close(term->text); + } + free(term_set->ptr); + free(term_set); + } + free(pattern->ptr); + } + SFREE(pattern); +} + +int32_t ufzf_get_score(UText *text, ufzf_pattern_t *pattern, + fzf_slab_t *slab) { + // If the pattern is an empty string then pattern->ptr will be NULL and we + // basically don't want to filter. Return 1 for telescope + if (pattern->ptr == NULL) { + return 1; + } + + ufzf_string_t input = {.data = text, .size = utext_nativeLength(text)}; + if (pattern->only_inv) { + int final = 0; + for (size_t i = 0; i < pattern->size; i++) { + ufzf_term_set_t *term_set = pattern->ptr[i]; + ufzf_term_t *term = &term_set->ptr[0]; + + final += UCALL_ALG(term, false, input, NULL, slab).score; + } + //I changed this to 100 to work better with a min score + return (final > 0) ? 0 : 1; + } + + int32_t total_score = 0; + for (size_t i = 0; i < pattern->size; i++) { + ufzf_term_set_t *term_set = pattern->ptr[i]; + int32_t current_score = 0; + bool matched = false; + for (size_t j = 0; j < term_set->size; j++) { + ufzf_term_t *term = &term_set->ptr[j]; + fzf_result_t res = UCALL_ALG(term, false, input, NULL, slab); + if (res.start >= 0) { + if (term->inv) { + continue; + } + current_score = res.score; + matched = true; + break; + } + + if (term->inv) { + current_score = 0; + matched = true; + } + } + if (matched) { + total_score += current_score; + } else { + total_score = 0; + break; + } + } + + return total_score; +} + +fzf_position_t *ufzf_get_positions(UText *text, ufzf_pattern_t *pattern, + fzf_slab_t *slab) { + // If the pattern is an empty string then pattern->ptr will be NULL and we + // basically don't want to filter. Return 1 for telescope + if (pattern->ptr == NULL) { + return NULL; + } + + ufzf_string_t input = {.data = text, .size = utext_nativeLength(text)}; + fzf_position_t *all_pos = fzf_pos_array(0); + for (size_t i = 0; i < pattern->size; i++) { + ufzf_term_set_t *term_set = pattern->ptr[i]; + bool matched = false; + for (size_t j = 0; j < term_set->size; j++) { + ufzf_term_t *term = &term_set->ptr[j]; + if (term->inv) { + // If we have an inverse term we need to check if we have a match, but + // we are not interested in the positions (for highlights) so to speed + // this up we can pass in NULL here and don't calculate the positions + fzf_result_t res = UCALL_ALG(term, false, input, NULL, slab); + if (res.start < 0) { + matched = true; + } + continue; + } + fzf_result_t res = UCALL_ALG(term, false, input, all_pos, slab); + if (res.start >= 0) { + matched = true; + break; + } + } + if (!matched) { + fzf_free_positions(all_pos); + return NULL; + } + } + return all_pos; +} + +void fzf_free_positions(fzf_position_t *pos) { + if (pos) { + SFREE(pos->data); + free(pos); + } +} + +fzf_slab_t *fzf_make_slab(fzf_slab_config_t config) { + fzf_slab_t *slab = (fzf_slab_t *)malloc(sizeof(fzf_slab_t)); + memset(slab, 0, sizeof(*slab)); + + slab->I16.data = (int16_t *)malloc(config.size_16 * sizeof(int16_t)); + memset(slab->I16.data, 0, config.size_16 * sizeof(*slab->I16.data)); + slab->I16.cap = config.size_16; + slab->I16.size = 0; + slab->I16.allocated = true; + + slab->I32.data = (int32_t *)malloc(config.size_32 * sizeof(int32_t)); + memset(slab->I32.data, 0, config.size_32 * sizeof(*slab->I32.data)); + slab->I32.cap = config.size_32; + slab->I32.size = 0; + slab->I32.allocated = true; + + return slab; +} + +fzf_slab_t *fzf_make_default_slab(void) { + return fzf_make_slab((fzf_slab_config_t){(size_t)100 * 1024, 2048}); +} + +void fzf_free_slab(fzf_slab_t *slab) { + if (slab) { + free(slab->I16.data); + free(slab->I32.data); + free(slab); + } +} +#pragma warning(pop) diff --git a/src/cascadia/TerminalControl/fzf/fzf.h b/src/cascadia/TerminalControl/fzf/fzf.h new file mode 100644 index 00000000000..32724b9ad59 --- /dev/null +++ b/src/cascadia/TerminalControl/fzf/fzf.h @@ -0,0 +1,118 @@ +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef FZF_H_ +#define FZF_H_ + +#include +#include +#include +#include + +typedef struct { + int16_t *data; + size_t size; + size_t cap; + bool allocated; +} fzf_i16_t; + +typedef struct { + int32_t *data; + size_t size; + size_t cap; + bool allocated; +} fzf_i32_t; + +typedef struct { + uint32_t *data; + size_t size; + size_t cap; +} fzf_position_t; + +typedef struct { + int32_t start; + int32_t end; + int32_t score; +} fzf_result_t; + +typedef struct { + fzf_i16_t I16; + fzf_i32_t I32; +} fzf_slab_t; + +typedef struct { + size_t size_16; + size_t size_32; +} fzf_slab_config_t; + +typedef struct { + UText *data; + size_t size; +} ufzf_string_t; + +typedef struct +{ + const UChar* data; + size_t size; +} ufzf_charString_t; + +typedef fzf_result_t (*ufzf_algo_t)(bool, bool, ufzf_string_t *, ufzf_string_t *, + fzf_position_t *, fzf_slab_t *); + +typedef enum { CaseSmart = 0, CaseIgnore, CaseRespect } fzf_case_types; + +typedef struct { + ufzf_algo_t fn; + bool inv; + UText *ptr; + void *text; + bool case_sensitive; +} ufzf_term_t; + +typedef struct { + ufzf_term_t *ptr; + size_t size; + size_t cap; +} ufzf_term_set_t; + +typedef struct { + ufzf_term_set_t **ptr; + size_t size; + size_t cap; + bool only_inv; +} ufzf_pattern_t; + +fzf_result_t ufzf_fuzzy_match_v2(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +fzf_result_t ufzf_exact_match_naive(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +fzf_result_t ufzf_prefix_match(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +fzf_result_t ufzf_suffix_match(bool case_sensitive, bool normalize, + ufzf_string_t *text, ufzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +/* interface */ +ufzf_pattern_t *ufzf_parse_pattern(fzf_case_types case_mode, bool normalize, + const UChar *pattern, bool fuzzy); +void ufzf_free_pattern(ufzf_pattern_t* pattern); + +int32_t ufzf_get_score(UText *text, ufzf_pattern_t *pattern, + fzf_slab_t *slab); + +fzf_position_t *fzf_pos_array(size_t len); +fzf_position_t *ufzf_get_positions(UText *text, ufzf_pattern_t *pattern, + fzf_slab_t *slab); +void fzf_free_positions(fzf_position_t *pos); + +fzf_slab_t *fzf_make_slab(fzf_slab_config_t config); +fzf_slab_t *fzf_make_default_slab(void); +void fzf_free_slab(fzf_slab_t *slab); + +#endif // FZF_H_ +#ifdef __cplusplus +} +#endif diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 7bba13bc4cc..dfeb9deb9e7 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -97,6 +97,7 @@ static constexpr std::string_view ExpandSelectionToWordKey{ "expandSelectionToWo static constexpr std::string_view RestartConnectionKey{ "restartConnection" }; static constexpr std::string_view ToggleBroadcastInputKey{ "toggleBroadcastInput" }; static constexpr std::string_view OpenScratchpadKey{ "experimental.openScratchpad" }; +static constexpr std::string_view OpenFuzzySearchKey{ "experimental.openFuzzySearch" }; static constexpr std::string_view OpenAboutKey{ "openAbout" }; static constexpr std::string_view ActionKey{ "action" }; @@ -433,6 +434,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::RestartConnection, RS_(L"RestartConnectionKey") }, { ShortcutAction::ToggleBroadcastInput, RS_(L"ToggleBroadcastInputCommandKey") }, { ShortcutAction::OpenScratchpad, RS_(L"OpenScratchpadKey") }, + { ShortcutAction::OpenFuzzySearch, RS_(L"OpenFuzzySearchKey") }, { ShortcutAction::OpenAbout, RS_(L"OpenAboutCommandKey") }, }; }(); diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index 28ba4281e69..fccb5480591 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -111,6 +111,7 @@ ON_ALL_ACTIONS(RestartConnection) \ ON_ALL_ACTIONS(ToggleBroadcastInput) \ ON_ALL_ACTIONS(OpenScratchpad) \ + ON_ALL_ACTIONS(OpenFuzzySearch) \ ON_ALL_ACTIONS(OpenAbout) #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \ diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 6179099bdf5..509ba2d983e 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -703,6 +703,9 @@ Open scratchpad + + Open fuzzy search + Select next command output @@ -727,4 +730,4 @@ Open about dialog This will open the "about" dialog, to display version info and other documentation - \ No newline at end of file + diff --git a/src/features.xml b/src/features.xml index c2af5ed009a..e1d9ab4b8f6 100644 --- a/src/features.xml +++ b/src/features.xml @@ -145,6 +145,17 @@ + + Feature_FuzzySearch + Fuzzy Search Active Terminal Pane + 9970 + AlwaysDisabled + + Dev + Canary + + + Feature_KeypadModeEnabled Enables the DECKPAM, DECKPNM sequences to work as intended