diff --git a/autoload/SpaceVim/layers/telescope.vim b/autoload/SpaceVim/layers/telescope.vim index eaa7e288e..7c4a41920 100644 --- a/autoload/SpaceVim/layers/telescope.vim +++ b/autoload/SpaceVim/layers/telescope.vim @@ -50,6 +50,7 @@ function! SpaceVim#layers#telescope#plugins() abort call add(plugins, [g:_spacevim_root_dir . 'bundle/telescope-menu', {'merged' : 0}]) call add(plugins, [g:_spacevim_root_dir . 'bundle/telescope-ctags-outline.nvim', {'merged' : 0}]) call add(plugins, [g:_spacevim_root_dir . 'bundle/neoyank.vim', { 'merged' : 0}]) + call add(plugins, [g:_spacevim_root_dir . 'bundle/telescope-fzf-native.nvim', { 'merged' : 0}]) return plugins endfunction diff --git a/bundle/telescope-fzf-native.nvim/.clang-format b/bundle/telescope-fzf-native.nvim/.clang-format new file mode 100644 index 000000000..32799e7d5 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/.clang-format @@ -0,0 +1,7 @@ +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +IndentCaseLabels: false +SortIncludes: false +ColumnLimit: 80 +IndentWidth: 2 diff --git a/bundle/telescope-fzf-native.nvim/.github/FUNDING.yml b/bundle/telescope-fzf-native.nvim/.github/FUNDING.yml new file mode 100644 index 000000000..66fb523f8 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/.github/FUNDING.yml @@ -0,0 +1 @@ +github: Conni2461 diff --git a/bundle/telescope-fzf-native.nvim/.github/workflows/ci.yml b/bundle/telescope-fzf-native.nvim/.github/workflows/ci.yml new file mode 100644 index 000000000..f158d68d0 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: CI + +on: [push, pull_request] + +jobs: + gcc: + name: c build and tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-20.04 + compiler: gcc + - os: ubuntu-20.04 + compiler: clang + - os: macos-10.15 + compiler: gcc + - os: macos-10.15 + compiler: clang + steps: + - uses: actions/checkout@v2 + - name: Prepare + env: + CC: ${{ matrix.compiler }} + run: | + cc --version + git clone https://github.com/Conni2461/examiner + cd examiner + make && sudo make install + - name: Build + env: + CC: ${{ matrix.compiler }} + LD_LIBRARY_PATH: /usr/lib:/usr/local/lib + run: make + - name: Tests + env: + CC: ${{ matrix.compiler }} + LD_LIBRARY_PATH: /usr/lib:/usr/local/lib + run: make test + + windows: + name: windows + runs-on: windows-2019 + steps: + - uses: actions/checkout@v2 + - uses: lukka/get-cmake@latest + - name: Build + run: | + cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release + cmake --build build --config Release + cmake --install build --prefix build + + nvim-tests: + name: nvim-tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-20.04, macos-10.15] + include: + - os: ubuntu-20.04 + url: https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz + - os: macos-10.15 + url: https://github.com/neovim/neovim/releases/download/nightly/nvim-macos.tar.gz + steps: + - uses: actions/checkout@v2 + - run: date +%F > todays-date + - name: Restore cache for today's nightly. + uses: actions/cache@v2 + with: + path: _neovim + key: ${{ matrix.os }}-${{ hashFiles('todays-date') }} + - name: Prepare + run: | + test -d _neovim || { + mkdir -p _neovim + curl -sL ${{ matrix.url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + } + mkdir -p ~/.local/share/nvim/site/pack/vendor/start + git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim + ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start + - name: Build + run: make + - name: Tests + run: | + export PATH="${PWD}/_neovim/bin:${PATH}" + export VIM="${PWD}/_neovim/share/nvim/runtime" + nvim --version + make ntest diff --git a/bundle/telescope-fzf-native.nvim/.github/workflows/lint.yml b/bundle/telescope-fzf-native.nvim/.github/workflows/lint.yml new file mode 100644 index 000000000..e964bc13f --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/.github/workflows/lint.yml @@ -0,0 +1,42 @@ +name: Linting and style checking + +on: [push, pull_request] + +jobs: + lint: + name: Lint + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Prepare + run: | + sudo apt-get update + sudo apt-get install luarocks + sudo luarocks install luacheck + + - name: Lint + run: make lint + + clangformat: + name: clangformat + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Prepare clang-format + run: | + sudo apt-get update + sudo apt-get install clang-format + - name: Format + run: make format + + stylua: + name: stylua + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - uses: JohnnyMorganz/stylua-action@1.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + # CLI arguments + args: --color always --check lua/ diff --git a/bundle/telescope-fzf-native.nvim/.gitignore b/bundle/telescope-fzf-native.nvim/.gitignore new file mode 100644 index 000000000..0b1aea85e --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/.gitignore @@ -0,0 +1,4 @@ +build/ +.cache/ + +compile_commands.json diff --git a/bundle/telescope-fzf-native.nvim/.luacheckrc b/bundle/telescope-fzf-native.nvim/.luacheckrc new file mode 100644 index 000000000..ce3221f17 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/.luacheckrc @@ -0,0 +1,16 @@ +-- cache false so i don't need sudo upstream +cache = false +std = luajit +codes = true +self = false + +-- Glorious list of warnings: https://luacheck.readthedocs.io/en/stable/warnings.html +ignore = { + "212", -- Unused argument, In the case of callback function, _arg_name is easier to understand than _, so this option is set to off. + "122", -- Indirectly setting a readonly global +} + +-- Global objects defined by the C code +read_globals = { + "vim", +} diff --git a/bundle/telescope-fzf-native.nvim/.stylua.toml b/bundle/telescope-fzf-native.nvim/.stylua.toml new file mode 100644 index 000000000..ecb6dca5a --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/.stylua.toml @@ -0,0 +1,6 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +call_parentheses = "None" diff --git a/bundle/telescope-fzf-native.nvim/CMakeLists.txt b/bundle/telescope-fzf-native.nvim/CMakeLists.txt new file mode 100644 index 000000000..256ae0ad8 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.2) +project(fzf C) + +add_library(${PROJECT_NAME} SHARED "src/fzf.c") + +set_target_properties(${PROJECT_NAME} PROPERTIES + PREFIX "lib" + C_STANDARD 99 + WINDOWS_EXPORT_ALL_SYMBOLS ON +) + +target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) + +if (CMAKE_C_COMPILER_ID STREQUAL "MSVC") + target_compile_options(${PROJECT_NAME} PRIVATE /W4) +else () + target_compile_options(${PROJECT_NAME} PRIVATE -Wall) +endif () + +install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_BINARY_DIR}) diff --git a/bundle/telescope-fzf-native.nvim/LICENSE b/bundle/telescope-fzf-native.nvim/LICENSE new file mode 100644 index 000000000..57a10051e --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/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/bundle/telescope-fzf-native.nvim/Makefile b/bundle/telescope-fzf-native.nvim/Makefile new file mode 100644 index 000000000..2a27e151c --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/Makefile @@ -0,0 +1,40 @@ +CFLAGS = -Wall -Werror -fpic -std=gnu99 + +ifeq ($(OS),Windows_NT) + MKD = -mkdir + RM = cmd /C rmdir /Q /S + CC = gcc + TARGET := libfzf.dll +else + MKD = mkdir -p + RM = rm -rf + TARGET := libfzf.so +endif + +all: build/$(TARGET) + +build/$(TARGET): src/fzf.c src/fzf.h + $(MKD) build + $(CC) -O3 $(CFLAGS) -shared src/fzf.c -o build/$(TARGET) + +build/test: build/$(TARGET) test/test.c + $(CC) -Og -ggdb3 $(CFLAGS) test/test.c -o build/test -I./src -L./build -lfzf -lexaminer + +.PHONY: lint format clangdhappy clean test ntest +lint: + luacheck lua + +format: + clang-format --style=file --dry-run -Werror src/fzf.c src/fzf.h test/test.c + +test: build/test + @LD_LIBRARY_PATH=${PWD}/build:${PWD}/examiner/build:${LD_LIBRARY_PATH} ./build/test + +ntest: + nvim --headless --noplugin -u test/minrc.vim -c "PlenaryBustedDirectory test/ { minimal_init = './test/minrc.vim' }" + +clangdhappy: + compiledb make + +clean: + $(RM) build diff --git a/bundle/telescope-fzf-native.nvim/README.md b/bundle/telescope-fzf-native.nvim/README.md new file mode 100644 index 000000000..bc78527f6 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/README.md @@ -0,0 +1,167 @@ +# telescope-fzf-native.nvim + +**fzf-native** is a `c` port of **[fzf][fzf]**. It only covers the algorithm and +implements few functions to support calculating the score. + +This means that the [fzf syntax](https://github.com/junegunn/fzf#search-syntax) +is supported: + +| Token | Match type | Description | +| --------- | -------------------------- | ------------------------------------ | +| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | +| `'wild` | exact-match (quoted) | Items that include `wild` | +| `^music` | prefix-exact-match | Items that start with `music` | +| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | +| `!fire` | inverse-exact-match | Items that do not include `fire` | +| `!^music` | inverse-prefix-exact-match | Items that do not start with `music` | +| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` | + +A single bar character term acts as an OR operator. For example, the following +query matches entries that start with `core` and end with either `go`, `rb`, +or `py`. + +``` +^core go$ | rb$ | py$ +``` + +This is an advantage over the more simpler `fzy` algorithm, which is also +available for telescope (as native component or as lua component). + +## Installation + +To get **fzf-native** working, you need to build it with either `cmake` or `make`. As of now, we do not ship binaries. +Both install methods will be supported going forward. + +### CMake (Windows, Linux, MacOS) + +This requires: + +- CMake, and the Microsoft C++ Build Tools on Windows +- CMake, make, and GCC or Clang on Linux and MacOS + +#### vim-plug + +```viml +Plug 'nvim-telescope/telescope-fzf-native.nvim', { 'do': 'cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release && cmake --build build --config Release && cmake --install build --prefix build' } +``` + +#### packer.nvim + +```lua +use {'nvim-telescope/telescope-fzf-native.nvim', run = 'cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release && cmake --build build --config Release && cmake --install build --prefix build' } +``` + +### Make (Linux, MacOS, Windows with MinGW) + +This requires `gcc` or `clang` and `make` + +#### vim-plug + +```viml +Plug 'nvim-telescope/telescope-fzf-native.nvim', { 'do': 'make' } +``` + +#### packer.nvim + +```lua +use {'nvim-telescope/telescope-fzf-native.nvim', run = 'make' } +``` + +## Telescope Setup and Configuration: + +```lua +-- You dont need to set any of these options. These are the default ones. Only +-- the loading is important +require('telescope').setup { + extensions = { + fzf = { + fuzzy = true, -- false will only do exact matching + override_generic_sorter = true, -- override the generic sorter + override_file_sorter = true, -- override the file sorter + case_mode = "smart_case", -- or "ignore_case" or "respect_case" + -- the default case_mode is "smart_case" + } + } +} +-- To get fzf loaded and working with telescope, you need to call +-- load_extension, somewhere after setup function: +require('telescope').load_extension('fzf') +``` + +## Developer Interface + +This section is only addressed towards developers who plan to use the library +(c or lua bindings). +This section is not addressed towards users of the telescope extension. + +### C Interface + +```c +fzf_slab_t *slab = fzf_make_default_slab(); +/* fzf_case_mode enum : CaseSmart = 0, CaseIgnore, CaseRespect + * normalize bool : always set to false because its not implemented yet. + * This is reserved for future use + * pattern char* : pattern you want to match. e.g. "src | lua !.c$ + * fuzzy bool : enable or disable fuzzy matching + */ +fzf_pattern_t *pattern = fzf_parse_pattern(CaseSmart, false, "src | lua !.c$", true); + +/* you can get the score/position for as many items as you want */ +int score = fzf_get_score(line, pattern, slab); +fzf_position_t *pos = fzf_get_positions(line, pattern, slab); + +fzf_free_positions(pos); +fzf_free_pattern(pattern); +fzf_free_slab(slab); +``` + +### Lua Interface + +```lua +local fzf = require('fzf_lib') + +local slab = fzf.allocate_slab() +-- pattern: string +-- case_mode: number with 0 = smart_case, 1 = ignore_case, 2 = respect_case +-- fuzzy: enable or disable fuzzy matching. default true +local pattern_obj = fzf.parse_pattern(pattern, case_mode, fuzzy) + +-- you can get the score/position for as many items as you want +-- line: string +-- score: number +local score = fzf.get_score(line, pattern_obj, slab) + +-- table (does not have to be freed) +local pos = fzf.get_pos(line, pattern_obj, slab) + +fzf.free_pattern(pattern_obj) +fzf.free_slab(slab) +``` + +## Disclaimer + +This projects implements **[fzf][fzf]** algorithm in c. So there might be +differences in matching. I don't guarantee completeness. + +### TODO + +Stuff still missing that is present in **[fzf][fzf]**. + +- [ ] normalize +- [ ] case for unicode (i don't think this works currently) + +## Benchmark + +Comparison with fzy-native and fzy-lua with a table containing 240201 file +strings. It calculated the score and position (if score > 0) for each of these +strings with the pattern that is listed below: + +![benchmark 1](https://raw.githubusercontent.com/wiki/nvim-telescope/telescope.nvim/imgs/bench1.png) +![benchmark 2](https://raw.githubusercontent.com/wiki/nvim-telescope/telescope.nvim/imgs/bench2.png) + +## Credit + +All credit for the algorithm goes to junegunn and his work on **[fzf][fzf]**. +This is merely a c fork distributed under MIT for telescope. + +[fzf]: https://github.com/junegunn/fzf diff --git a/bundle/telescope-fzf-native.nvim/lua/fzf_lib.lua b/bundle/telescope-fzf-native.nvim/lua/fzf_lib.lua new file mode 100644 index 000000000..bced3d8df --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/lua/fzf_lib.lua @@ -0,0 +1,85 @@ +local ffi = require "ffi" + +local library_path = (function() + local dirname = string.sub(debug.getinfo(1).source, 2, #"/fzf_lib.lua" * -1) + if package.config:sub(1, 1) == "\\" then + return dirname .. "../build/libfzf.dll" + else + return dirname .. "../build/libfzf.so" + end +end)() +local native = ffi.load(library_path) + +ffi.cdef [[ + typedef struct {} fzf_i16_t; + typedef struct {} fzf_i32_t; + typedef struct { + fzf_i16_t I16; + fzf_i32_t I32; + } fzf_slab_t; + + typedef struct {} fzf_term_set_t; + typedef struct { + fzf_term_set_t **ptr; + size_t size; + size_t cap; + } fzf_pattern_t; + typedef struct { + uint32_t *data; + size_t size; + size_t cap; + } fzf_position_t; + + fzf_position_t *fzf_get_positions(const char *text, fzf_pattern_t *pattern, fzf_slab_t *slab); + void fzf_free_positions(fzf_position_t *pos); + int32_t fzf_get_score(const char *text, fzf_pattern_t *pattern, fzf_slab_t *slab); + + fzf_pattern_t *fzf_parse_pattern(int32_t case_mode, bool normalize, char *pattern, bool fuzzy); + void fzf_free_pattern(fzf_pattern_t *pattern); + + fzf_slab_t *fzf_make_default_slab(void); + void fzf_free_slab(fzf_slab_t *slab); +]] + +local fzf = {} + +fzf.get_score = function(input, pattern_struct, slab) + return native.fzf_get_score(input, pattern_struct, slab) +end + +fzf.get_pos = function(input, pattern_struct, slab) + local pos = native.fzf_get_positions(input, pattern_struct, slab) + if pos == nil then + return + end + + local res = {} + for i = 1, tonumber(pos.size) do + res[i] = pos.data[i - 1] + 1 + end + native.fzf_free_positions(pos) + + return res +end + +fzf.parse_pattern = function(pattern, case_mode, fuzzy) + case_mode = case_mode == nil and 0 or case_mode + fuzzy = fuzzy == nil and true or fuzzy + local c_str = ffi.new("char[?]", #pattern + 1) + ffi.copy(c_str, pattern) + return native.fzf_parse_pattern(case_mode, false, c_str, fuzzy) +end + +fzf.free_pattern = function(p) + native.fzf_free_pattern(p) +end + +fzf.allocate_slab = function() + return native.fzf_make_default_slab() +end + +fzf.free_slab = function(s) + native.fzf_free_slab(s) +end + +return fzf diff --git a/bundle/telescope-fzf-native.nvim/lua/telescope/_extensions/fzf.lua b/bundle/telescope-fzf-native.nvim/lua/telescope/_extensions/fzf.lua new file mode 100644 index 000000000..3b8679fc8 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/lua/telescope/_extensions/fzf.lua @@ -0,0 +1,148 @@ +local fzf = require "fzf_lib" +local sorters = require "telescope.sorters" + +local case_enum = setmetatable({ + ["smart_case"] = 0, + ["ignore_case"] = 1, + ["respect_case"] = 2, +}, { + __index = function(_, k) + error(string.format("%s is not a valid case mode", k)) + end, + __newindex = function() + error "Don't set new things" + end, +}) + +local get_fzf_sorter = function(opts) + local case_mode = case_enum[opts.case_mode] + local fuzzy_mode = opts.fuzzy == nil and true or opts.fuzzy + local post_or = false + local post_inv = false + local post_escape = false + + local get_struct = function(self, prompt) + local struct = self.state.prompt_cache[prompt] + if not struct then + struct = fzf.parse_pattern(prompt, case_mode, fuzzy_mode) + self.state.prompt_cache[prompt] = struct + end + return struct + end + + local clear_filter_fun = function(self, prompt) + local filter = "^(" .. self._delimiter .. "(%S+)" .. "[" .. self._delimiter .. "%s]" .. ")" + local matched = prompt:match(filter) + + if matched == nil then + return prompt + end + return prompt:sub(#matched + 1, -1) + end + + return sorters.Sorter:new { + init = function(self) + self.state.slab = fzf.allocate_slab() + self.state.prompt_cache = {} + + if self.filter_function then + self.__highlight_prefilter = clear_filter_fun + end + end, + destroy = function(self) + for _, v in pairs(self.state.prompt_cache) do + fzf.free_pattern(v) + end + self.state.prompt_cache = {} + if self.state.slab ~= nil then + fzf.free_slab(self.state.slab) + self.state.slab = nil + end + end, + start = function(self, prompt) + local last = prompt:sub(-1, -1) + + if last == "|" then + self._discard_state.filtered = {} + post_or = true + elseif last == " " and post_or then + self._discard_state.filtered = {} + elseif post_or then + self._discard_state.filtered = {} + post_or = false + else + post_or = false + end + + if last == "\\" and not post_escape then + self._discard_state.filtered = {} + post_escape = true + else + self._discard_state.filtered = {} + post_escape = false + end + + if last == "!" and not post_inv then + post_inv = true + self._discard_state.filtered = {} + elseif post_inv then + self._discard_state.filtered = {} + elseif post_inv and " " then + post_inv = false + end + end, + discard = true, + scoring_function = function(self, prompt, line) + local obj = get_struct(self, prompt) + local score = fzf.get_score(line, obj, self.state.slab) + if score == 0 then + return -1 + else + return 1 / score + end + end, + highlighter = function(self, prompt, display) + if self.__highlight_prefilter then + prompt = self:__highlight_prefilter(prompt) + end + return fzf.get_pos(display, get_struct(self, prompt), self.state.slab) + end, + } +end + +local fast_extend = function(opts, conf) + local ret = {} + ret.case_mode = vim.F.if_nil(opts.case_mode, conf.case_mode) + ret.fuzzy = vim.F.if_nil(opts.fuzzy, conf.fuzzy) + return ret +end + +return require("telescope").register_extension { + setup = function(ext_config, config) + local override_file = vim.F.if_nil(ext_config.override_file_sorter, true) + local override_generic = vim.F.if_nil(ext_config.override_generic_sorter, true) + + local conf = {} + conf.case_mode = vim.F.if_nil(ext_config.case_mode, "smart_case") + conf.fuzzy = vim.F.if_nil(ext_config.fuzzy, true) + + if override_file then + config.file_sorter = function(opts) + opts = opts or {} + return get_fzf_sorter(fast_extend(opts, conf)) + end + end + + if override_generic then + config.generic_sorter = function(opts) + opts = opts or {} + return get_fzf_sorter(fast_extend(opts, conf)) + end + end + end, + exports = { + native_fzf_sorter = function(opts) + return get_fzf_sorter(opts or { case_mode = "smart_case", fuzzy = true }) + end, + }, +} diff --git a/bundle/telescope-fzf-native.nvim/src/fzf.c b/bundle/telescope-fzf-native.nvim/src/fzf.c new file mode 100644 index 000000000..6973b24e0 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/src/fzf.c @@ -0,0 +1,1270 @@ +#include "fzf.h" + +#include +#include +#include + +// 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); +#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 { + CharNonWord = 0, + CharLower, + CharUpper, + CharLetter, + CharNumber +} char_types; + +static int32_t index_byte(fzf_string_t *string, char b) { + for (size_t i = 0; i < string->size; i++) { + if (string->data[i] == b) { + return (int32_t)i; + } + } + return -1; +} + +static size_t leading_whitespaces(fzf_string_t *str) { + size_t whitespaces = 0; + for (size_t i = 0; i < str->size; i++) { + if (!isspace((uint8_t)str->data[i])) { + break; + } + whitespaces++; + } + return whitespaces; +} + +static size_t trailing_whitespaces(fzf_string_t *str) { + size_t whitespaces = 0; + for (size_t i = str->size - 1; i >= 0; i--) { + if (!isspace((uint8_t)str->data[i])) { + break; + } + whitespaces++; + } + return whitespaces; +} + +static void copy_runes(fzf_string_t *src, fzf_i32_t *destination) { + for (size_t i = 0; i < src->size; i++) { + destination->data[i] = (int32_t)src->data[i]; + } +} + +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]; + } +} + +// char* helpers +static char *trim_whitespace_left(char *str, size_t *len) { + for (size_t i = 0; i < *len; i++) { + if (str[0] == ' ') { + (*len)--; + str++; + } else { + break; + } + } + return str; +} + +static bool has_prefix(const char *str, const char *prefix, size_t prefix_len) { + return strncmp(prefix, str, prefix_len) == 0; +} + +static bool has_suffix(const char *str, size_t len, const char *suffix, + size_t suffix_len) { + return len >= suffix_len && + strncmp(slice_str(str, len - suffix_len, len).data, suffix, + suffix_len) == 0; +} + +static char *str_replace_char(char *str, char find, char replace) { + char *current_pos = strchr(str, find); + while (current_pos) { + *current_pos = replace; + current_pos = strchr(current_pos, find); + } + return str; +} + +static char *str_replace(char *orig, char *rep, char *with) { + if (!orig || !rep || !with) { + return NULL; + } + + char *result; + char *ins; + char *tmp; + + size_t len_rep = strlen(rep); + size_t len_front = 0; + size_t len_orig = strlen(orig); + size_t len_with = strlen(with); + size_t count = 0; + + if (len_rep == 0) { + return NULL; + } + + ins = orig; + for (; (tmp = strstr(ins, rep)); ++count) { + ins = tmp + len_rep; + } + + tmp = result = (char *)malloc(len_orig + (len_with - len_rep) * count + 1); + if (!result) { + return NULL; + } + + while (count--) { + ins = strstr(orig, rep); + len_front = (size_t)(ins - orig); + tmp = strncpy(tmp, orig, len_front) + len_front; + tmp = strcpy(tmp, with) + len_with; + orig += len_front + len_rep; + len_orig -= len_front + len_rep; + } + strncpy(tmp, orig, len_orig); + tmp[len_orig] = 0; + return result; +} + +// TODO(conni2461): REFACTOR +static char *str_tolower(char *str, size_t size) { + char *lower_str = (char *)malloc((size + 1) * sizeof(char)); + for (size_t i = 0; i < size; i++) { + lower_str[i] = (char)tolower((uint8_t)str[i]); + } + lower_str[size] = '\0'; + return lower_str; +} + +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] = 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] = 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 char_class_of_ascii(char ch) { + if (ch >= 'a' && ch <= 'z') { + return CharLower; + } + if (ch >= 'A' && ch <= 'Z') { + return CharUpper; + } + if (ch >= '0' && ch <= '9') { + return CharNumber; + } + return CharNonWord; +} + +// static char_class char_class_of_non_ascii(char ch) { +// return 0; +// } + +static char_class char_class_of(char ch) { + return char_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 == CharNonWord && class != CharNonWord) { + return BonusBoundary; + } + if ((prev_class == CharLower && class == CharUpper) || + (prev_class != CharNumber && class == CharNumber)) { + return BonusCamel123; + } + if (class == CharNonWord) { + return BonusNonWord; + } + return 0; +} + +static int16_t bonus_at(fzf_string_t *input, size_t idx) { + if (idx == 0) { + return BonusBoundary; + } + return bonus_for(char_class_of(input->data[idx - 1]), + char_class_of(input->data[idx])); +} + +/* TODO(conni2461): maybe just not do this */ +static char normalize_rune(char r) { + // TODO(conni2461) + /* if (r < 0x00C0 || r > 0x2184) { */ + /* return r; */ + /* } */ + /* rune n = normalized[r]; */ + /* if n > 0 { */ + /* return n; */ + /* } */ + return r; +} + +static int32_t try_skip(fzf_string_t *input, bool case_sensitive, byte b, + int32_t from) { + str_slice_t slice = slice_str(input->data, (size_t)from, input->size); + fzf_string_t byte_array = {.data = slice.data, .size = slice.size}; + int32_t idx = index_byte(&byte_array, b); + if (idx == 0) { + return from; + } + + if (!case_sensitive && b >= 'a' && b <= 'z') { + if (idx > 0) { + str_slice_t tmp = slice_str_right(byte_array.data, (size_t)idx); + byte_array.data = tmp.data; + byte_array.size = tmp.size; + } + int32_t uidx = index_byte(&byte_array, b - (byte)32); + if (uidx >= 0) { + idx = uidx; + } + } + if (idx < 0) { + return -1; + } + + return from + idx; +} + +static bool is_ascii(const char *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 ascii_fuzzy_index(fzf_string_t *input, const char *pattern, + size_t size, bool case_sensitive) { + if (!is_ascii(pattern, size)) { + return -1; + } + + int32_t first_idx = 0; + int32_t idx = 0; + for (size_t pidx = 0; pidx < size; pidx++) { + idx = try_skip(input, case_sensitive, pattern[pidx], idx); + if (idx < 0) { + return -1; + } + if (pidx == 0 && idx > 0) { + first_idx = idx - 1; + } + idx++; + } + + return first_idx; +} + +static int32_t calculate_score(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_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 = CharNonWord; + if (sidx > 0) { + prev_class = char_class_of(text->data[sidx - 1]); + } + for (size_t idx = sidx; idx < eidx; idx++) { + char c = text->data[idx]; + int32_t class = char_class_of(c); + if (!case_sensitive) { + /* TODO(conni2461): He does some unicode stuff here, investigate */ + c = (char)tolower((uint8_t)c); + } + if (normalize) { + c = normalize_rune(c); + } + if (c == 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 fzf_fuzzy_match_v1(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_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 (ascii_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++) { + char c = 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 = (char)tolower((uint8_t)c); + } + if (normalize) { + c = normalize_rune(c); + } + if (c == 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--) { + char c = text->data[idx]; + if (!case_sensitive) { + /* TODO(conni2461): He does some unicode stuff here, investigate */ + c = (char)tolower((uint8_t)c); + } + if (c == pattern->data[pidx]) { + pidx--; + if (pidx < 0) { + start = idx; + break; + } + } + } + + int32_t score = calculate_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 fzf_fuzzy_match_v2(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_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 fzf_fuzzy_match_v1(case_sensitive, normalize, text, pattern, pos, + slab); + } + + size_t idx; + { + int32_t tmp_idx = ascii_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); + // Rune array + fzf_i32_t t = alloc32(&offset32, slab, N); + copy_runes(text, &t); // input.CopyRunes(T) + + // 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; + + char pchar0 = pattern->data[0]; + char pchar = pattern->data[0]; + int16_t prev_h0 = 0; + int32_t prev_class = CharNonWord; + bool in_gap = false; + + i32_slice_t t_sub = slice_i32(t.data, idx, t.size); // T[idx:]; + 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; + char c = (char)t_sub.data[off]; + class = char_class_of_ascii(c); + if (!case_sensitive && class == CharUpper) { + /* TODO(conni2461): unicode support */ + c = (char)tolower((uint8_t)c); + } + if (normalize) { + c = normalize_rune(c); + } + + t_sub.data[off] = (uint8_t)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 = 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(t); + free_alloc(f); + free_alloc(bo); + free_alloc(c0); + free_alloc(h0); + return (fzf_result_t){-1, -1, 0}; + } + if (M == 1) { + free_alloc(t); + 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); + str_slice_t p_sub = + slice_str_right(slice_str(pattern->data, 1, M).data, f_sub.size); + for (size_t off = 0; off < f_sub.size; off++) { + size_t f = (size_t)f_sub.data[off]; + pchar = p_sub.data[off]; + pidx = off + 1; + size_t row = pidx * width; + in_gap = false; + t_sub = slice_i32(t.data, f, last_idx + 1); + b_sub = slice_i16_right(slice_i16(bo.data, f, bo.size).data, 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++) { + char ch = (char)t_sub.data[j]; + 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(t); + 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 fzf_exact_match_naive(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_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 (ascii_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++) { + char c = text->data[idx]; + if (!case_sensitive) { + /* TODO(conni2461): He does some unicode stuff here, investigate */ + c = (char)tolower((uint8_t)c); + } + if (normalize) { + c = normalize_rune(c); + } + if (c == pattern->data[pidx]) { + if (pidx == 0) { + bonus = bonus_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 = calculate_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 fzf_prefix_match(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_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 (!isspace((uint8_t)pattern->data[0])) { + trimmed_len = leading_whitespaces(text); + } + if (text->size - trimmed_len < M) { + return (fzf_result_t){-1, -1, 0}; + } + for (size_t i = 0; i < M; i++) { + char c = text->data[trimmed_len + i]; + if (!case_sensitive) { + c = (char)tolower((uint8_t)c); + } + if (normalize) { + c = normalize_rune(c); + } + if (c != pattern->data[i]) { + return (fzf_result_t){-1, -1, 0}; + } + } + size_t start = trimmed_len; + size_t end = trimmed_len + M; + int32_t score = calculate_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 fzf_suffix_match(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_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 || !isspace((uint8_t)pattern->data[M - 1])) { + trimmed_len -= trailing_whitespaces(text); + } + 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++) { + char c = text->data[idx + diff]; + if (!case_sensitive) { + c = (char)tolower((uint8_t)c); + } + if (normalize) { + c = normalize_rune(c); + } + if (c != pattern->data[idx]) { + return (fzf_result_t){-1, -1, 0}; + } + } + size_t start = trimmed_len - M; + size_t end = trimmed_len; + int32_t score = calculate_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 fzf_equal_match(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_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 = leading_whitespaces(text); + size_t trimmed_end_len = trailing_whitespaces(text); + + 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++) { + char pchar = pattern->data[idx]; + char c = text->data[trimmed_len + idx]; + if (!case_sensitive) { + c = (char)tolower((uint8_t)c); + } + if (normalize_rune(c) != normalize_rune(pchar)) { + match = false; + break; + } + } + } else { + // TODO(conni2461): to rune + for (size_t idx = 0; idx < M; idx++) { + char pchar = pattern->data[idx]; + char c = text->data[trimmed_len + idx]; + if (!case_sensitive) { + c = (char)tolower((uint8_t)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 append_set(fzf_term_set_t *set, fzf_term_t value) { + if (set->cap == 0) { + set->cap = 1; + set->ptr = (fzf_term_t *)malloc(sizeof(fzf_term_t)); + } else if (set->size + 1 > set->cap) { + set->cap *= 2; + set->ptr = realloc(set->ptr, sizeof(fzf_term_t) * set->cap); + } + set->ptr[set->size] = value; + set->size++; +} + +static void append_pattern(fzf_pattern_t *pattern, fzf_term_set_t *value) { + if (pattern->cap == 0) { + pattern->cap = 1; + pattern->ptr = (fzf_term_set_t **)malloc(sizeof(fzf_term_set_t *)); + } else if (pattern->size + 1 > pattern->cap) { + pattern->cap *= 2; + pattern->ptr = + realloc(pattern->ptr, sizeof(fzf_term_set_t *) * pattern->cap); + } + pattern->ptr[pattern->size] = value; + pattern->size++; +} + +#define CALL_ALG(term, normalize, input, pos, slab) \ + term->fn((term)->case_sensitive, normalize, &(input), \ + (fzf_string_t *)(term)->text, pos, slab) + +// TODO(conni2461): REFACTOR +/* assumption (maybe i change that later) + * - always v2 alg + * - bool extended always true (thats the whole point of this isn't it) + */ +fzf_pattern_t *fzf_parse_pattern(fzf_case_types case_mode, bool normalize, + char *pattern, bool fuzzy) { + fzf_pattern_t *pat_obj = (fzf_pattern_t *)malloc(sizeof(fzf_pattern_t)); + memset(pat_obj, 0, sizeof(*pat_obj)); + + size_t pat_len = strlen(pattern); + if (pat_len == 0) { + return pat_obj; + } + pattern = trim_whitespace_left(pattern, &pat_len); + while (has_suffix(pattern, pat_len, " ", 1) && + !has_suffix(pattern, pat_len, "\\ ", 2)) { + pattern[pat_len - 1] = 0; + pat_len--; + } + + char *pattern_copy = str_replace(pattern, "\\ ", "\t"); + const char *delim = " "; + char *ptr = strtok(pattern_copy, delim); + + fzf_term_set_t *set = (fzf_term_set_t *)malloc(sizeof(fzf_term_set_t)); + memset(set, 0, sizeof(*set)); + + bool switch_set = false; + bool after_bar = false; + while (ptr != NULL) { + fzf_algo_t fn = fzf_fuzzy_match_v2; + bool inv = false; + + size_t len = strlen(ptr); + str_replace_char(ptr, '\t', ' '); + char *text = strdup(ptr); + + char *og_str = text; + char *lower_text = str_tolower(text, len); + bool case_sensitive = + case_mode == CaseRespect || + (case_mode == CaseSmart && strcmp(text, lower_text) != 0); + if (!case_sensitive) { + SFREE(text); + text = lower_text; + og_str = lower_text; + } else { + SFREE(lower_text); + } + if (!fuzzy) { + fn = fzf_exact_match_naive; + } + if (set->size > 0 && !after_bar && strcmp(text, "|") == 0) { + switch_set = false; + after_bar = true; + ptr = strtok(NULL, delim); + SFREE(og_str); + continue; + } + after_bar = false; + if (has_prefix(text, "!", 1)) { + inv = true; + fn = fzf_exact_match_naive; + text++; + len--; + } + + if (strcmp(text, "$") != 0 && has_suffix(text, len, "$", 1)) { + fn = fzf_suffix_match; + text[len - 1] = 0; + len--; + } + + if (has_prefix(text, "'", 1)) { + if (fuzzy && !inv) { + fn = fzf_exact_match_naive; + text++; + len--; + } else { + fn = fzf_fuzzy_match_v2; + text++; + len--; + } + } else if (has_prefix(text, "^", 1)) { + if (fn == fzf_suffix_match) { + fn = fzf_equal_match; + } else { + fn = fzf_prefix_match; + } + text++; + len--; + } + + if (len > 0) { + if (switch_set) { + append_pattern(pat_obj, set); + set = (fzf_term_set_t *)malloc(sizeof(fzf_term_set_t)); + set->cap = 0; + set->size = 0; + } + fzf_string_t *text_ptr = (fzf_string_t *)malloc(sizeof(fzf_string_t)); + text_ptr->data = text; + text_ptr->size = len; + append_set(set, (fzf_term_t){.fn = fn, + .inv = inv, + .ptr = og_str, + .text = text_ptr, + .case_sensitive = case_sensitive}); + switch_set = true; + } else { + SFREE(og_str); + } + + ptr = strtok(NULL, delim); + } + if (set->size > 0) { + append_pattern(pat_obj, set); + } else { + SFREE(set->ptr); + SFREE(set); + } + bool only = true; + for (size_t i = 0; i < pat_obj->size; i++) { + fzf_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); + return pat_obj; +} + +void fzf_free_pattern(fzf_pattern_t *pattern) { + if (pattern->ptr) { + for (size_t i = 0; i < pattern->size; i++) { + fzf_term_set_t *term_set = pattern->ptr[i]; + for (size_t j = 0; j < term_set->size; j++) { + fzf_term_t *term = &term_set->ptr[j]; + free(term->ptr); + free(term->text); + } + free(term_set->ptr); + free(term_set); + } + free(pattern->ptr); + } + SFREE(pattern); +} + +int32_t fzf_get_score(const char *text, fzf_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; + } + + fzf_string_t input = {.data = text, .size = strlen(text)}; + if (pattern->only_inv) { + int final = 0; + for (size_t i = 0; i < pattern->size; i++) { + fzf_term_set_t *term_set = pattern->ptr[i]; + fzf_term_t *term = &term_set->ptr[0]; + + final += CALL_ALG(term, false, input, NULL, slab).score; + } + return (final > 0) ? 0 : 1; + } + + int32_t total_score = 0; + for (size_t i = 0; i < pattern->size; i++) { + fzf_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++) { + fzf_term_t *term = &term_set->ptr[j]; + fzf_result_t res = CALL_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 *fzf_get_positions(const char *text, fzf_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; + } + + fzf_string_t input = {.data = text, .size = strlen(text)}; + fzf_position_t *all_pos = fzf_pos_array(0); + for (size_t i = 0; i < pattern->size; i++) { + fzf_term_set_t *term_set = pattern->ptr[i]; + bool matched = false; + for (size_t j = 0; j < term_set->size; j++) { + fzf_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 = CALL_ALG(term, false, input, NULL, slab); + if (res.start < 0) { + matched = true; + } + continue; + } + fzf_result_t res = CALL_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); + } +} diff --git a/bundle/telescope-fzf-native.nvim/src/fzf.h b/bundle/telescope-fzf-native.nvim/src/fzf.h new file mode 100644 index 000000000..7bc16473c --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/src/fzf.h @@ -0,0 +1,111 @@ +#ifndef FZF_H_ +#define FZF_H_ + +#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 { + const char *data; + size_t size; +} fzf_string_t; + +typedef fzf_result_t (*fzf_algo_t)(bool, bool, fzf_string_t *, fzf_string_t *, + fzf_position_t *, fzf_slab_t *); + +typedef enum { CaseSmart = 0, CaseIgnore, CaseRespect } fzf_case_types; + +typedef struct { + fzf_algo_t fn; + bool inv; + char *ptr; + void *text; + bool case_sensitive; +} fzf_term_t; + +typedef struct { + fzf_term_t *ptr; + size_t size; + size_t cap; +} fzf_term_set_t; + +typedef struct { + fzf_term_set_t **ptr; + size_t size; + size_t cap; + bool only_inv; +} fzf_pattern_t; + +fzf_result_t fzf_fuzzy_match_v1(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +fzf_result_t fzf_fuzzy_match_v2(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +fzf_result_t fzf_exact_match_naive(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +fzf_result_t fzf_prefix_match(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +fzf_result_t fzf_suffix_match(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); +fzf_result_t fzf_equal_match(bool case_sensitive, bool normalize, + fzf_string_t *text, fzf_string_t *pattern, + fzf_position_t *pos, fzf_slab_t *slab); + +/* interface */ +fzf_pattern_t *fzf_parse_pattern(fzf_case_types case_mode, bool normalize, + char *pattern, bool fuzzy); +void fzf_free_pattern(fzf_pattern_t *pattern); + +int32_t fzf_get_score(const char *text, fzf_pattern_t *pattern, + fzf_slab_t *slab); + +fzf_position_t *fzf_pos_array(size_t len); +fzf_position_t *fzf_get_positions(const char *text, fzf_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_ diff --git a/bundle/telescope-fzf-native.nvim/test/fzf_lib_spec.lua b/bundle/telescope-fzf-native.nvim/test/fzf_lib_spec.lua new file mode 100644 index 000000000..f5ccdae33 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/test/fzf_lib_spec.lua @@ -0,0 +1,111 @@ +local fzf = require "fzf_lib" +local eq = assert.are.same +local is_nil = assert.is_nil + +describe("fzf", function() + local slab = fzf.allocate_slab() + it("can get the score for simple pattern", function() + local p = fzf.parse_pattern("fzf", 0) + eq(80, fzf.get_score("src/fzf", p, slab)) + eq(0, fzf.get_score("asdf", p, slab)) + eq(54, fzf.get_score("fasdzasdf", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the score for or pattern", function() + local p = fzf.parse_pattern("lua | src | 'doc | ^asdfasdf | file$", 0) + eq(80, fzf.get_score("src/fzf.c", p, slab)) + eq(0, fzf.get_score("build/libfzf", p, slab)) + eq(80, fzf.get_score("lua/fzf_lib.lua", p, slab)) + eq(80, fzf.get_score("doc/fzf.txt", p, slab)) + eq(0, fzf.get_score("daonc/fzf.txt", p, slab)) + eq(200, fzf.get_score("asdfasdf", p, slab)) + eq(0, fzf.get_score("noasdfasdf", p, slab)) + eq(104, fzf.get_score("not_file", p, slab)) + eq(0, fzf.get_score("not_file.txt", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the score for and pattern", function() + local p = fzf.parse_pattern("fzf !lib", 0) + eq(80, fzf.get_score("src/fzf.c", p, slab)) + eq(0, fzf.get_score("lua/fzf_lib.lua", p, slab)) + eq(0, fzf.get_score("build/libfzf", p, slab)) + fzf.free_pattern(p) + + local p = fzf.parse_pattern("fzf src c", 0) + eq(192, fzf.get_score("src/fzf.c", p, slab)) + eq(0, fzf.get_score("lua/fzf_lib.lua", p, slab)) + eq(0, fzf.get_score("build/libfzf", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the score for patterns with escaped space", function() + local p = fzf.parse_pattern("\\ ", 0) + eq(32, fzf.get_score("src file", p, slab)) + eq(0, fzf.get_score("src_file", p, slab)) + eq(32, fzf.get_score("another another file", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the score for issue 11", function() + local p = fzf.parse_pattern("feature/1337-some-times-i-have-a-lot-of-hyphens", 0) + eq(1136, fzf.get_score("feature/1337-some-times-i-have-a-lot-of-hyphens", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the pos for simple pattern", function() + local p = fzf.parse_pattern("fzf", 0) + eq({ 7, 6, 5 }, fzf.get_pos("src/fzf", p, slab)) + is_nil(fzf.get_pos("asdf", p, slab)) + eq({ 9, 5, 1 }, fzf.get_pos("fasdzasdf", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the pos for or pattern", function() + local p = fzf.parse_pattern("lua | src | 'doc | ^asdfasdf | file$", 0) + eq({ 3, 2, 1 }, fzf.get_pos("src/fzf.c", p, slab)) + is_nil(fzf.get_pos("build/libfzf", p, slab)) + eq({ 3, 2, 1 }, fzf.get_pos("lua/fzf_lib.lua", p, slab)) + eq({ 1, 2, 3 }, fzf.get_pos("doc/fzf.txt", p, slab)) + is_nil(fzf.get_pos("daonc/fzf.txt", p, slab)) + eq({ 1, 2, 3, 4, 5, 6, 7, 8 }, fzf.get_pos("asdfasdf", p, slab)) + is_nil(fzf.get_pos("noasdfasdf", p, slab)) + eq({ 5, 6, 7, 8 }, fzf.get_pos("not_file", p, slab)) + is_nil(fzf.get_pos("not_file.txt", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the pos for and pattern", function() + local p = fzf.parse_pattern("fzf !lib", 0) + eq({ 7, 6, 5 }, fzf.get_pos("src/fzf.c", p, slab)) + is_nil(fzf.get_pos("lua/fzf_lib.lua", p, slab)) + is_nil(fzf.get_pos("build/libfzf", p, slab)) + fzf.free_pattern(p) + + p = fzf.parse_pattern("fzf src c", 0) + eq({ 7, 6, 5, 3, 2, 1, 9 }, fzf.get_pos("src/fzf.c", p, slab)) + is_nil(fzf.get_pos("lua/fzf_lib.lua", p, slab)) + is_nil(fzf.get_pos("build/libfzf", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the pos for patterns with escaped space", function() + local p = fzf.parse_pattern("\\ ", 0) + eq({ 4 }, fzf.get_pos("src file", p, slab)) + is_nil(fzf.get_pos("src_file", p, slab)) + eq({ 8 }, fzf.get_pos("another another file", p, slab)) + fzf.free_pattern(p) + end) + + it("can get the pos for issue 11", function() + local p = fzf.parse_pattern("feature/1337-some-times-i-have-a-lot-of-hyphens", 0) + local expected = {} + for i = 47, 1, -1 do + table.insert(expected, i) + end + eq(expected, fzf.get_pos("feature/1337-some-times-i-have-a-lot-of-hyphens", p, slab)) + fzf.free_pattern(p) + end) + fzf.free_slab(slab) +end) diff --git a/bundle/telescope-fzf-native.nvim/test/minrc.vim b/bundle/telescope-fzf-native.nvim/test/minrc.vim new file mode 100644 index 000000000..da29e2f95 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/test/minrc.vim @@ -0,0 +1,4 @@ +set rtp+=. +set rtp+=../plenary.nvim/ + +runtime! plugin/plenary.vim diff --git a/bundle/telescope-fzf-native.nvim/test/test.c b/bundle/telescope-fzf-native.nvim/test/test.c new file mode 100644 index 000000000..c22961a61 --- /dev/null +++ b/bundle/telescope-fzf-native.nvim/test/test.c @@ -0,0 +1,771 @@ +#include "fzf.h" + +#include +#include +#include + +typedef enum { + ScoreMatch = 16, + ScoreGapStart = -3, + ScoreGapExtension = -1, + BonusBoundary = ScoreMatch / 2, + BonusNonWord = ScoreMatch / 2, + BonusCamel123 = BonusBoundary + ScoreGapExtension, + BonusConsecutive = -(ScoreGapStart + ScoreGapExtension), + BonusFirstCharMultiplier = 2, +} score_t; + +#define call_alg(alg, case, txt, pat, assert_block) \ + { \ + fzf_position_t *pos = fzf_pos_array(0); \ + fzf_result_t res = alg(case, false, txt, pat, pos, NULL); \ + assert_block; \ + fzf_free_positions(pos); \ + } \ + { \ + fzf_position_t *pos = fzf_pos_array(0); \ + fzf_slab_t *slab = fzf_make_default_slab(); \ + fzf_result_t res = alg(case, false, txt, pat, pos, slab); \ + assert_block; \ + fzf_free_positions(pos); \ + fzf_free_slab(slab); \ + } + +static int8_t max_i8(int8_t a, int8_t b) { + return a > b ? a : b; +} + +#define MATCH_WRAPPER(nn, og) \ + fzf_result_t nn(bool case_sensitive, bool normalize, const char *text, \ + const char *pattern, fzf_position_t *pos, \ + fzf_slab_t *slab) { \ + fzf_string_t input = {.data = text, .size = strlen(text)}; \ + fzf_string_t pattern_wrap = {.data = pattern, .size = strlen(pattern)}; \ + return og(case_sensitive, normalize, &input, &pattern_wrap, pos, slab); \ + } + +MATCH_WRAPPER(fuzzy_match_v2, fzf_fuzzy_match_v2); +MATCH_WRAPPER(fuzzy_match_v1, fzf_fuzzy_match_v1); +MATCH_WRAPPER(exact_match_naive, fzf_exact_match_naive); +MATCH_WRAPPER(prefix_match, fzf_prefix_match); +MATCH_WRAPPER(suffix_match, fzf_suffix_match); +MATCH_WRAPPER(equal_match, fzf_equal_match); + +// TODO(conni2461): Implement normalize and test it here +TEST(FuzzyMatchV2, case1) { + call_alg(fuzzy_match_v2, true, "So Danco Samba", "So", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(2, res.end); + ASSERT_EQ(56, res.score); + + ASSERT_EQ(2, pos->size); + ASSERT_EQ(1, pos->data[0]); + ASSERT_EQ(0, pos->data[1]); + }); +} + +TEST(FuzzyMatchV2, case2) { + call_alg(fuzzy_match_v2, false, "So Danco Samba", "sodc", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(7, res.end); + ASSERT_EQ(89, res.score); + + ASSERT_EQ(4, pos->size); + ASSERT_EQ(6, pos->data[0]); + ASSERT_EQ(3, pos->data[1]); + ASSERT_EQ(1, pos->data[2]); + ASSERT_EQ(0, pos->data[3]); + }); +} + +TEST(FuzzyMatchV2, case3) { + call_alg(fuzzy_match_v2, false, "Danco", "danco", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(5, res.end); + ASSERT_EQ(128, res.score); + + ASSERT_EQ(5, pos->size); + ASSERT_EQ(4, pos->data[0]); + ASSERT_EQ(3, pos->data[1]); + ASSERT_EQ(2, pos->data[2]); + ASSERT_EQ(1, pos->data[3]); + ASSERT_EQ(0, pos->data[4]); + }); +} + +TEST(FuzzyMatchV2, case4) { + call_alg(fuzzy_match_v2, false, "fooBarbaz1", "obz", { + ASSERT_EQ(2, res.start); + ASSERT_EQ(9, res.end); + int expected_score = + ScoreMatch * 3 + BonusCamel123 + ScoreGapStart + ScoreGapExtension * 3; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case5) { + call_alg(fuzzy_match_v2, false, "foo bar baz", "fbb", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(9, res.end); + int expected_score = + ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case6) { + call_alg(fuzzy_match_v2, false, "/AutomatorDocument.icns", "rdoc", { + ASSERT_EQ(9, res.start); + ASSERT_EQ(13, res.end); + int expected_score = ScoreMatch * 4 + BonusCamel123 + BonusConsecutive * 2; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case7) { + call_alg(fuzzy_match_v2, false, "/man1/zshcompctl.1", "zshc", { + ASSERT_EQ(6, res.start); + ASSERT_EQ(10, res.end); + int expected_score = ScoreMatch * 4 + + BonusBoundary * BonusFirstCharMultiplier + + BonusBoundary * 3; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case8) { + call_alg(fuzzy_match_v2, false, "/.oh-my-zsh/cache", "zshc", { + ASSERT_EQ(8, res.start); + ASSERT_EQ(13, res.end); + int expected_score = ScoreMatch * 4 + + BonusBoundary * BonusFirstCharMultiplier + + BonusBoundary * 3 + ScoreGapStart; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case9) { + call_alg(fuzzy_match_v2, false, "ab0123 456", "12356", { + ASSERT_EQ(3, res.start); + ASSERT_EQ(10, res.end); + int expected_score = ScoreMatch * 5 + BonusConsecutive * 3 + ScoreGapStart + + ScoreGapExtension; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case10) { + call_alg(fuzzy_match_v2, false, "abc123 456", "12356", { + ASSERT_EQ(3, res.start); + ASSERT_EQ(10, res.end); + int expected_score = ScoreMatch * 5 + + BonusCamel123 * BonusFirstCharMultiplier + + BonusCamel123 * 2 + BonusConsecutive + ScoreGapStart + + ScoreGapExtension; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case11) { + call_alg(fuzzy_match_v2, false, "foo/bar/baz", "fbb", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(9, res.end); + int expected_score = + ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case12) { + call_alg(fuzzy_match_v2, false, "fooBarBaz", "fbb", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(7, res.end); + int expected_score = + ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + + BonusCamel123 * 2 + 2 * ScoreGapStart + 2 * ScoreGapExtension; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case13) { + call_alg(fuzzy_match_v2, false, "foo barbaz", "fbb", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(8, res.end); + int expected_score = + ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + + BonusBoundary + ScoreGapStart * 2 + ScoreGapExtension * 3; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case14) { + call_alg(fuzzy_match_v2, false, "fooBar Baz", "foob", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(4, res.end); + int expected_score = ScoreMatch * 4 + + BonusBoundary * BonusFirstCharMultiplier + + BonusBoundary * 3; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case15) { + call_alg(fuzzy_match_v2, false, "xFoo-Bar Baz", "foo-b", { + ASSERT_EQ(1, res.start); + ASSERT_EQ(6, res.end); + int expected_score = ScoreMatch * 5 + + BonusCamel123 * BonusFirstCharMultiplier + + BonusCamel123 * 2 + BonusNonWord + BonusBoundary; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case16) { + call_alg(fuzzy_match_v2, true, "fooBarbaz", "oBz", { + ASSERT_EQ(2, res.start); + ASSERT_EQ(9, res.end); + int expected_score = + ScoreMatch * 3 + BonusCamel123 + ScoreGapStart + ScoreGapExtension * 3; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case17) { + call_alg(fuzzy_match_v2, true, "Foo/Bar/Baz", "FBB", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(9, res.end); + int expected_score = ScoreMatch * 3 + + BonusBoundary * (BonusFirstCharMultiplier + 2) + + ScoreGapStart * 2 + ScoreGapExtension * 4; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case18) { + call_alg(fuzzy_match_v2, true, "FooBarBaz", "FBB", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(7, res.end); + int expected_score = + ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + + BonusCamel123 * 2 + ScoreGapStart * 2 + ScoreGapExtension * 2; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case19) { + call_alg(fuzzy_match_v2, true, "FooBar Baz", "FooB", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(4, res.end); + int expected_score = + ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + + BonusBoundary * 2 + max_i8(BonusCamel123, BonusBoundary); + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case20) { + call_alg(fuzzy_match_v2, true, "foo-bar", "o-ba", { + ASSERT_EQ(2, res.start); + ASSERT_EQ(6, res.end); + int expected_score = ScoreMatch * 4 + BonusBoundary * 3; + ASSERT_EQ(expected_score, res.score); + }); +} + +TEST(FuzzyMatchV2, case21) { + call_alg(fuzzy_match_v2, true, "fooBarbaz", "oBZ", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(FuzzyMatchV2, case22) { + call_alg(fuzzy_match_v2, true, "Foo Bar Baz", "fbb", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(FuzzyMatchV2, case23) { + call_alg(fuzzy_match_v2, true, "fooBarbaz", "fooBarbazz", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(FuzzyMatchV1, case1) { + call_alg(fuzzy_match_v1, true, "So Danco Samba", "So", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(2, res.end); + ASSERT_EQ(56, res.score); + + ASSERT_EQ(2, pos->size); + ASSERT_EQ(0, pos->data[0]); + ASSERT_EQ(1, pos->data[1]); + }); +} + +TEST(FuzzyMatchV1, case2) { + call_alg(fuzzy_match_v1, false, "So Danco Samba", "sodc", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(7, res.end); + ASSERT_EQ(89, res.score); + + ASSERT_EQ(4, pos->size); + ASSERT_EQ(0, pos->data[0]); + ASSERT_EQ(1, pos->data[1]); + ASSERT_EQ(3, pos->data[2]); + ASSERT_EQ(6, pos->data[3]); + }); +} + +TEST(FuzzyMatchV1, case3) { + call_alg(fuzzy_match_v1, false, "Danco", "danco", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(5, res.end); + ASSERT_EQ(128, res.score); + + ASSERT_EQ(5, pos->size); + ASSERT_EQ(0, pos->data[0]); + ASSERT_EQ(1, pos->data[1]); + ASSERT_EQ(2, pos->data[2]); + ASSERT_EQ(3, pos->data[3]); + ASSERT_EQ(4, pos->data[4]); + }); +} + +TEST(ExactMatch, case1) { + call_alg(exact_match_naive, true, "So Danco Samba", "So", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(2, res.end); + ASSERT_EQ(56, res.score); + }); +} + +TEST(ExactMatch, case2) { + call_alg(exact_match_naive, false, "So Danco Samba", "sodc", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(ExactMatch, case3) { + call_alg(exact_match_naive, false, "Danco", "danco", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(5, res.end); + ASSERT_EQ(128, res.score); + }); +} + +TEST(PrefixMatch, case1) { + call_alg(prefix_match, true, "So Danco Samba", "So", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(2, res.end); + ASSERT_EQ(56, res.score); + }); +} + +TEST(PrefixMatch, case2) { + call_alg(prefix_match, false, "So Danco Samba", "sodc", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(PrefixMatch, case3) { + call_alg(prefix_match, false, "Danco", "danco", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(5, res.end); + ASSERT_EQ(128, res.score); + }); +} + +TEST(SuffixMatch, case1) { + call_alg(suffix_match, true, "So Danco Samba", "So", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(SuffixMatch, case2) { + call_alg(suffix_match, false, "So Danco Samba", "sodc", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(SuffixMatch, case3) { + call_alg(suffix_match, false, "Danco", "danco", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(5, res.end); + ASSERT_EQ(128, res.score); + }); +} + +TEST(EqualMatch, case1) { + call_alg(equal_match, true, "So Danco Samba", "So", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(EqualMatch, case2) { + call_alg(equal_match, false, "So Danco Samba", "sodc", { + ASSERT_EQ(-1, res.start); + ASSERT_EQ(-1, res.end); + ASSERT_EQ(0, res.score); + }); +} + +TEST(EqualMatch, case3) { + call_alg(equal_match, false, "Danco", "danco", { + ASSERT_EQ(0, res.start); + ASSERT_EQ(5, res.end); + ASSERT_EQ(128, res.score); + }); +} + +TEST(PatternParsing, empty) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, "", true); + ASSERT_EQ(0, pat->size); + ASSERT_EQ(0, pat->cap); + ASSERT_FALSE(pat->only_inv); + + fzf_free_pattern(pat); +} + +TEST(PatternParsing, simple) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, "lua", true); + ASSERT_EQ(1, pat->size); + ASSERT_EQ(1, pat->cap); + ASSERT_FALSE(pat->only_inv); + + ASSERT_EQ(1, pat->ptr[0]->size); + ASSERT_EQ(1, pat->ptr[0]->cap); + + ASSERT_EQ((void *)fzf_fuzzy_match_v2, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ("lua", ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[0]->ptr[0].case_sensitive); + fzf_free_pattern(pat); +} + +TEST(PatternParsing, withEscapedSpace) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, "file\\ ", true); + ASSERT_EQ(1, pat->size); + ASSERT_EQ(1, pat->cap); + ASSERT_FALSE(pat->only_inv); + + ASSERT_EQ(1, pat->ptr[0]->size); + ASSERT_EQ(1, pat->ptr[0]->cap); + + ASSERT_EQ((void *)fzf_fuzzy_match_v2, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ("file ", ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[0]->ptr[0].case_sensitive); + fzf_free_pattern(pat); +} + +TEST(PatternParsing, withComplexEscapedSpace) { + fzf_pattern_t *pat = + fzf_parse_pattern(CaseSmart, false, "file\\ with\\ space", true); + ASSERT_EQ(1, pat->size); + ASSERT_EQ(1, pat->cap); + ASSERT_FALSE(pat->only_inv); + + ASSERT_EQ(1, pat->ptr[0]->size); + ASSERT_EQ(1, pat->ptr[0]->cap); + + ASSERT_EQ((void *)fzf_fuzzy_match_v2, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ("file with space", + ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[0]->ptr[0].case_sensitive); + fzf_free_pattern(pat); +} + +TEST(PatternParsing, withEscapedSpaceAndNormalSpace) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, "file\\ new", true); + ASSERT_EQ(2, pat->size); + ASSERT_EQ(2, pat->cap); + ASSERT_FALSE(pat->only_inv); + + ASSERT_EQ(1, pat->ptr[0]->size); + ASSERT_EQ(1, pat->ptr[0]->cap); + ASSERT_EQ(1, pat->ptr[1]->size); + ASSERT_EQ(1, pat->ptr[1]->cap); + + ASSERT_EQ((void *)fzf_fuzzy_match_v2, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ("file ", ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[0]->ptr[0].case_sensitive); + + ASSERT_EQ((void *)fzf_fuzzy_match_v2, pat->ptr[1]->ptr[0].fn); + ASSERT_EQ("new", ((fzf_string_t *)(pat->ptr[1]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[1]->ptr[0].case_sensitive); + fzf_free_pattern(pat); +} + +TEST(PatternParsing, invert) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, "!Lua", true); + ASSERT_EQ(1, pat->size); + ASSERT_EQ(1, pat->cap); + ASSERT_TRUE(pat->only_inv); + + ASSERT_EQ(1, pat->ptr[0]->size); + ASSERT_EQ(1, pat->ptr[0]->cap); + + ASSERT_EQ((void *)fzf_exact_match_naive, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ("Lua", ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_TRUE(pat->ptr[0]->ptr[0].case_sensitive); + ASSERT_TRUE(pat->ptr[0]->ptr[0].inv); + fzf_free_pattern(pat); +} + +TEST(PatternParsing, invertMultiple) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, "!fzf !test", true); + ASSERT_EQ(2, pat->size); + ASSERT_EQ(2, pat->cap); + ASSERT_TRUE(pat->only_inv); + + ASSERT_EQ(1, pat->ptr[0]->size); + ASSERT_EQ(1, pat->ptr[0]->cap); + ASSERT_EQ(1, pat->ptr[1]->size); + ASSERT_EQ(1, pat->ptr[1]->cap); + + ASSERT_EQ((void *)fzf_exact_match_naive, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ("fzf", ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[0]->ptr[0].case_sensitive); + ASSERT_TRUE(pat->ptr[0]->ptr[0].inv); + + ASSERT_EQ((void *)fzf_exact_match_naive, pat->ptr[1]->ptr[0].fn); + ASSERT_EQ("test", ((fzf_string_t *)(pat->ptr[1]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[1]->ptr[0].case_sensitive); + ASSERT_TRUE(pat->ptr[1]->ptr[0].inv); + fzf_free_pattern(pat); +} + +TEST(PatternParsing, smartCase) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, "Lua", true); + ASSERT_EQ(1, pat->size); + ASSERT_EQ(1, pat->cap); + ASSERT_FALSE(pat->only_inv); + + ASSERT_EQ(1, pat->ptr[0]->size); + ASSERT_EQ(1, pat->ptr[0]->cap); + + ASSERT_EQ((void *)fzf_fuzzy_match_v2, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ("Lua", ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_TRUE(pat->ptr[0]->ptr[0].case_sensitive); + fzf_free_pattern(pat); +} + +TEST(PatternParsing, simpleOr) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, "'src | ^Lua", true); + ASSERT_EQ(1, pat->size); + ASSERT_EQ(1, pat->cap); + ASSERT_FALSE(pat->only_inv); + + ASSERT_EQ(2, pat->ptr[0]->size); + ASSERT_EQ(2, pat->ptr[0]->cap); + + ASSERT_EQ((void *)fzf_exact_match_naive, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ("src", ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[0]->ptr[0].case_sensitive); + + ASSERT_EQ((void *)fzf_prefix_match, pat->ptr[0]->ptr[1].fn); + ASSERT_EQ("Lua", ((fzf_string_t *)(pat->ptr[0]->ptr[1].text))->data); + ASSERT_TRUE(pat->ptr[0]->ptr[1].case_sensitive); + fzf_free_pattern(pat); +} + +TEST(PatternParsing, complexAnd) { + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, + ".lua$ 'previewer !'term !asdf", true); + ASSERT_EQ(4, pat->size); + ASSERT_EQ(4, pat->cap); + ASSERT_FALSE(pat->only_inv); + + ASSERT_EQ(1, pat->ptr[0]->size); + ASSERT_EQ(1, pat->ptr[0]->cap); + ASSERT_EQ(1, pat->ptr[1]->size); + ASSERT_EQ(1, pat->ptr[1]->cap); + ASSERT_EQ(1, pat->ptr[2]->size); + ASSERT_EQ(1, pat->ptr[2]->cap); + ASSERT_EQ(1, pat->ptr[3]->size); + ASSERT_EQ(1, pat->ptr[3]->cap); + + ASSERT_EQ((void *)fzf_suffix_match, pat->ptr[0]->ptr[0].fn); + ASSERT_EQ(".lua", ((fzf_string_t *)(pat->ptr[0]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[0]->ptr[0].case_sensitive); + + ASSERT_EQ((void *)fzf_exact_match_naive, pat->ptr[1]->ptr[0].fn); + ASSERT_EQ("previewer", ((fzf_string_t *)(pat->ptr[1]->ptr[0].text))->data); + ASSERT_EQ(0, pat->ptr[1]->ptr[0].case_sensitive); + + ASSERT_EQ((void *)fzf_fuzzy_match_v2, pat->ptr[2]->ptr[0].fn); + ASSERT_EQ("term", ((fzf_string_t *)(pat->ptr[2]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[2]->ptr[0].case_sensitive); + ASSERT_TRUE(pat->ptr[2]->ptr[0].inv); + + ASSERT_EQ((void *)fzf_exact_match_naive, pat->ptr[3]->ptr[0].fn); + ASSERT_EQ("asdf", ((fzf_string_t *)(pat->ptr[3]->ptr[0].text))->data); + ASSERT_FALSE(pat->ptr[3]->ptr[0].case_sensitive); + ASSERT_TRUE(pat->ptr[3]->ptr[0].inv); + fzf_free_pattern(pat); +} + +static void score_wrapper(char *pattern, char **input, int *expected) { + fzf_slab_t *slab = fzf_make_default_slab(); + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, pattern, true); + for (size_t i = 0; input[i] != NULL; ++i) { + ASSERT_EQ(expected[i], fzf_get_score(input[i], pat, slab)); + } + fzf_free_pattern(pat); + fzf_free_slab(slab); +} + +TEST(ScoreIntegration, simple) { + char *input[] = {"fzf", "main.c", "src/fzf", "fz/noooo", NULL}; + int expected[] = {0, 1, 0, 1}; + score_wrapper("!fzf", input, expected); +} + +TEST(ScoreIntegration, invertAnd) { + char *input[] = {"src/fzf.c", "README.md", "lua/asdf", "test/test.c", NULL}; + int expected[] = {0, 1, 1, 0}; + score_wrapper("!fzf !test", input, expected); +} + +TEST(ScoreIntegration, withEscapedSpace) { + char *input[] = {"file ", "file lua", "lua", NULL}; + int expected[] = {0, 200, 0}; + score_wrapper("file\\ lua", input, expected); +} + +TEST(ScoreIntegration, onlyEscapedSpace) { + char *input[] = {"file with space", "file lua", "lua", "src", "test", NULL}; + int expected[] = {32, 32, 0, 0, 0}; + score_wrapper("\\ ", input, expected); +} + +TEST(ScoreIntegration, simpleOr) { + char *input[] = {"src/fzf.h", "README.md", "build/fzf", + "lua/fzf_lib.lua", "Lua/fzf_lib.lua", NULL}; + int expected[] = {80, 0, 0, 0, 80}; + score_wrapper("'src | ^Lua", input, expected); +} + +TEST(ScoreIntegration, complexTerm) { + char *input[] = {"lua/random_previewer", "README.md", + "previewers/utils.lua", "previewers/buffer.lua", + "previewers/term.lua", NULL}; + int expected[] = {0, 0, 328, 328, 0}; + score_wrapper(".lua$ 'previewer !'term", input, expected); +} + +static void pos_wrapper(char *pattern, char **input, int **expected) { + fzf_slab_t *slab = fzf_make_default_slab(); + fzf_pattern_t *pat = fzf_parse_pattern(CaseSmart, false, pattern, true); + for (size_t i = 0; input[i] != NULL; ++i) { + fzf_position_t *pos = fzf_get_positions(input[i], pat, slab); + if (!pos) { + ASSERT_EQ((void *)pos, expected[i]); + continue; + } + + // Verify that the size is correct + if (expected[i]) { + ASSERT_EQ(-1, expected[i][pos->size]); + } else { + ASSERT_EQ(0, pos->size); + } + ASSERT_EQ_MEM(expected[i], pos->data, pos->size * sizeof(pos->data[0])); + fzf_free_positions(pos); + } + fzf_free_pattern(pat); + fzf_free_slab(slab); +} + +TEST(PosIntegration, simple) { + char *input[] = {"src/fzf.c", "src/fzf.h", + "lua/fzf_lib.lua", "lua/telescope/_extensions/fzf.lua", + "README.md", NULL}; + int match1[] = {6, 5, 4, -1}; + int match2[] = {6, 5, 4, -1}; + int match3[] = {6, 5, 4, -1}; + int match4[] = {28, 27, 26, -1}; + int *expected[] = {match1, match2, match3, match4, NULL}; + pos_wrapper("fzf", input, expected); +} + +TEST(PosIntegration, invert) { + char *input[] = {"fzf", "main.c", "src/fzf", "fz/noooo", NULL}; + int *expected[] = {NULL, NULL, NULL, NULL, NULL}; + pos_wrapper("!fzf", input, expected); +} + +TEST(PosIntegration, andWithSecondInvert) { + char *input[] = {"src/fzf.c", "lua/fzf_lib.lua", "build/libfzf", NULL}; + int match1[] = {6, 5, 4, -1}; + int *expected[] = {match1, NULL, NULL}; + pos_wrapper("fzf !lib", input, expected); +} + +TEST(PosIntegration, andAllInvert) { + char *input[] = {"src/fzf.c", "README.md", "lua/asdf", "test/test.c", NULL}; + int *expected[] = {NULL, NULL, NULL, NULL}; + pos_wrapper("!fzf !test", input, expected); +} + +TEST(PosIntegration, withEscapedSpace) { + char *input[] = {"file ", "file lua", "lua", NULL}; + int match1[] = {7, 6, 5, 4, 3, 2, 1, 0, -1}; + int *expected[] = {NULL, match1, NULL}; + pos_wrapper("file\\ lua", input, expected); +} + +TEST(PosIntegration, onlyEscapedSpace) { + char *input[] = {"file with space", "lul lua", "lua", "src", "test", NULL}; + int match1[] = {4, -1}; + int match2[] = {3, -1}; + int *expected[] = {match1, match2, NULL, NULL, NULL}; + pos_wrapper("\\ ", input, expected); +} + +TEST(PosIntegration, simpleOr) { + char *input[] = {"src/fzf.h", "README.md", "build/fzf", + "lua/fzf_lib.lua", "Lua/fzf_lib.lua", NULL}; + int match1[] = {0, 1, 2, -1}; + int match2[] = {0, 1, 2, -1}; + int *expected[] = {match1, NULL, NULL, NULL, match2}; + pos_wrapper("'src | ^Lua", input, expected); +} + +TEST(PosIntegration, orMemLeak) { + char *input[] = {"src/fzf.h", NULL}; + int match1[] = {2, 1, 0, -1}; + int *expected[] = {match1}; + pos_wrapper("src | src", input, expected); +} + +TEST(PosIntegration, complexTerm) { + char *input[] = {"lua/random_previewer", "README.md", + "previewers/utils.lua", "previewers/buffer.lua", + "previewers/term.lua", NULL}; + int match1[] = {16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1}; + int match2[] = {17, 18, 19, 20, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1}; + int *expected[] = {NULL, NULL, match1, match2, NULL}; + pos_wrapper(".lua$ 'previewer !'term", input, expected); +} + +int main(int argc, char **argv) { + exam_init(argc, argv); + return exam_run(); +} diff --git a/config/plugins/telescope.vim b/config/plugins/telescope.vim index a47c563ef..8a6ecad77 100644 --- a/config/plugins/telescope.vim +++ b/config/plugins/telescope.vim @@ -3,25 +3,29 @@ lua require('telescope').load_extension('messages') lua require('telescope').load_extension('project') lua require('telescope').load_extension('scriptnames') lua require('telescope').load_extension('neoyank') +if filereadable(g:_spacevim_root_dir . 'bundle/telescope-fzf-native.nvim/build/libfzf.so') + \ || filereadable(g:_spacevim_root_dir . 'bundle/telescope-fzf-native.nvim/build/libfzf.dll') + lua require('telescope').load_extension('fzf') +endif lua <"] = actions.move_selection_next, - [""] = actions.move_selection_next, - [""] = actions.move_selection_previous, - [""] = actions.move_selection_previous, - [""] = actions.close, - [""] = "which_key" +defaults = { + mappings = { + i = { + -- the default key binding should same as other fuzzy finder layer + -- tab move to next + [""] = actions.move_selection_next, + [""] = actions.move_selection_next, + [""] = actions.move_selection_previous, + [""] = actions.move_selection_previous, + [""] = actions.close, + [""] = "which_key" }, }, sorting_strategy = "ascending", - layout_config = { - prompt_position = "bottom" + layout_config = { + prompt_position = "bottom" } } }