Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5451c09b7 | |||
| 184f245be1 | |||
| 29f81a141f | |||
| d2e3993398 | |||
| a411100fa4 | |||
| 0a993d7a30 | |||
| 7c2d46f1e2 |
+3
-8
@@ -10,12 +10,7 @@ rustflags = ["-C", "target-feature=-crt-static"]
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=-crt-static"]
|
||||
|
||||
# Android/Termux: no hardcoded linker so native Termux builds use the system cc.
|
||||
# For CI cross-compilation the linker is set via CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER env var.
|
||||
[target.aarch64-linux-android]
|
||||
rustflags = [
|
||||
"-C",
|
||||
"linker=aarch64-linux-android-clang",
|
||||
"-C",
|
||||
"link-args=-rdynamic",
|
||||
"-C",
|
||||
"default-linker-libraries",
|
||||
]
|
||||
rustflags = ["-C", "link-args=-rdynamic"]
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Build Rust binary (Windows)
|
||||
if: matrix.target
|
||||
run: cargo build --release --target ${{ matrix.target }} -p fff-nvim
|
||||
run: cargo build --release --target ${{ matrix.target }} -p fff-nvim --features zlob
|
||||
|
||||
- name: Copy binary to target/release (Windows)
|
||||
if: matrix.target
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
- name: Build Rust binary
|
||||
if: ${{ !matrix.target }}
|
||||
run: cargo build --release -p fff-nvim
|
||||
run: cargo build --release -p fff-nvim --features zlob
|
||||
|
||||
- name: Install Neovim
|
||||
uses: rhysd/action-setup-vim@v1
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Prebuild
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, feat/interchangable-ffi]
|
||||
branches: [main, feat/interchangable-ffi, feat/termux]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -36,6 +36,12 @@ jobs:
|
||||
artifact_name: target/aarch64-unknown-linux-musl/release/libfff_nvim.so
|
||||
ext: so
|
||||
|
||||
## Android (Termux)
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
artifact_name: target/aarch64-linux-android/release/libfff_nvim.so
|
||||
ext: so
|
||||
|
||||
## macOS builds
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
@@ -73,15 +79,29 @@ jobs:
|
||||
run: cargo install cargo-zigbuild
|
||||
|
||||
- name: Build for Linux
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
if: contains(matrix.os, 'ubuntu') && !contains(matrix.target, 'android')
|
||||
run: |
|
||||
cargo zigbuild --release --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-nvim
|
||||
cargo zigbuild --release --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-nvim --features zlob
|
||||
mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}"
|
||||
|
||||
- name: Build for Android (Termux)
|
||||
if: contains(matrix.target, 'android')
|
||||
run: |
|
||||
NDK_BIN="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
|
||||
# NDK clang for C deps (libgit2, lmdb, blake3) that need Bionic sysroot headers
|
||||
export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang"
|
||||
export CXX_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang++"
|
||||
export AR_aarch64_linux_android="$NDK_BIN/llvm-ar"
|
||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android24-clang"
|
||||
|
||||
cargo build --release --target ${{ matrix.target }} -p fff-nvim --features zlob
|
||||
mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}"
|
||||
|
||||
- name: Build for macOS
|
||||
if: contains(matrix.os, 'macos')
|
||||
run: |
|
||||
MACOSX_DEPLOYMENT_TARGET="13" cargo build --release --target ${{ matrix.target }} -p fff-nvim
|
||||
MACOSX_DEPLOYMENT_TARGET="13" cargo build --release --target ${{ matrix.target }} -p fff-nvim --features zlob
|
||||
mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}"
|
||||
|
||||
- name: Ad-hoc sign macOS binary
|
||||
@@ -92,7 +112,7 @@ jobs:
|
||||
if: contains(matrix.os, 'windows')
|
||||
shell: bash
|
||||
run: |
|
||||
cargo build --release --target ${{ matrix.target }} -p fff-nvim
|
||||
cargo build --release --target ${{ matrix.target }} -p fff-nvim --features zlob
|
||||
mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}"
|
||||
|
||||
- name: Upload artifacts
|
||||
@@ -137,6 +157,13 @@ jobs:
|
||||
lib_filename: libfff_c.so
|
||||
ext: so
|
||||
|
||||
## Android (Termux)
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
artifact_name: target/aarch64-linux-android/release/libfff_c.so
|
||||
lib_filename: libfff_c.so
|
||||
ext: so
|
||||
|
||||
## macOS builds
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
@@ -183,15 +210,28 @@ jobs:
|
||||
run: cargo install cargo-zigbuild
|
||||
|
||||
- name: Build for Linux
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
if: contains(matrix.os, 'ubuntu') && !contains(matrix.target, 'android')
|
||||
run: |
|
||||
cargo zigbuild --release --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-c
|
||||
cargo zigbuild --release --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-c --features zlob
|
||||
mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}"
|
||||
|
||||
- name: Build for Android (Termux)
|
||||
if: contains(matrix.target, 'android')
|
||||
run: |
|
||||
NDK_BIN="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
|
||||
export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang"
|
||||
export CXX_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang++"
|
||||
export AR_aarch64_linux_android="$NDK_BIN/llvm-ar"
|
||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android24-clang"
|
||||
|
||||
cargo build --release --target ${{ matrix.target }} -p fff-c --features zlob
|
||||
mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}"
|
||||
|
||||
- name: Build for macOS
|
||||
if: contains(matrix.os, 'macos')
|
||||
run: |
|
||||
MACOSX_DEPLOYMENT_TARGET="13" cargo build --release --target ${{ matrix.target }} -p fff-c
|
||||
MACOSX_DEPLOYMENT_TARGET="13" cargo build --release --target ${{ matrix.target }} -p fff-c --features zlob
|
||||
mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}"
|
||||
|
||||
- name: Ad-hoc sign macOS binary
|
||||
@@ -202,10 +242,11 @@ jobs:
|
||||
if: contains(matrix.os, 'windows')
|
||||
shell: bash
|
||||
run: |
|
||||
cargo build --release --target ${{ matrix.target }} -p fff-c
|
||||
cargo build --release --target ${{ matrix.target }} -p fff-c --features zlob
|
||||
mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}"
|
||||
|
||||
- name: Prepare npm package
|
||||
if: "!contains(matrix.target, 'android')"
|
||||
shell: bash
|
||||
run: |
|
||||
# Copy the built binary into the platform npm package directory
|
||||
@@ -218,6 +259,7 @@ jobs:
|
||||
path: c-lib-${{ matrix.target }}.*
|
||||
|
||||
- name: Upload npm package artifact
|
||||
if: "!contains(matrix.target, 'android')"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-${{ matrix.npm_package }}
|
||||
@@ -227,6 +269,8 @@ jobs:
|
||||
name: Release
|
||||
needs: [build-nvim, build-c]
|
||||
runs-on: ubuntu-latest
|
||||
# do not create releases on the forks (no permissions)
|
||||
if: github.repository == 'dmtrKovalenko/fff.nvim'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -403,7 +447,8 @@ jobs:
|
||||
name: Comment on PR
|
||||
needs: [build-nvim, build-c]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
# comments doesn't work on forks
|
||||
if: github.event_name == 'pull_request' && github.repository == 'dmtrKovalenko/fff.nvim'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
|
||||
@@ -37,7 +37,9 @@ jobs:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose --workspace --exclude fff-nvim
|
||||
run: |
|
||||
cargo test --verbose -p fff-core -p fff-query-parser -p fff-c --features zlob
|
||||
cargo test --verbose -p grep-searcher
|
||||
|
||||
fmt:
|
||||
name: cargo fmt
|
||||
@@ -71,4 +73,6 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
run: |
|
||||
cargo clippy -p fff-core -p fff-query-parser -p fff-nvim -p fff-c --features zlob -- -D warnings
|
||||
cargo clippy -p grep-searcher -- -D warnings
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@
|
||||
"param-type-mismatch": "Warning",
|
||||
"assign-type-mismatch": "Warning",
|
||||
"cast-type-mismatch": "Warning",
|
||||
"deprecated": "Hint",
|
||||
"undefined-doc-param": "Hint"
|
||||
"deprecated": "Warning",
|
||||
"undefined-doc-param": "Warning"
|
||||
},
|
||||
"neededFileStatus": {
|
||||
"undefined-global": "Any",
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@
|
||||
"param-type-mismatch": "Warning",
|
||||
"assign-type-mismatch": "Warning",
|
||||
"cast-type-mismatch": "Warning",
|
||||
"deprecated": "Hint",
|
||||
"undefined-doc-param": "Hint"
|
||||
"deprecated": "Warning",
|
||||
"undefined-doc-param": "Warning"
|
||||
},
|
||||
"neededFileStatus": {
|
||||
"undefined-global": "Any",
|
||||
|
||||
Generated
+3
-70
@@ -482,6 +482,7 @@ dependencies = [
|
||||
"fff-query-parser",
|
||||
"git2",
|
||||
"glidesort",
|
||||
"globset",
|
||||
"grep-matcher",
|
||||
"grep-searcher",
|
||||
"heed",
|
||||
@@ -492,7 +493,6 @@ dependencies = [
|
||||
"notify",
|
||||
"notify-debouncer-full 0.7.0",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"parking_lot",
|
||||
"pathdiff",
|
||||
"rand",
|
||||
@@ -531,7 +531,6 @@ dependencies = [
|
||||
"notify",
|
||||
"notify-debouncer-full 0.6.0",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"pathdiff",
|
||||
"rand",
|
||||
"rayon",
|
||||
@@ -543,7 +542,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"zlob",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -564,21 +562,6 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@@ -630,7 +613,6 @@ dependencies = [
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"openssl-sys",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -986,7 +968,6 @@ dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@@ -1301,54 +1282,6 @@ version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.5.1+3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -2576,9 +2509,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zlob"
|
||||
version = "1.2.9"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "674f4e74544c0a00887c0dff7862fe13742de8c5dbcf6db9eac0643362554d44"
|
||||
checksum = "07934fb13f0f4e14281bab6b0e984cfc03891d2cf5b0a8bd0ab14fd240e21106"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"bitflags 2.9.1",
|
||||
|
||||
+2
-2
@@ -20,16 +20,16 @@ dunce = "1.0"
|
||||
# git2 - base config without TLS (each crate adds platform-specific TLS)
|
||||
git2 = { version = "0.20.2", default-features = false, features = [
|
||||
"vendored-libgit2",
|
||||
"vendored-openssl",
|
||||
] }
|
||||
glidesort = "0.1"
|
||||
globset = "0.4"
|
||||
grep-matcher = "0.1.8"
|
||||
grep-searcher = { path = "crates/fff-searcher" }
|
||||
heed = "0.22.0"
|
||||
ignore = "0.4.22"
|
||||
memmap2 = "0.9"
|
||||
mimalloc = "0.1.47"
|
||||
zlob = "1.2.9"
|
||||
zlob = "1.3.0"
|
||||
|
||||
mlua = { version = "0.11.1", features = ["module", "luajit"] }
|
||||
neo_frizbee = "0.8.1"
|
||||
|
||||
@@ -3,7 +3,7 @@ PLENARY_DIR ?= ../plenary.nvim
|
||||
.PHONY: build test test-rust test-lua test-bun test-setup prepare-bun
|
||||
|
||||
build:
|
||||
cargo build --release
|
||||
cargo build --release --features zlob
|
||||
|
||||
test-setup:
|
||||
@if [ ! -d "$(PLENARY_DIR)" ]; then \
|
||||
@@ -12,7 +12,7 @@ test-setup:
|
||||
fi
|
||||
|
||||
test-rust:
|
||||
cargo test --workspace
|
||||
cargo test --workspace --features zlob
|
||||
|
||||
test-lua: test-setup build
|
||||
nvim --headless -u tests/minimal_init.lua \
|
||||
|
||||
@@ -141,6 +141,10 @@ require('fff').setup({
|
||||
prompt_position = 'bottom', -- or 'top'
|
||||
preview_position = 'right', -- or 'left', 'right', 'top', 'bottom'
|
||||
preview_size = 0.5,
|
||||
flex = { -- set to false to disable flex layout
|
||||
size = 130, -- column threshold: if screen width >= size, use preview_position; otherwise use wrap
|
||||
wrap = 'top', -- position to use when screen is narrower than size
|
||||
},
|
||||
show_scrollbar = true, -- Show scrollbar for pagination
|
||||
-- How to shorten long directory paths in the file list:
|
||||
-- 'middle_number' (default): uses dots for 1-3 hidden (a/./b, a/../b, a/.../b)
|
||||
@@ -156,6 +160,7 @@ require('fff').setup({
|
||||
binary_file_threshold = 1024, -- amount of bytes to scan for binary content (set 0 to disable)
|
||||
imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit',
|
||||
line_numbers = false,
|
||||
cursorlineopt = 'both', -- the cursorlineopt used for lines in grep file previews, see :h cursorlineopt
|
||||
wrap_lines = false,
|
||||
filetypes = {
|
||||
svg = { wrap_lines = true },
|
||||
@@ -176,7 +181,7 @@ require('fff').setup({
|
||||
preview_scroll_down = '<C-d>',
|
||||
toggle_debug = '<F2>',
|
||||
-- grep mode: cycle between plain text, regex, and fuzzy search
|
||||
toggle_grep_regex = '<S-Tab>',
|
||||
cycle_grep_modes = '<S-Tab>',
|
||||
-- goes to the previous query in history
|
||||
cycle_previous_query = '<C-Up>',
|
||||
-- multi-select keymaps for quickfix
|
||||
@@ -185,16 +190,14 @@ require('fff').setup({
|
||||
-- this are specific for the normal mode (you can exit it using any other keybind like jj)
|
||||
focus_list = '<leader>l',
|
||||
focus_preview = '<leader>p',
|
||||
toggle_grep_regex = '<S-Tab>',
|
||||
},
|
||||
hl = {
|
||||
border = 'FloatBorder',
|
||||
normal = 'Normal',
|
||||
cursor = 'CursorLine',
|
||||
cursor = 'CursorLine', -- Falls back to 'Visual' if CursorLine is not defined
|
||||
matched = 'IncSearch',
|
||||
title = 'Title',
|
||||
prompt = 'Question',
|
||||
cursor = fallback_hl({ 'CursorLine', 'Visual' }),
|
||||
frecency = 'Number',
|
||||
debug = 'Comment',
|
||||
combo_header = 'Number',
|
||||
@@ -271,7 +274,7 @@ require('fff').setup({
|
||||
time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit)
|
||||
modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order
|
||||
},
|
||||
}})
|
||||
})
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
@@ -8,6 +8,10 @@ license = "MIT"
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
zlob = ["fff-core/zlob"]
|
||||
|
||||
[dependencies]
|
||||
mimalloc.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -13,6 +13,9 @@ crate-type = ["rlib", "staticlib", "cdylib"]
|
||||
default = []
|
||||
# Enable C FFI exports
|
||||
ffi = []
|
||||
# Use zlob (Zig-compiled C globbing library) for glob matching.
|
||||
# Requires Zig to be installed. When disabled, falls back to globset (pure Rust).
|
||||
zlob = ["dep:zlob", "fff-query-parser/zlob"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace dependencies
|
||||
@@ -23,7 +26,7 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Local crates
|
||||
fff-query-parser = { path = "../fff-query-parser" }
|
||||
fff-query-parser = { path = "../fff-query-parser", default-features = false }
|
||||
|
||||
# External dependencies
|
||||
bindet = { workspace = true }
|
||||
@@ -32,6 +35,7 @@ chrono = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
git2 = { workspace = true }
|
||||
glidesort = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
grep-matcher = { workspace = true }
|
||||
grep-searcher = { workspace = true }
|
||||
memchr = "2"
|
||||
@@ -49,12 +53,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
smartstring = { version = "1.0.1", features = ["serde"] }
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
zlob = { workspace = true }
|
||||
|
||||
# Platform-specific: Use vendored OpenSSL on non-Windows (Linux, macOS)
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
||||
zlob = { workspace = true, optional = true }
|
||||
# Platform-specific: dunce for Windows to avoid \\?\ extended path prefix
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
dunce = { workspace = true }
|
||||
|
||||
+42
-12
@@ -1,16 +1,46 @@
|
||||
fn main() {
|
||||
// When the `zlob` feature is enabled (Zig-compiled C library):
|
||||
// On Windows MSVC, explicitly link the C runtime libraries.
|
||||
// This is needed because Zig-compiled static libraries (zlob) don't emit
|
||||
// /DEFAULTLIB directives for the MSVC CRT. Without this, symbols like
|
||||
// strcmp, memcpy, memchr etc. from vendored C libraries (libgit2, lmdb)
|
||||
// are unresolved when linking the cdylib.
|
||||
//
|
||||
// We link both msvcrt (classic CRT) and ucrt (Universal CRT where memchr,
|
||||
// strcmp etc. live on newer MSVC/ARM64 targets).
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
if target.contains("windows") && target.contains("msvc") {
|
||||
println!("cargo:rustc-link-lib=msvcrt");
|
||||
println!("cargo:rustc-link-lib=ucrt");
|
||||
println!("cargo:rustc-link-lib=vcruntime");
|
||||
// Zig-compiled static libraries don't emit /DEFAULTLIB directives for the
|
||||
// MSVC CRT, so symbols like strcmp, memcpy etc. would be unresolved.
|
||||
if std::env::var("CARGO_FEATURE_ZLOB").is_ok() {
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
if target.contains("windows") && target.contains("msvc") {
|
||||
println!("cargo:rustc-link-lib=msvcrt");
|
||||
println!("cargo:rustc-link-lib=ucrt");
|
||||
println!("cargo:rustc-link-lib=vcruntime");
|
||||
}
|
||||
} else if std::env::var("CI").is_ok() {
|
||||
// CI must always build with zlob for production-quality binaries.
|
||||
if !zig_available() {
|
||||
panic!(
|
||||
"CI detected but Zig is not installed. \
|
||||
Please install Zig and build with `--features zlob`."
|
||||
);
|
||||
}
|
||||
panic!(
|
||||
"CI detected but `zlob` feature is not enabled. \
|
||||
Build with `--features zlob`."
|
||||
);
|
||||
} else {
|
||||
// Hint: if Zig is available but the zlob feature wasn't enabled,
|
||||
// let the developer know they can get faster glob matching.
|
||||
if zig_available() {
|
||||
println!(
|
||||
"cargo:warning=Zig detected but `zlob` feature is not enabled. \
|
||||
Build with `--features zlob` for faster glob matching."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe the system for a working Zig installation.
|
||||
fn zig_available() -> bool {
|
||||
std::process::Command::new("zig")
|
||||
.arg("version")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
use ahash::AHashSet;
|
||||
use fff_query_parser::{Constraint, GitStatusFilter};
|
||||
use smallvec::SmallVec;
|
||||
use zlob::{ZlobFlags, zlob_match_paths};
|
||||
|
||||
use crate::git::is_modified_status;
|
||||
|
||||
@@ -235,32 +234,8 @@ fn collect_glob_indices<'a>(
|
||||
) {
|
||||
match constraint {
|
||||
Constraint::Glob(pattern) => {
|
||||
if let Ok(Some(matches)) = zlob_match_paths(pattern, paths, ZlobFlags::RECOMMENDED) {
|
||||
let matched_set: AHashSet<usize> =
|
||||
matches.iter().map(|s| s.as_ptr() as usize).collect();
|
||||
|
||||
let indices: AHashSet<usize> = if paths.len() >= PAR_THRESHOLD {
|
||||
use rayon::prelude::*;
|
||||
paths
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| matched_set.contains(&(p.as_ptr() as usize)))
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
} else {
|
||||
paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| matched_set.contains(&(p.as_ptr() as usize)))
|
||||
.map(|(i, _)| i)
|
||||
.collect()
|
||||
};
|
||||
results.push((is_negated, indices));
|
||||
} else {
|
||||
results.push((is_negated, AHashSet::new()));
|
||||
}
|
||||
let indices = match_glob_pattern(pattern, paths);
|
||||
results.push((is_negated, indices));
|
||||
}
|
||||
Constraint::Not(inner) => {
|
||||
collect_glob_indices(inner, paths, results, !is_negated);
|
||||
@@ -269,6 +244,66 @@ fn collect_glob_indices<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Match a glob pattern against a list of paths, returning the set of matching indices.
|
||||
///
|
||||
/// When the `zlob` feature is enabled, delegates to `zlob::zlob_match_paths` (Zig-compiled
|
||||
/// C library, fastest). Otherwise falls back to `globset::Glob` (pure Rust).
|
||||
#[cfg(feature = "zlob")]
|
||||
fn match_glob_pattern(pattern: &str, paths: &[&str]) -> AHashSet<usize> {
|
||||
let Ok(Some(matches)) = zlob::zlob_match_paths(pattern, paths, zlob::ZlobFlags::RECOMMENDED)
|
||||
else {
|
||||
return AHashSet::new();
|
||||
};
|
||||
|
||||
let matched_set: AHashSet<usize> = matches.iter().map(|s| s.as_ptr() as usize).collect();
|
||||
|
||||
if paths.len() >= PAR_THRESHOLD {
|
||||
use rayon::prelude::*;
|
||||
paths
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| matched_set.contains(&(p.as_ptr() as usize)))
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
} else {
|
||||
paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| matched_set.contains(&(p.as_ptr() as usize)))
|
||||
.map(|(i, _)| i)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "zlob"))]
|
||||
fn match_glob_pattern(pattern: &str, paths: &[&str]) -> AHashSet<usize> {
|
||||
let Ok(glob) = globset::Glob::new(pattern) else {
|
||||
return AHashSet::new();
|
||||
};
|
||||
let matcher = glob.compile_matcher();
|
||||
|
||||
if paths.len() >= PAR_THRESHOLD {
|
||||
use rayon::prelude::*;
|
||||
paths
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| matcher.is_match(p))
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
} else {
|
||||
paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| matcher.is_match(p))
|
||||
.map(|(i, _)| i)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -7,6 +7,10 @@ edition = "2024"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
zlob = ["fff-core/zlob"]
|
||||
|
||||
[[bin]]
|
||||
name = "test_watcher"
|
||||
path = "src/bin/test_watcher.rs"
|
||||
@@ -63,7 +67,6 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
smartstring = { version = "1.0.1", features = ["serde"] }
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
zlob = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
@@ -78,7 +81,3 @@ harness = false
|
||||
name = "query_tracker_bench"
|
||||
harness = false
|
||||
|
||||
# Platform-specific: Use vendored OpenSSL on non-Windows (Linux, macOS)
|
||||
# On Windows, git2 uses the native SChannel TLS backend
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
||||
@@ -6,9 +6,13 @@ edition = "2024"
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
zlob = ["dep:zlob"]
|
||||
|
||||
[dependencies]
|
||||
smallvec = { workspace = true }
|
||||
zlob = { version = "1.2.8" }
|
||||
zlob = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::constraints::Constraint;
|
||||
use zlob::{ZlobFlags, has_wildcards};
|
||||
use crate::glob_detect::has_wildcards;
|
||||
|
||||
/// Parser configuration trait - allows different picker types to customize parsing
|
||||
pub trait ParserConfig {
|
||||
@@ -40,7 +40,7 @@ pub trait ParserConfig {
|
||||
/// Override this in configs where some wildcard characters are common
|
||||
/// in search text (e.g. grep mode where `?` and `[` appear in code).
|
||||
fn is_glob_pattern(&self, token: &str) -> bool {
|
||||
has_wildcards(token, ZlobFlags::RECOMMENDED)
|
||||
has_wildcards(token)
|
||||
}
|
||||
|
||||
/// Custom constraint parsers for picker-specific needs
|
||||
@@ -87,7 +87,7 @@ impl ParserConfig for GrepConfig {
|
||||
/// - Contains `{…}` → brace expansion (e.g. `{src,lib}`)
|
||||
fn is_glob_pattern(&self, token: &str) -> bool {
|
||||
// Must contain at least one glob wildcard character
|
||||
if !has_wildcards(token, ZlobFlags::RECOMMENDED) {
|
||||
if !has_wildcards(token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//! Glob wildcard detection — delegates to zlob when available, pure-Rust fallback otherwise.
|
||||
//!
|
||||
//! All call sites use a single function: `has_wildcards(text) -> bool`.
|
||||
//! When the `zlob` feature is enabled this calls `zlob::has_wildcards` with
|
||||
//! `ZlobFlags::RECOMMENDED`; without it we check for the same set of wildcard
|
||||
//! characters (`*`, `?`, `[`, `{`) in pure Rust.
|
||||
|
||||
#[cfg(feature = "zlob")]
|
||||
#[inline]
|
||||
pub fn has_wildcards(s: &str) -> bool {
|
||||
zlob::has_wildcards(s, zlob::ZlobFlags::RECOMMENDED)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "zlob"))]
|
||||
#[inline]
|
||||
pub fn has_wildcards(s: &str) -> bool {
|
||||
s.bytes().any(|b| matches!(b, b'*' | b'?' | b'[' | b'{'))
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
mod config;
|
||||
mod constraints;
|
||||
pub mod glob_detect;
|
||||
pub mod location;
|
||||
mod parser;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::ConstraintVec;
|
||||
use crate::config::ParserConfig;
|
||||
use crate::constraints::{Constraint, GitStatusFilter, TextPartsBuffer};
|
||||
use crate::glob_detect::has_wildcards;
|
||||
use crate::location::{Location, parse_location};
|
||||
use zlob::{ZlobFlags, has_wildcards};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -184,7 +184,7 @@ fn parse_token<'a, C: ParserConfig>(token: &'a str, config: &C) -> Option<Constr
|
||||
// Only return Extension if the rest doesn't have wildcards
|
||||
// e.g., *.rs is Extension, but *.test.* should be Glob
|
||||
let ext_part = &token[2..];
|
||||
if !has_wildcards(ext_part, ZlobFlags::RECOMMENDED) {
|
||||
if !has_wildcards(ext_part) {
|
||||
return Some(constraint);
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,7 @@ fn parse_token_without_negation<'a, C: ParserConfig>(
|
||||
// Try extension first (*.rs) - simple patterns without additional wildcards
|
||||
if let Some(constraint) = parse_extension(token) {
|
||||
let ext_part = &token[2..];
|
||||
if !has_wildcards(ext_part, ZlobFlags::RECOMMENDED) {
|
||||
if !has_wildcards(ext_part) {
|
||||
return Some(constraint);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -1,4 +1,4 @@
|
||||
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 February 25
|
||||
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 February 28
|
||||
|
||||
==============================================================================
|
||||
Table of Contents *fff.nvim-table-of-contents*
|
||||
@@ -149,6 +149,10 @@ all available options:
|
||||
prompt_position = 'bottom', -- or 'top'
|
||||
preview_position = 'right', -- or 'left', 'right', 'top', 'bottom'
|
||||
preview_size = 0.5,
|
||||
flex = { -- set to false to disable flex layout
|
||||
size = 130, -- column threshold: if screen width >= size, use preview_position; otherwise use wrap
|
||||
wrap = 'top', -- position to use when screen is narrower than size
|
||||
},
|
||||
show_scrollbar = true, -- Show scrollbar for pagination
|
||||
-- How to shorten long directory paths in the file list:
|
||||
-- 'middle_number' (default): uses dots for 1-3 hidden (a/./b, a/../b, a/.../b)
|
||||
@@ -164,6 +168,7 @@ all available options:
|
||||
binary_file_threshold = 1024, -- amount of bytes to scan for binary content (set 0 to disable)
|
||||
imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit',
|
||||
line_numbers = false,
|
||||
cursorlineopt = 'both', -- the cursorlineopt used for lines in grep file previews, see :h cursorlineopt
|
||||
wrap_lines = false,
|
||||
filetypes = {
|
||||
svg = { wrap_lines = true },
|
||||
@@ -184,7 +189,7 @@ all available options:
|
||||
preview_scroll_down = '<C-d>',
|
||||
toggle_debug = '<F2>',
|
||||
-- grep mode: cycle between plain text, regex, and fuzzy search
|
||||
toggle_grep_regex = '<S-Tab>',
|
||||
cycle_grep_modes = '<S-Tab>',
|
||||
-- goes to the previous query in history
|
||||
cycle_previous_query = '<C-Up>',
|
||||
-- multi-select keymaps for quickfix
|
||||
@@ -193,16 +198,14 @@ all available options:
|
||||
-- this are specific for the normal mode (you can exit it using any other keybind like jj)
|
||||
focus_list = '<leader>l',
|
||||
focus_preview = '<leader>p',
|
||||
toggle_grep_regex = '<S-Tab>',
|
||||
},
|
||||
hl = {
|
||||
border = 'FloatBorder',
|
||||
normal = 'Normal',
|
||||
cursor = 'CursorLine',
|
||||
cursor = 'CursorLine', -- Falls back to 'Visual' if CursorLine is not defined
|
||||
matched = 'IncSearch',
|
||||
title = 'Title',
|
||||
prompt = 'Question',
|
||||
cursor = fallback_hl({ 'CursorLine', 'Visual' }),
|
||||
frecency = 'Number',
|
||||
debug = 'Comment',
|
||||
combo_header = 'Number',
|
||||
@@ -279,7 +282,7 @@ all available options:
|
||||
time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit)
|
||||
modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order
|
||||
},
|
||||
}})
|
||||
})
|
||||
<
|
||||
|
||||
|
||||
|
||||
@@ -57,14 +57,13 @@ end
|
||||
|
||||
local function apply_header_highlights(buf, ns_id, line_idx, text_len, border_hl)
|
||||
local config = require('fff.conf').get()
|
||||
vim.api.nvim_buf_add_highlight(buf, ns_id, border_hl, line_idx - 1, 0, -1)
|
||||
vim.api.nvim_buf_add_highlight(
|
||||
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx - 1, 0, { end_row = line_idx, end_col = 0, hl_group = border_hl })
|
||||
vim.api.nvim_buf_set_extmark(
|
||||
buf,
|
||||
ns_id,
|
||||
config.hl.combo_header,
|
||||
line_idx - 1,
|
||||
LEFT_HEADER_PADDING,
|
||||
LEFT_HEADER_PADDING + text_len
|
||||
{ end_col = LEFT_HEADER_PADDING + text_len, hl_group = config.hl.combo_header }
|
||||
)
|
||||
end
|
||||
|
||||
@@ -72,18 +71,18 @@ local function get_or_create_overlay_buf(state_key)
|
||||
if not overlay_state[state_key] or not vim.api.nvim_buf_is_valid(overlay_state[state_key]) then
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
overlay_state[state_key] = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_option(overlay_state[state_key], 'bufhidden', 'wipe')
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = overlay_state[state_key] })
|
||||
end
|
||||
return overlay_state[state_key]
|
||||
end
|
||||
|
||||
local function update_overlay_content(buf, content, border_hl)
|
||||
-- Batch all buffer operations together for performance
|
||||
vim.api.nvim_buf_set_option(buf, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = buf })
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { content })
|
||||
vim.api.nvim_buf_clear_namespace(buf, overlay_state.ns_id, 0, -1)
|
||||
vim.api.nvim_buf_add_highlight(buf, overlay_state.ns_id, border_hl, 0, 0, -1)
|
||||
vim.api.nvim_buf_set_option(buf, 'modifiable', false)
|
||||
vim.api.nvim_buf_set_extmark(buf, overlay_state.ns_id, 0, 0, { end_row = 1, end_col = 0, hl_group = border_hl })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
|
||||
end
|
||||
|
||||
local function position_overlay_window(state_key, buf, width, row, col)
|
||||
@@ -106,7 +105,7 @@ local function position_overlay_window(state_key, buf, width, row, col)
|
||||
overlay_state[state_key] = vim.api.nvim_open_win(buf, false, win_config)
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_option(overlay_state[state_key], 'winhl', 'Normal:Normal')
|
||||
vim.api.nvim_set_option_value('winhighlight', 'Normal:Normal', { win = overlay_state[state_key] })
|
||||
end
|
||||
|
||||
local function update_overlays(list_win, combo_header_line, border_hl)
|
||||
|
||||
+84
-5
@@ -1,8 +1,82 @@
|
||||
local M = {}
|
||||
|
||||
--@class fff.conf.State
|
||||
--- @class FffLayoutConfig
|
||||
--- @field height number
|
||||
--- @field width number
|
||||
--- @field prompt_position string
|
||||
--- @field preview_position string
|
||||
--- @field preview_size number
|
||||
--- @field show_scrollbar boolean
|
||||
--- @field path_shorten_strategy string
|
||||
|
||||
--- @class FffPreviewConfig
|
||||
--- @field enabled boolean
|
||||
--- @field max_size number
|
||||
--- @field chunk_size number
|
||||
--- @field binary_file_threshold number
|
||||
--- @field imagemagick_info_format_str string
|
||||
--- @field line_numbers boolean
|
||||
--- @field cursorlineopt string
|
||||
--- @field wrap_lines boolean
|
||||
--- @field filetypes table<string, table>
|
||||
|
||||
--- @class FffKeymapsConfig
|
||||
--- @field close string
|
||||
--- @field select string
|
||||
--- @field select_split string
|
||||
--- @field select_vsplit string
|
||||
--- @field select_tab string
|
||||
--- @field move_up string|string[]
|
||||
--- @field move_down string|string[]
|
||||
--- @field preview_scroll_up string
|
||||
--- @field preview_scroll_down string
|
||||
--- @field toggle_debug string
|
||||
--- @field cycle_grep_modes string
|
||||
--- @field cycle_previous_query string
|
||||
--- @field toggle_select string
|
||||
--- @field send_to_quickfix string
|
||||
--- @field focus_list string
|
||||
--- @field focus_preview string
|
||||
|
||||
--- @class FffFrecencyConfig
|
||||
--- @field enabled boolean
|
||||
--- @field db_path string
|
||||
|
||||
--- @class FffHistoryConfig
|
||||
--- @field enabled boolean
|
||||
--- @field db_path string
|
||||
--- @field min_combo_count number
|
||||
--- @field combo_boost_score_multiplier number
|
||||
|
||||
--- @class FffGrepConfig
|
||||
--- @field max_file_size number
|
||||
--- @field max_matches_per_file number
|
||||
--- @field smart_case boolean
|
||||
--- @field time_budget_ms number
|
||||
--- @field modes string[]
|
||||
|
||||
--- @class FffConfig
|
||||
--- @field base_path string
|
||||
--- @field prompt string
|
||||
--- @field title string
|
||||
--- @field max_results number
|
||||
--- @field max_threads number
|
||||
--- @field lazy_sync boolean
|
||||
--- @field layout FffLayoutConfig
|
||||
--- @field preview FffPreviewConfig
|
||||
--- @field keymaps FffKeymapsConfig
|
||||
--- @field hl table<string, string>
|
||||
--- @field frecency FffFrecencyConfig
|
||||
--- @field history FffHistoryConfig
|
||||
--- @field git table
|
||||
--- @field debug table
|
||||
--- @field logging table
|
||||
--- @field file_picker table
|
||||
--- @field grep FffGrepConfig
|
||||
|
||||
---@class fff.conf.State
|
||||
local state = {
|
||||
---@type table | nil
|
||||
---@type FffConfig|nil
|
||||
config = nil,
|
||||
}
|
||||
|
||||
@@ -127,6 +201,10 @@ local function init()
|
||||
prompt_position = 'bottom', -- or 'top'
|
||||
preview_position = 'right', -- or 'left', 'right', 'top', 'bottom'
|
||||
preview_size = 0.5,
|
||||
flex = { -- set to nil to disable flex layout
|
||||
size = 130, -- column threshold: if screen width >= size, use preview_position; otherwise use wrap
|
||||
wrap = 'top', -- position to use when screen is narrower than size
|
||||
},
|
||||
show_scrollbar = true, -- Show scrollbar for pagination
|
||||
-- How to shorten long directory paths in the file list:
|
||||
-- 'middle_number' (default): uses dots for 1-3 hidden (a/./b, a/../b, a/.../b)
|
||||
@@ -142,6 +220,7 @@ local function init()
|
||||
binary_file_threshold = 1024, -- amount of bytes to scan for binary content (set 0 to disable)
|
||||
imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit',
|
||||
line_numbers = false,
|
||||
cursorlineopt = 'both',
|
||||
wrap_lines = false,
|
||||
filetypes = {
|
||||
svg = { wrap_lines = true },
|
||||
@@ -162,7 +241,7 @@ local function init()
|
||||
preview_scroll_down = '<C-d>',
|
||||
toggle_debug = '<F2>',
|
||||
-- grep mode: cycle between plain text, regex, and fuzzy search
|
||||
toggle_grep_regex = '<S-Tab>',
|
||||
cycle_grep_modes = '<S-Tab>',
|
||||
-- goes to the previous query in history
|
||||
cycle_previous_query = '<C-Up>',
|
||||
-- multi-select keymaps for quickfix
|
||||
@@ -264,10 +343,10 @@ local function init()
|
||||
end
|
||||
|
||||
--- Setup the file picker with the given configuration
|
||||
--- @param config table Configuration options
|
||||
--- @param config FffConfig Configuration options
|
||||
function M.setup(config) vim.g.fff = config end
|
||||
|
||||
--- @return table the fff configuration
|
||||
--- @return FffConfig the fff configuration
|
||||
function M.get()
|
||||
if not state.config then init() end
|
||||
return state.config
|
||||
|
||||
@@ -25,11 +25,11 @@ local function reserve_image_buffer_space(bufnr, metadata_lines_count)
|
||||
table.insert(buffer_lines, '')
|
||||
end
|
||||
|
||||
local was_modifiable = vim.api.nvim_buf_get_option(bufnr, 'modifiable')
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
local was_modifiable = vim.api.nvim_get_option_value('modifiable', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, buffer_lines)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', was_modifiable)
|
||||
vim.api.nvim_set_option_value('modifiable', was_modifiable, { buf = bufnr })
|
||||
|
||||
return metadata_lines_count or 2
|
||||
end
|
||||
@@ -44,9 +44,9 @@ local function update_metadata_lines(bufnr, info_lines, reserved_lines_count)
|
||||
metadata_lines[i] = info_lines[i] or ''
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, reserved_lines_count, false, metadata_lines)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
end
|
||||
|
||||
local function identify_image_lines_async(file_path, bufnr, callback)
|
||||
@@ -146,7 +146,10 @@ end
|
||||
--- @param bufnr number Buffer number to display in
|
||||
--- @return boolean
|
||||
function M.display_image(file_path, bufnr)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'number', false)
|
||||
local wins = vim.fn.win_findbuf(bufnr)
|
||||
for _, win in ipairs(wins) do
|
||||
vim.api.nvim_set_option_value('number', false, { win = win })
|
||||
end
|
||||
|
||||
local reserved_metadata_lines = reserve_image_buffer_space(bufnr, 2)
|
||||
local image_content_starts_at_line = reserved_metadata_lines + 1
|
||||
@@ -163,9 +166,9 @@ function M.display_image(file_path, bufnr)
|
||||
'',
|
||||
'snacks.nvim plugin is not installed or not available.',
|
||||
}
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, image_content_starts_at_line, -1, false, error_lines)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -176,9 +179,9 @@ function M.display_image(file_path, bufnr)
|
||||
'Terminal does not support image preview.',
|
||||
'Please use a terminal that supports images, such as Kitty, Wezterm or Alacritty.',
|
||||
}
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, image_content_starts_at_line, -1, false, error_lines)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -189,9 +192,9 @@ function M.display_image(file_path, bufnr)
|
||||
'File format is not supported for image preview.',
|
||||
'File: ' .. vim.fn.fnamemodify(file_path, ':t'),
|
||||
}
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, image_content_starts_at_line, -1, false, error_lines)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
@@ -8,19 +8,19 @@ local M = {}
|
||||
local function set_buffer_lines(bufnr, lines)
|
||||
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end
|
||||
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
end
|
||||
|
||||
local function append_buffer_lines(bufnr, lines)
|
||||
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then return end
|
||||
if not lines or #lines == 0 then return end
|
||||
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
local current_lines = vim.api.nvim_buf_line_count(bufnr)
|
||||
vim.api.nvim_buf_set_lines(bufnr, current_lines, current_lines, false, lines)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
end
|
||||
|
||||
local function find_existing_buffer(file_path)
|
||||
@@ -272,8 +272,8 @@ local function link_buffer_content(source_bufnr, target_bufnr)
|
||||
local lines = vim.api.nvim_buf_get_lines(source_bufnr, 0, -1, false)
|
||||
set_buffer_lines(target_bufnr, lines)
|
||||
|
||||
local source_ft = vim.api.nvim_buf_get_option(source_bufnr, 'filetype')
|
||||
if source_ft ~= '' then vim.api.nvim_buf_set_option(target_bufnr, 'filetype', source_ft) end
|
||||
local source_ft = vim.api.nvim_get_option_value('filetype', { buf = source_bufnr })
|
||||
if source_ft ~= '' then vim.api.nvim_set_option_value('filetype', source_ft, { buf = target_bufnr }) end
|
||||
|
||||
M.state.has_more_content = false
|
||||
M.state.total_file_lines = #lines
|
||||
@@ -315,7 +315,6 @@ end
|
||||
|
||||
--- Check if file is too big for initial preview (inspired by snacks.nvim)
|
||||
--- @param file_path string Path to the file
|
||||
--- @param bufnr number|nil Buffer number to check (unused with dynamic loading)
|
||||
--- @return boolean True if file is too big for initial preview
|
||||
function M.is_big_file(file_path)
|
||||
-- Only check file size for early detection - no line limits with dynamic loading
|
||||
@@ -485,10 +484,12 @@ function M.preview_file(file_path, bufnr)
|
||||
if success then
|
||||
local file_config = M.get_file_config(file_path)
|
||||
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'buftype', 'nofile')
|
||||
vim.api.nvim_buf_set_option(bufnr, 'wrap', file_config.wrap_lines or M.config.wrap_lines)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', true, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr })
|
||||
if M.state.winid and vim.api.nvim_win_is_valid(M.state.winid) then
|
||||
vim.api.nvim_set_option_value('wrap', file_config.wrap_lines or M.config.wrap_lines, { win = M.state.winid })
|
||||
end
|
||||
|
||||
M.state.scroll_offset = 0
|
||||
|
||||
@@ -528,11 +529,13 @@ function M.preview_file(file_path, bufnr)
|
||||
set_buffer_lines(bufnr, content)
|
||||
|
||||
local file_config = M.get_file_config(file_path)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'filetype', info.filetype)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'buftype', 'nofile')
|
||||
vim.api.nvim_buf_set_option(bufnr, 'wrap', file_config.wrap_lines or M.config.wrap_lines)
|
||||
vim.api.nvim_set_option_value('filetype', info.filetype, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', true, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr })
|
||||
if M.state.winid and vim.api.nvim_win_is_valid(M.state.winid) then
|
||||
vim.api.nvim_set_option_value('wrap', file_config.wrap_lines or M.config.wrap_lines, { win = M.state.winid })
|
||||
end
|
||||
|
||||
M.state.content_height = #content
|
||||
M.state.scroll_offset = 0
|
||||
@@ -560,9 +563,9 @@ function M.preview_binary_file(file_path, bufnr)
|
||||
local lines = {}
|
||||
|
||||
set_buffer_lines(bufnr, lines)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'filetype', 'text')
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
|
||||
vim.api.nvim_set_option_value('filetype', 'text', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', true, { buf = bufnr })
|
||||
|
||||
if vim.fn.executable('file') == 1 then
|
||||
local cmd = { 'file', '-b', file_path }
|
||||
@@ -726,10 +729,10 @@ function M.update_file_info_buffer(file, bufnr, file_index)
|
||||
end
|
||||
set_buffer_lines(bufnr, file_info_lines)
|
||||
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'buftype', 'nofile')
|
||||
vim.api.nvim_buf_set_option(bufnr, 'wrap', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', true, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('wrap', false, { buf = bufnr })
|
||||
|
||||
return true
|
||||
end
|
||||
@@ -769,10 +772,10 @@ function M.clear_buffer(bufnr)
|
||||
|
||||
pcall(vim.treesitter.stop, bufnr)
|
||||
|
||||
vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
vim.api.nvim_buf_set_option(bufnr, 'filetype', '')
|
||||
vim.api.nvim_buf_set_option(bufnr, 'syntax', '')
|
||||
vim.api.nvim_buf_set_option(bufnr, 'buftype', 'nofile')
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('filetype', '', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('syntax', '', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr })
|
||||
|
||||
set_buffer_lines(bufnr, {})
|
||||
end
|
||||
|
||||
@@ -106,7 +106,13 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont
|
||||
-- 2. Icon
|
||||
if icon and icon_hl_group and vim.fn.strdisplaywidth(icon) > 0 then
|
||||
local icon_hl = is_current_file and 'Comment' or icon_hl_group
|
||||
vim.api.nvim_buf_add_highlight(buf, ns_id, icon_hl, line_idx - 1, 0, vim.fn.strdisplaywidth(icon))
|
||||
vim.api.nvim_buf_set_extmark(
|
||||
buf,
|
||||
ns_id,
|
||||
line_idx - 1,
|
||||
0,
|
||||
{ end_col = vim.fn.strdisplaywidth(icon), hl_group = icon_hl }
|
||||
)
|
||||
end
|
||||
|
||||
-- 3. Git text color (filename)
|
||||
@@ -114,7 +120,13 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont
|
||||
local git_text_hl = item.git_status and git_utils.get_text_highlight(item.git_status) or nil
|
||||
if git_text_hl and git_text_hl ~= '' and not is_current_file then
|
||||
local filename_start = #icon + 1
|
||||
vim.api.nvim_buf_add_highlight(buf, ns_id, git_text_hl, line_idx - 1, filename_start, filename_start + #filename)
|
||||
vim.api.nvim_buf_set_extmark(
|
||||
buf,
|
||||
ns_id,
|
||||
line_idx - 1,
|
||||
filename_start,
|
||||
{ end_col = filename_start + #filename, hl_group = git_text_hl }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -122,7 +134,13 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont
|
||||
if ctx.debug_enabled then
|
||||
local start_pos, end_pos = line_content:find('[⭐️🔥✨•]%d+')
|
||||
if start_pos and end_pos then
|
||||
vim.api.nvim_buf_add_highlight(buf, ns_id, ctx.config.hl.frecency, line_idx - 1, start_pos - 1, end_pos)
|
||||
vim.api.nvim_buf_set_extmark(
|
||||
buf,
|
||||
ns_id,
|
||||
line_idx - 1,
|
||||
start_pos - 1,
|
||||
{ end_col = end_pos, hl_group = ctx.config.hl.frecency }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -132,13 +150,12 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont
|
||||
if icon then
|
||||
prefix_len = prefix_len + #icon + 1 -- if icon add icon bytes + space
|
||||
end
|
||||
vim.api.nvim_buf_add_highlight(
|
||||
vim.api.nvim_buf_set_extmark(
|
||||
buf,
|
||||
ns_id,
|
||||
ctx.config.hl.directory_path,
|
||||
line_idx - 1,
|
||||
prefix_len,
|
||||
prefix_len + #dir_path
|
||||
{ end_col = prefix_len + #dir_path, hl_group = ctx.config.hl.directory_path }
|
||||
)
|
||||
end
|
||||
|
||||
@@ -210,13 +227,12 @@ function M.apply_highlights(item, ctx, item_idx, buf, ns_id, line_idx, line_cont
|
||||
if ctx.query and ctx.query ~= '' then
|
||||
local match_start, match_end = string.find(line_content, ctx.query, 1)
|
||||
if match_start and match_end then
|
||||
vim.api.nvim_buf_add_highlight(
|
||||
vim.api.nvim_buf_set_extmark(
|
||||
buf,
|
||||
ns_id,
|
||||
ctx.config.hl.matched or 'IncSearch',
|
||||
line_idx - 1,
|
||||
match_start - 1,
|
||||
match_end
|
||||
{ end_col = match_end, hl_group = ctx.config.hl.matched or 'IncSearch' }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
local M = {}
|
||||
|
||||
--- @class ListRenderContext
|
||||
--- @field config table User configuration
|
||||
--- @field config FffConfig User configuration
|
||||
--- @field items table[] Array of data items to render
|
||||
--- @field cursor number Current cursor position (1-based index into items)
|
||||
--- @field win_height number Window height in lines
|
||||
@@ -162,9 +162,9 @@ local function update_buffer_and_cursor(lines, item_to_lines, ctx, list_buf, lis
|
||||
if cursor_item then cursor_line = cursor_item.last end
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_option(list_buf, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = list_buf })
|
||||
vim.api.nvim_buf_set_lines(list_buf, 0, -1, false, lines)
|
||||
vim.api.nvim_buf_set_option(list_buf, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = list_buf })
|
||||
|
||||
vim.api.nvim_buf_clear_namespace(list_buf, ns_id, 0, -1)
|
||||
|
||||
@@ -225,7 +225,14 @@ function M.render(ctx, list_buf, list_win, ns_id)
|
||||
for i = 0, #lines - 1 do
|
||||
local line = lines[i + 1]
|
||||
if line and line:match('^%s+No results found') then
|
||||
pcall(vim.api.nvim_buf_add_highlight, list_buf, ns_id, suggestion_hl, i, 0, -1)
|
||||
pcall(
|
||||
vim.api.nvim_buf_set_extmark,
|
||||
list_buf,
|
||||
ns_id,
|
||||
i,
|
||||
0,
|
||||
{ end_row = i + 1, end_col = 0, hl_group = suggestion_hl }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+5
-12
@@ -18,14 +18,7 @@ function M.find_files(opts)
|
||||
end
|
||||
|
||||
--- Live grep: search file contents in the current directory
|
||||
--- @param opts? table Optional configuration overrides
|
||||
--- @param opts.cwd? string Custom working directory
|
||||
--- @param opts.title? string Window title (default: "Live Grep")
|
||||
--- @param opts.prompt? string Input prompt text (default: "grep> ")
|
||||
--- @param opts.layout? table Layout overrides
|
||||
--- @param opts.grep? table Grep-specific overrides {max_file_size, smart_case, max_matches_per_file, modes}
|
||||
--- @param opts.grep.modes? table Available search modes and their cycling order (default: {'plain', 'regex', 'fuzzy'})
|
||||
--- @param opts.query? string Initial search query to pre-fill
|
||||
--- @param opts? {cwd?: string, title?: string, prompt?: string, layout?: table, grep?: {max_file_size?: number, smart_case?: boolean, max_matches_per_file?: number, modes?: string[]}, query?: string} Optional configuration overrides
|
||||
function M.live_grep(opts)
|
||||
local picker_ok, picker_ui = pcall(require, 'fff.picker_ui')
|
||||
if not picker_ok then
|
||||
@@ -38,13 +31,13 @@ function M.live_grep(opts)
|
||||
|
||||
local grep_config = vim.tbl_deep_extend('force', config.grep or {}, (opts and opts.grep) or {})
|
||||
|
||||
local picker_opts = vim.tbl_deep_extend('force', opts or {}, {
|
||||
title = (opts and opts.title) or 'Live Grep',
|
||||
local picker_opts = vim.tbl_deep_extend('force', {
|
||||
title = 'Live Grep',
|
||||
mode = 'grep',
|
||||
renderer = grep_renderer,
|
||||
grep_config = grep_config,
|
||||
query = opts and opts.query or nil,
|
||||
})
|
||||
query = '',
|
||||
}, opts or {})
|
||||
|
||||
picker_ui.open(picker_opts)
|
||||
end
|
||||
|
||||
+356
-275
@@ -67,7 +67,7 @@ local function get_preview_position()
|
||||
local terminal_width = vim.o.columns
|
||||
local terminal_height = vim.o.lines
|
||||
|
||||
return utils.resolve_config_value(
|
||||
local position = utils.resolve_config_value(
|
||||
config.layout.preview_position,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
@@ -75,30 +75,206 @@ local function get_preview_position()
|
||||
'right',
|
||||
'layout.preview_position'
|
||||
)
|
||||
|
||||
local flex = config.layout.flex
|
||||
if flex then
|
||||
local size = flex.size or 130
|
||||
local wrap = flex.wrap or 'top'
|
||||
if terminal_width < size then return wrap end
|
||||
end
|
||||
|
||||
return position
|
||||
end
|
||||
|
||||
return 'right'
|
||||
end
|
||||
|
||||
--- Function-based config options:
|
||||
--- config.layout.width: number|function(terminal_width, terminal_height): number
|
||||
--- config.layout.height: number|function(terminal_width, terminal_height): number
|
||||
--- config.layout.preview_size: number|function(terminal_width, terminal_height): number
|
||||
--- config.layout.preview_position: string|function(terminal_width, terminal_height): string
|
||||
--- config.layout.prompt_position: string|function(terminal_width, terminal_height): string
|
||||
local function compute_layout(config)
|
||||
local debug_enabled_in_preview = M.enabled_preview() and config.debug and config.debug.show_file_info or false
|
||||
|
||||
--- @class LayoutConfig
|
||||
--- @field total_width number
|
||||
--- @field total_height number
|
||||
--- @field start_col number
|
||||
--- @field start_row number
|
||||
--- @field preview_position string|function Preview position ('left'|'right'|'top'|'bottom') or function(terminal_width, terminal_height): string
|
||||
--- @field prompt_position string
|
||||
--- @field debug_enabled boolean
|
||||
--- @field preview_width number
|
||||
--- @field preview_height number
|
||||
--- @field separator_width number
|
||||
--- @field file_info_height number
|
||||
local terminal_width = vim.o.columns
|
||||
local terminal_height = vim.o.lines
|
||||
|
||||
local width_ratio = utils.resolve_config_value(
|
||||
config.layout.width,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
0.8,
|
||||
'layout.width'
|
||||
)
|
||||
local height_ratio = utils.resolve_config_value(
|
||||
config.layout.height,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
0.8,
|
||||
'layout.height'
|
||||
)
|
||||
|
||||
local width = math.floor(terminal_width * width_ratio)
|
||||
local height = math.floor(terminal_height * height_ratio)
|
||||
|
||||
local col_ratio_default = 0.5 - (width_ratio / 2)
|
||||
local col_ratio = col_ratio_default
|
||||
if config.layout.col ~= nil then
|
||||
col_ratio = utils.resolve_config_value(
|
||||
config.layout.col,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
col_ratio_default,
|
||||
'layout.col'
|
||||
)
|
||||
end
|
||||
local row_ratio_default = 0.5 - (height_ratio / 2)
|
||||
local row_ratio = row_ratio_default
|
||||
if config.layout.row ~= nil then
|
||||
row_ratio = utils.resolve_config_value(
|
||||
config.layout.row,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
row_ratio_default,
|
||||
'layout.row'
|
||||
)
|
||||
end
|
||||
|
||||
local col = math.floor(terminal_width * col_ratio)
|
||||
local row = math.floor(terminal_height * row_ratio)
|
||||
|
||||
local prompt_position = get_prompt_position()
|
||||
local preview_position = get_preview_position()
|
||||
|
||||
local preview_size_ratio = utils.resolve_config_value(
|
||||
config.layout.preview_size,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
0.4,
|
||||
'layout.preview_size'
|
||||
)
|
||||
|
||||
local layout_config = {
|
||||
total_width = width,
|
||||
total_height = height,
|
||||
start_col = col,
|
||||
start_row = row,
|
||||
preview_position = preview_position,
|
||||
prompt_position = prompt_position,
|
||||
debug_enabled = debug_enabled_in_preview,
|
||||
preview_width = M.enabled_preview() and math.floor(width * preview_size_ratio) or 0,
|
||||
preview_height = M.enabled_preview() and math.floor(height * preview_size_ratio) or 0,
|
||||
separator_width = 3,
|
||||
file_info_height = debug_enabled_in_preview and 10 or 0,
|
||||
}
|
||||
|
||||
local layout = M.calculate_layout_dimensions(layout_config)
|
||||
return layout, debug_enabled_in_preview
|
||||
end
|
||||
|
||||
--- Build window config tables for list, input, preview, and file_info windows.
|
||||
--- @param layout table The computed layout from calculate_layout_dimensions
|
||||
--- @param config table The picker config
|
||||
--- @return table window_configs Table with list, input, preview, file_info keys
|
||||
local function build_window_configs(layout, config)
|
||||
local border_chars, t_junctions = get_border_chars()
|
||||
local prompt_position = get_prompt_position()
|
||||
local title = ' ' .. (config.title or 'FFFiles') .. ' '
|
||||
|
||||
-- List border: when prompt at bottom, list has top+sides (no bottom); when top, T-junctions at top
|
||||
local list_border = prompt_position == 'bottom'
|
||||
and { border_chars[1], border_chars[2], border_chars[3], border_chars[4], '', '', '', border_chars[8] }
|
||||
or {
|
||||
t_junctions[1],
|
||||
border_chars[2],
|
||||
t_junctions[2],
|
||||
border_chars[4],
|
||||
border_chars[5],
|
||||
border_chars[6],
|
||||
border_chars[7],
|
||||
border_chars[8],
|
||||
}
|
||||
|
||||
local list_cfg = {
|
||||
relative = 'editor',
|
||||
width = layout.list_width,
|
||||
height = layout.list_height,
|
||||
col = layout.list_col,
|
||||
row = layout.list_row,
|
||||
border = list_border,
|
||||
style = 'minimal',
|
||||
}
|
||||
if prompt_position == 'bottom' then
|
||||
list_cfg.title = title
|
||||
list_cfg.title_pos = 'left'
|
||||
end
|
||||
|
||||
-- Input border: inverse of list border
|
||||
local input_border = prompt_position == 'bottom'
|
||||
and {
|
||||
t_junctions[1],
|
||||
border_chars[2],
|
||||
t_junctions[2],
|
||||
border_chars[4],
|
||||
border_chars[5],
|
||||
border_chars[6],
|
||||
border_chars[7],
|
||||
border_chars[8],
|
||||
}
|
||||
or { border_chars[1], border_chars[2], border_chars[3], border_chars[4], '', '', '', border_chars[8] }
|
||||
|
||||
local input_cfg = {
|
||||
relative = 'editor',
|
||||
width = layout.input_width,
|
||||
height = 1,
|
||||
col = layout.input_col,
|
||||
row = layout.input_row,
|
||||
border = input_border,
|
||||
style = 'minimal',
|
||||
}
|
||||
if prompt_position == 'top' then
|
||||
input_cfg.title = title
|
||||
input_cfg.title_pos = 'left'
|
||||
end
|
||||
|
||||
local preview_cfg = nil
|
||||
if layout.preview then
|
||||
preview_cfg = {
|
||||
relative = 'editor',
|
||||
width = layout.preview.width,
|
||||
height = layout.preview.height,
|
||||
col = layout.preview.col,
|
||||
row = layout.preview.row,
|
||||
style = 'minimal',
|
||||
border = border_chars,
|
||||
title = ' Preview ',
|
||||
title_pos = 'left',
|
||||
}
|
||||
end
|
||||
|
||||
local file_info_cfg = nil
|
||||
if layout.file_info then
|
||||
file_info_cfg = {
|
||||
relative = 'editor',
|
||||
width = layout.file_info.width,
|
||||
height = layout.file_info.height,
|
||||
col = layout.file_info.col,
|
||||
row = layout.file_info.row,
|
||||
style = 'minimal',
|
||||
border = border_chars,
|
||||
title = ' File Info ',
|
||||
title_pos = 'left',
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
list = list_cfg,
|
||||
input = input_cfg,
|
||||
preview = preview_cfg,
|
||||
file_info = file_info_cfg,
|
||||
}
|
||||
end
|
||||
|
||||
--- Calculate layout dimensions and positions for all windows
|
||||
--- @param cfg table
|
||||
@@ -307,7 +483,7 @@ M.state = {
|
||||
grep_next_file_offset = 0,
|
||||
},
|
||||
|
||||
config = nil,
|
||||
config = nil, -- @type FFFConfig|nil
|
||||
|
||||
-- Custom renderer (optional, defaults to file_renderer if not provided)
|
||||
renderer = nil,
|
||||
@@ -350,216 +526,48 @@ M.state = {
|
||||
|
||||
function M.create_ui()
|
||||
local config = M.state.config
|
||||
if not config then return false end
|
||||
|
||||
if not M.state.ns_id then
|
||||
M.state.ns_id = vim.api.nvim_create_namespace('fff_picker_status')
|
||||
combo_renderer.init(M.state.ns_id)
|
||||
end
|
||||
|
||||
local debug_enabled_in_preview = M.enabled_preview() and config and config.debug and config.debug.show_file_info
|
||||
|
||||
local terminal_width = vim.o.columns
|
||||
local terminal_height = vim.o.lines
|
||||
|
||||
-- Calculate width and height (support function or number)
|
||||
---@diagnostic disable: need-check-nil
|
||||
local width_ratio = utils.resolve_config_value(
|
||||
config.layout.width,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
0.8,
|
||||
'layout.width'
|
||||
)
|
||||
local height_ratio = utils.resolve_config_value(
|
||||
config.layout.height,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
0.8,
|
||||
'layout.height'
|
||||
)
|
||||
|
||||
local width = math.floor(terminal_width * width_ratio)
|
||||
local height = math.floor(terminal_height * height_ratio)
|
||||
|
||||
-- Calculate col and row (support function or number)
|
||||
local col_ratio_default = 0.5 - (width_ratio / 2) -- default center
|
||||
local col_ratio
|
||||
if config.layout.col ~= nil then
|
||||
col_ratio = utils.resolve_config_value(
|
||||
config.layout.col,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
col_ratio_default,
|
||||
'layout.col'
|
||||
)
|
||||
else
|
||||
col_ratio = col_ratio_default
|
||||
end
|
||||
local row_ratio_default = 0.5 - (height_ratio / 2) -- default center
|
||||
local row_ratio
|
||||
if config.layout.row ~= nil then
|
||||
row_ratio = utils.resolve_config_value(
|
||||
config.layout.row,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
row_ratio_default,
|
||||
'layout.row'
|
||||
)
|
||||
else
|
||||
row_ratio = row_ratio_default
|
||||
end
|
||||
|
||||
local col = math.floor(terminal_width * col_ratio)
|
||||
local row = math.floor(terminal_height * row_ratio)
|
||||
|
||||
local prompt_position = get_prompt_position()
|
||||
local preview_position = get_preview_position()
|
||||
|
||||
local preview_size_ratio = utils.resolve_config_value(
|
||||
config.layout.preview_size,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
utils.is_valid_ratio,
|
||||
0.4,
|
||||
'layout.preview_size'
|
||||
)
|
||||
---@diagnostic enable: need-check-nil
|
||||
|
||||
local layout_config = {
|
||||
total_width = width,
|
||||
total_height = height,
|
||||
start_col = col,
|
||||
start_row = row,
|
||||
preview_position = preview_position,
|
||||
prompt_position = prompt_position,
|
||||
debug_enabled = debug_enabled_in_preview,
|
||||
preview_width = M.enabled_preview() and math.floor(width * preview_size_ratio) or 0,
|
||||
preview_height = M.enabled_preview() and math.floor(height * preview_size_ratio) or 0,
|
||||
separator_width = 3,
|
||||
file_info_height = debug_enabled_in_preview and 10 or 0,
|
||||
}
|
||||
|
||||
local layout = M.calculate_layout_dimensions(layout_config)
|
||||
local layout, debug_enabled_in_preview = compute_layout(config)
|
||||
M.state.layout = layout
|
||||
|
||||
M.state.input_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_option(M.state.input_buf, 'bufhidden', 'wipe')
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = M.state.input_buf })
|
||||
|
||||
M.state.list_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_option(M.state.list_buf, 'bufhidden', 'wipe')
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = M.state.list_buf })
|
||||
|
||||
if M.enabled_preview() then
|
||||
M.state.preview_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_option(M.state.preview_buf, 'bufhidden', 'wipe')
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = M.state.preview_buf })
|
||||
end
|
||||
|
||||
if debug_enabled_in_preview then
|
||||
M.state.file_info_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_option(M.state.file_info_buf, 'bufhidden', 'wipe')
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = M.state.file_info_buf })
|
||||
else
|
||||
M.state.file_info_buf = nil
|
||||
end
|
||||
|
||||
local border_chars, t_junctions = get_border_chars()
|
||||
local list_window_config = {
|
||||
relative = 'editor',
|
||||
width = layout.list_width,
|
||||
height = layout.list_height,
|
||||
col = layout.list_col,
|
||||
row = layout.list_row,
|
||||
-- To make the input feel connected with the picker, we customize the
|
||||
-- respective corner border characters based on prompt_position
|
||||
-- When prompt at bottom: list has top border + sides, no bottom (connects to input below)
|
||||
-- When prompt at top: list has sides + bottom with T-junctions at top (connects to input above)
|
||||
border = prompt_position == 'bottom'
|
||||
and { border_chars[1], border_chars[2], border_chars[3], border_chars[4], '', '', '', border_chars[8] }
|
||||
or {
|
||||
t_junctions[1],
|
||||
border_chars[2],
|
||||
t_junctions[2],
|
||||
border_chars[4],
|
||||
border_chars[5],
|
||||
border_chars[6],
|
||||
border_chars[7],
|
||||
border_chars[8],
|
||||
},
|
||||
style = 'minimal',
|
||||
}
|
||||
local win_configs = build_window_configs(layout, config)
|
||||
|
||||
local title = ' ' .. (M.state.config.title or 'FFFiles') .. ' '
|
||||
-- Only add title if prompt is at bottom - when prompt is top, title should be on input
|
||||
if prompt_position == 'bottom' then
|
||||
list_window_config.title = title
|
||||
list_window_config.title_pos = 'left'
|
||||
end
|
||||
|
||||
M.state.list_win = vim.api.nvim_open_win(M.state.list_buf, false, list_window_config)
|
||||
|
||||
-- Create file info window if debug enabled
|
||||
if debug_enabled_in_preview and layout.file_info then
|
||||
M.state.file_info_win = vim.api.nvim_open_win(M.state.file_info_buf, false, {
|
||||
relative = 'editor',
|
||||
width = layout.file_info.width,
|
||||
height = layout.file_info.height,
|
||||
col = layout.file_info.col,
|
||||
row = layout.file_info.row,
|
||||
style = 'minimal',
|
||||
border = border_chars,
|
||||
title = ' File Info ',
|
||||
title_pos = 'left',
|
||||
})
|
||||
M.state.list_win = vim.api.nvim_open_win(M.state.list_buf, false, win_configs.list)
|
||||
if debug_enabled_in_preview and win_configs.file_info then
|
||||
M.state.file_info_win = vim.api.nvim_open_win(M.state.file_info_buf, false, win_configs.file_info)
|
||||
else
|
||||
M.state.file_info_win = nil
|
||||
end
|
||||
|
||||
-- Create preview window
|
||||
if M.enabled_preview() and layout.preview then
|
||||
M.state.preview_win = vim.api.nvim_open_win(M.state.preview_buf, false, {
|
||||
relative = 'editor',
|
||||
width = layout.preview.width,
|
||||
height = layout.preview.height,
|
||||
col = layout.preview.col,
|
||||
row = layout.preview.row,
|
||||
style = 'minimal',
|
||||
border = border_chars,
|
||||
title = ' Preview ',
|
||||
title_pos = 'left',
|
||||
})
|
||||
if M.enabled_preview() and win_configs.preview then
|
||||
M.state.preview_win = vim.api.nvim_open_win(M.state.preview_buf, false, win_configs.preview)
|
||||
end
|
||||
|
||||
local input_window_config = {
|
||||
relative = 'editor',
|
||||
width = layout.input_width,
|
||||
height = 1,
|
||||
col = layout.input_col,
|
||||
row = layout.input_row,
|
||||
-- To make the input feel connected with the picker, we customize the
|
||||
-- respective corner border characters based on prompt_position
|
||||
-- if prompt at bottom: input has T-junctions at top (connects to list above), full bottom border
|
||||
-- if prompt at top: input has top border + sides, no bottom (connects to list below)
|
||||
border = prompt_position == 'bottom' and {
|
||||
t_junctions[1],
|
||||
border_chars[2],
|
||||
t_junctions[2],
|
||||
border_chars[4],
|
||||
border_chars[5],
|
||||
border_chars[6],
|
||||
border_chars[7],
|
||||
border_chars[8],
|
||||
} or { border_chars[1], border_chars[2], border_chars[3], border_chars[4], '', '', '', border_chars[8] },
|
||||
style = 'minimal',
|
||||
}
|
||||
|
||||
if prompt_position == 'top' then
|
||||
input_window_config.title = title
|
||||
input_window_config.title_pos = 'left'
|
||||
end
|
||||
|
||||
M.state.input_win = vim.api.nvim_open_win(M.state.input_buf, false, input_window_config)
|
||||
M.state.input_win = vim.api.nvim_open_win(M.state.input_buf, false, win_configs.input)
|
||||
|
||||
M.setup_buffers()
|
||||
M.setup_windows()
|
||||
@@ -581,8 +589,8 @@ function M.setup_buffers()
|
||||
vim.api.nvim_buf_set_name(M.state.list_buf, 'fffiles list')
|
||||
if M.enabled_preview() then vim.api.nvim_buf_set_name(M.state.preview_buf, 'fffile preview') end
|
||||
|
||||
vim.api.nvim_buf_set_option(M.state.input_buf, 'buftype', 'prompt')
|
||||
vim.api.nvim_buf_set_option(M.state.input_buf, 'filetype', 'fff_input')
|
||||
vim.api.nvim_set_option_value('buftype', 'prompt', { buf = M.state.input_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'fff_input', { buf = M.state.input_buf })
|
||||
|
||||
vim.fn.prompt_setprompt(M.state.input_buf, M.state.config.prompt)
|
||||
|
||||
@@ -590,23 +598,23 @@ function M.setup_buffers()
|
||||
-- syntax highlighting. This makes sure that it's always off.
|
||||
vim.api.nvim_create_autocmd('Syntax', {
|
||||
buffer = M.state.input_buf,
|
||||
callback = function() vim.api.nvim_buf_set_option(M.state.input_buf, 'syntax', '') end,
|
||||
callback = function() vim.api.nvim_set_option_value('syntax', '', { buf = M.state.input_buf }) end,
|
||||
})
|
||||
|
||||
vim.api.nvim_buf_set_option(M.state.list_buf, 'buftype', 'nofile')
|
||||
vim.api.nvim_buf_set_option(M.state.list_buf, 'filetype', 'fff_list')
|
||||
vim.api.nvim_buf_set_option(M.state.list_buf, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = M.state.list_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'fff_list', { buf = M.state.list_buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = M.state.list_buf })
|
||||
|
||||
if M.state.file_info_buf then
|
||||
vim.api.nvim_buf_set_option(M.state.file_info_buf, 'buftype', 'nofile')
|
||||
vim.api.nvim_buf_set_option(M.state.file_info_buf, 'filetype', 'fff_file_info')
|
||||
vim.api.nvim_buf_set_option(M.state.file_info_buf, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = M.state.file_info_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'fff_file_info', { buf = M.state.file_info_buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = M.state.file_info_buf })
|
||||
end
|
||||
|
||||
if M.enabled_preview() then
|
||||
vim.api.nvim_buf_set_option(M.state.preview_buf, 'buftype', 'nofile')
|
||||
vim.api.nvim_buf_set_option(M.state.preview_buf, 'filetype', 'fff_preview')
|
||||
vim.api.nvim_buf_set_option(M.state.preview_buf, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = M.state.preview_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'fff_preview', { buf = M.state.preview_buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = M.state.preview_buf })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -614,34 +622,64 @@ function M.setup_windows()
|
||||
local hl = M.state.config.hl
|
||||
local win_hl = string.format('Normal:%s,FloatBorder:%s,FloatTitle:%s', hl.normal, hl.border, hl.title)
|
||||
|
||||
vim.api.nvim_win_set_option(M.state.input_win, 'wrap', false)
|
||||
vim.api.nvim_win_set_option(M.state.input_win, 'cursorline', false)
|
||||
vim.api.nvim_win_set_option(M.state.input_win, 'number', false)
|
||||
vim.api.nvim_win_set_option(M.state.input_win, 'relativenumber', false)
|
||||
vim.api.nvim_win_set_option(M.state.input_win, 'signcolumn', 'no')
|
||||
vim.api.nvim_win_set_option(M.state.input_win, 'foldcolumn', '0')
|
||||
vim.api.nvim_win_set_option(M.state.input_win, 'winhighlight', win_hl)
|
||||
vim.api.nvim_set_option_value('wrap', false, { win = M.state.input_win })
|
||||
vim.api.nvim_set_option_value('cursorline', false, { win = M.state.input_win })
|
||||
vim.api.nvim_set_option_value('number', false, { win = M.state.input_win })
|
||||
vim.api.nvim_set_option_value('relativenumber', false, { win = M.state.input_win })
|
||||
vim.api.nvim_set_option_value('signcolumn', 'no', { win = M.state.input_win })
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = M.state.input_win })
|
||||
vim.api.nvim_set_option_value('winhighlight', win_hl, { win = M.state.input_win })
|
||||
|
||||
vim.api.nvim_win_set_option(M.state.list_win, 'wrap', false)
|
||||
vim.api.nvim_win_set_option(M.state.list_win, 'cursorline', false)
|
||||
vim.api.nvim_win_set_option(M.state.list_win, 'number', false)
|
||||
vim.api.nvim_win_set_option(M.state.list_win, 'relativenumber', false)
|
||||
vim.api.nvim_win_set_option(M.state.list_win, 'signcolumn', 'yes:1') -- Enable signcolumn for git status borders
|
||||
vim.api.nvim_win_set_option(M.state.list_win, 'foldcolumn', '0')
|
||||
vim.api.nvim_win_set_option(M.state.list_win, 'winhighlight', win_hl)
|
||||
vim.api.nvim_set_option_value('wrap', false, { win = M.state.list_win })
|
||||
vim.api.nvim_set_option_value('cursorline', false, { win = M.state.list_win })
|
||||
vim.api.nvim_set_option_value('number', false, { win = M.state.list_win })
|
||||
vim.api.nvim_set_option_value('relativenumber', false, { win = M.state.list_win })
|
||||
vim.api.nvim_set_option_value('signcolumn', 'yes:1', { win = M.state.list_win }) -- Enable signcolumn for git status borders
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = M.state.list_win })
|
||||
vim.api.nvim_set_option_value('winhighlight', win_hl, { win = M.state.list_win })
|
||||
|
||||
if M.enabled_preview() then
|
||||
vim.api.nvim_win_set_option(M.state.preview_win, 'wrap', false)
|
||||
vim.api.nvim_win_set_option(M.state.preview_win, 'cursorline', M.state.mode == 'grep')
|
||||
vim.api.nvim_win_set_option(
|
||||
M.state.preview_win,
|
||||
'number',
|
||||
M.state.mode == 'grep' or (preview_config and preview_config.line_numbers or false)
|
||||
vim.api.nvim_set_option_value('wrap', false, { win = M.state.preview_win })
|
||||
vim.api.nvim_set_option_value('cursorline', M.state.mode == 'grep', { win = M.state.preview_win })
|
||||
|
||||
local cursorlineopt = utils.resolve_config_value(
|
||||
preview_config.cursorlineopt,
|
||||
vim.o.columns,
|
||||
vim.o.lines,
|
||||
function(value)
|
||||
if type(value) ~= 'string' or #value == 0 then return false end
|
||||
|
||||
local has_line = false
|
||||
local has_screenline = false
|
||||
for opt in value:gmatch('[^,]+') do
|
||||
if not utils.is_one_of(opt:gsub('%s+', ''), { 'line', 'screenline', 'number', 'both' }) then return false end
|
||||
if opt == 'line' or opt == 'both' then has_line = true end
|
||||
if opt == 'screenline' then has_screenline = true end
|
||||
end
|
||||
|
||||
if has_line and has_screenline then return false end
|
||||
|
||||
return true
|
||||
end,
|
||||
'both',
|
||||
'preview.cursorlineopt'
|
||||
)
|
||||
vim.api.nvim_win_set_option(M.state.preview_win, 'relativenumber', false)
|
||||
vim.api.nvim_win_set_option(M.state.preview_win, 'signcolumn', 'no')
|
||||
vim.api.nvim_win_set_option(M.state.preview_win, 'foldcolumn', '0')
|
||||
vim.api.nvim_win_set_option(M.state.preview_win, 'winhighlight', win_hl)
|
||||
|
||||
vim.api.nvim_set_option_value(
|
||||
'cursorlineopt',
|
||||
M.state.mode == 'grep' and cursorlineopt or vim.o.cursorlineopt,
|
||||
{ win = M.state.preview_win }
|
||||
)
|
||||
|
||||
vim.api.nvim_set_option_value(
|
||||
'number',
|
||||
M.state.mode == 'grep' or (preview_config and preview_config.line_numbers or false),
|
||||
{ win = M.state.preview_win }
|
||||
)
|
||||
vim.api.nvim_set_option_value('relativenumber', false, { win = M.state.preview_win })
|
||||
vim.api.nvim_set_option_value('signcolumn', 'no', { win = M.state.preview_win })
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = M.state.preview_win })
|
||||
vim.api.nvim_set_option_value('winhighlight', win_hl, { win = M.state.preview_win })
|
||||
end
|
||||
|
||||
local picker_group = vim.api.nvim_create_augroup('fff_picker_focus', { clear = true })
|
||||
@@ -685,6 +723,18 @@ function M.setup_windows()
|
||||
end,
|
||||
desc = 'Close picker when focus leaves picker windows',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('VimResized', {
|
||||
group = picker_group,
|
||||
callback = function()
|
||||
if not M.state.active then return end
|
||||
vim.schedule(function()
|
||||
if not M.state.active then return end
|
||||
M.relayout()
|
||||
end)
|
||||
end,
|
||||
desc = 'Re-layout picker on terminal resize',
|
||||
})
|
||||
end
|
||||
|
||||
local function set_keymap(mode, keys, handler, opts)
|
||||
@@ -779,7 +829,7 @@ function M.setup_keymaps()
|
||||
set_keymap({ 'i', 'n' }, keymaps.toggle_debug, M.toggle_debug, input_opts)
|
||||
set_keymap({ 'i', 'n' }, keymaps.toggle_select, M.toggle_select, input_opts)
|
||||
set_keymap({ 'i', 'n' }, keymaps.send_to_quickfix, M.send_to_quickfix, input_opts)
|
||||
set_keymap({ 'i', 'n' }, keymaps.toggle_grep_regex, M.toggle_grep_regex, input_opts)
|
||||
set_keymap({ 'i', 'n' }, keymaps.cycle_grep_modes, M.cycle_grep_modes, input_opts)
|
||||
|
||||
-- List buffer
|
||||
set_keymap('n', keymaps.close, M.close, list_opts)
|
||||
@@ -878,7 +928,7 @@ end
|
||||
--- Cycle through grep search modes based on configured modes list.
|
||||
--- Only works when the picker is in grep mode. Triggers a re-search
|
||||
--- with the current query using the new mode.
|
||||
function M.toggle_grep_regex()
|
||||
function M.cycle_grep_modes()
|
||||
if not M.state.active or M.state.mode ~= 'grep' then return end
|
||||
|
||||
local config = conf.get()
|
||||
@@ -929,7 +979,7 @@ function M.on_input_change()
|
||||
|
||||
query = query:gsub('\r', ''):match('^%s*(.-)%s*$') or ''
|
||||
|
||||
vim.api.nvim_buf_set_option(M.state.input_buf, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = M.state.input_buf })
|
||||
vim.api.nvim_buf_set_lines(M.state.input_buf, 0, -1, false, { M.state.config.prompt .. query })
|
||||
|
||||
-- Move cursor to end
|
||||
@@ -1125,7 +1175,7 @@ function M.load_page_at_index(new_page_index, adjust_cursor_fn)
|
||||
|
||||
if not ok then
|
||||
vim.notify('Error in paginated search: ' .. tostring(results), vim.log.levels.ERROR)
|
||||
vim.api.nvim_err_writeln('FFF ERROR: Paginated search failed: ' .. tostring(results))
|
||||
vim.notify('FFF ERROR: Paginated search failed: ' .. tostring(results))
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -1343,16 +1393,23 @@ local function render_grep_empty_state(ctx)
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_option(M.state.list_buf, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = M.state.list_buf })
|
||||
vim.api.nvim_buf_set_lines(M.state.list_buf, 0, -1, false, content)
|
||||
vim.api.nvim_buf_set_option(M.state.list_buf, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = M.state.list_buf })
|
||||
|
||||
vim.api.nvim_buf_clear_namespace(M.state.list_buf, M.state.ns_id, 0, -1)
|
||||
|
||||
-- For bottom prompt, ensure empty state is anchored at the bottom
|
||||
if prompt_position == 'bottom' then scroll_to_bottom() end
|
||||
for _, h in ipairs(hl_cmds) do
|
||||
pcall(vim.api.nvim_buf_add_highlight, M.state.list_buf, M.state.ns_id, h.hl, h.row, h.col_start, h.col_end)
|
||||
pcall(
|
||||
vim.api.nvim_buf_set_extmark,
|
||||
M.state.list_buf,
|
||||
M.state.ns_id,
|
||||
h.row,
|
||||
h.col_start,
|
||||
{ end_col = h.col_end, hl_group = h.hl }
|
||||
)
|
||||
end
|
||||
|
||||
for i = 0, #content - 1 do
|
||||
@@ -1360,18 +1417,24 @@ local function render_grep_empty_state(ctx)
|
||||
if
|
||||
line and (line:match('^%s+Start typing') or line:match('^%s+Tips') or line:match('^%s+"') or line:match('^%s+!'))
|
||||
then
|
||||
pcall(vim.api.nvim_buf_add_highlight, M.state.list_buf, M.state.ns_id, 'Comment', i, 0, -1)
|
||||
pcall(
|
||||
vim.api.nvim_buf_set_extmark,
|
||||
M.state.list_buf,
|
||||
M.state.ns_id,
|
||||
i,
|
||||
0,
|
||||
{ end_row = i + 1, end_col = 0, hl_group = 'Comment' }
|
||||
)
|
||||
end
|
||||
-- Dim border characters
|
||||
if line and (line:match('^%s+[╭╰│]') or line:match('[╮╯│]%s*$')) then
|
||||
pcall(
|
||||
vim.api.nvim_buf_add_highlight,
|
||||
vim.api.nvim_buf_set_extmark,
|
||||
M.state.list_buf,
|
||||
M.state.ns_id,
|
||||
config.hl.border or 'FloatBorder',
|
||||
i,
|
||||
0,
|
||||
-1
|
||||
{ end_row = i + 1, end_col = 0, hl_group = config.hl.border or 'FloatBorder' }
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1403,7 +1466,7 @@ local function build_render_context()
|
||||
local text_width = win_width - text_offset
|
||||
|
||||
-- Combo detection (only in file picker mode with real results, not grep or suggestions)
|
||||
local combo_boost_score_multiplier = config.history and config.history.combo_boost_multiplier or 100
|
||||
local combo_boost_score_multiplier = config.history and config.history.combo_boost_score_multiplier or 100
|
||||
local has_combo, combo_header_line, combo_header_text_len, combo_item_index
|
||||
if M.state.mode == 'grep' or M.state.suggestion_source then
|
||||
has_combo = false
|
||||
@@ -1669,7 +1732,7 @@ function M.clear_preview()
|
||||
})
|
||||
|
||||
if M.state.file_info_buf then
|
||||
vim.api.nvim_buf_set_option(M.state.file_info_buf, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = M.state.file_info_buf })
|
||||
vim.api.nvim_buf_set_lines(M.state.file_info_buf, 0, -1, false, {
|
||||
'File Info Panel',
|
||||
'',
|
||||
@@ -1682,12 +1745,12 @@ function M.clear_preview()
|
||||
'',
|
||||
'Navigate: ↑↓ or Ctrl+p/n',
|
||||
})
|
||||
vim.api.nvim_buf_set_option(M.state.file_info_buf, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = M.state.file_info_buf })
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_option(M.state.preview_buf, 'modifiable', true)
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = M.state.preview_buf })
|
||||
vim.api.nvim_buf_set_lines(M.state.preview_buf, 0, -1, false, { 'No preview available' })
|
||||
vim.api.nvim_buf_set_option(M.state.preview_buf, 'modifiable', false)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = M.state.preview_buf })
|
||||
end
|
||||
|
||||
--- Update status information on the right side of input using virtual text
|
||||
@@ -1716,8 +1779,7 @@ function M.update_status(progress)
|
||||
return
|
||||
end
|
||||
|
||||
-- Grep mode: show keybind + label, e.g. "<S-Tab> fuzzy"
|
||||
local keybind = config.keymaps.toggle_grep_regex
|
||||
local keybind = config.keymaps.cycle_grep_modes
|
||||
-- Normalize: if it's a table of keys, use the first one for display
|
||||
if type(keybind) == 'table' then keybind = keybind[1] or '<S-Tab>' end
|
||||
|
||||
@@ -2005,9 +2067,9 @@ local function find_suitable_window()
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
local buftype = vim.api.nvim_buf_get_option(buf, 'buftype')
|
||||
local modifiable = vim.api.nvim_buf_get_option(buf, 'modifiable')
|
||||
local filetype = vim.api.nvim_buf_get_option(buf, 'filetype')
|
||||
local buftype = vim.api.nvim_get_option_value('buftype', { buf = buf })
|
||||
local modifiable = vim.api.nvim_get_option_value('modifiable', { buf = buf })
|
||||
local filetype = vim.api.nvim_get_option_value('filetype', { buf = buf })
|
||||
|
||||
local is_picker_window = (
|
||||
win == M.state.input_win
|
||||
@@ -2210,8 +2272,8 @@ function M.select(action)
|
||||
|
||||
if action == 'edit' then
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
local current_buftype = vim.api.nvim_buf_get_option(current_buf, 'buftype')
|
||||
local current_buf_modifiable = vim.api.nvim_buf_get_option(current_buf, 'modifiable')
|
||||
local current_buftype = vim.api.nvim_get_option_value('buftype', { buf = current_buf })
|
||||
local current_buf_modifiable = vim.api.nvim_get_option_value('modifiable', { buf = current_buf })
|
||||
|
||||
-- If current active buffer is not a normal buffer we find a suitable window with a tab otherwise opening a new split
|
||||
if current_buftype ~= '' or not current_buf_modifiable then
|
||||
@@ -2247,6 +2309,39 @@ function M.select(action)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.relayout()
|
||||
if not M.state.active then return end
|
||||
|
||||
local config = M.state.config
|
||||
if not config then return end
|
||||
|
||||
local layout, _ = compute_layout(config)
|
||||
M.state.layout = layout
|
||||
|
||||
local win_configs = build_window_configs(layout, config)
|
||||
|
||||
if M.state.list_win and vim.api.nvim_win_is_valid(M.state.list_win) then
|
||||
vim.api.nvim_win_set_config(M.state.list_win, win_configs.list)
|
||||
end
|
||||
|
||||
if M.state.input_win and vim.api.nvim_win_is_valid(M.state.input_win) then
|
||||
vim.api.nvim_win_set_config(M.state.input_win, win_configs.input)
|
||||
end
|
||||
|
||||
if M.state.preview_win and vim.api.nvim_win_is_valid(M.state.preview_win) and win_configs.preview then
|
||||
vim.api.nvim_win_set_config(M.state.preview_win, win_configs.preview)
|
||||
end
|
||||
|
||||
if M.state.file_info_win and vim.api.nvim_win_is_valid(M.state.file_info_win) and win_configs.file_info then
|
||||
vim.api.nvim_win_set_config(M.state.file_info_win, win_configs.file_info)
|
||||
end
|
||||
|
||||
-- now rerenderw ith the new computed windows
|
||||
M.render_list()
|
||||
M.update_preview()
|
||||
M.update_status()
|
||||
end
|
||||
|
||||
function M.close()
|
||||
if not M.state.active then return end
|
||||
|
||||
@@ -2467,21 +2562,7 @@ function M.open_with_callback(query, callback, opts)
|
||||
end
|
||||
|
||||
--- Open the file picker UI
|
||||
--- @param opts? table Optional configuration to override defaults
|
||||
--- @param opts.cwd? string Custom working directory (default: vim.fn.getcwd())
|
||||
--- @param opts.title? string Window title (default: "FFFiles")
|
||||
--- @param opts.prompt? string Input prompt text (default: "🪿 ")
|
||||
--- @param opts.max_results? number Maximum number of results to display (default: 100)
|
||||
--- @param opts.max_threads? number Maximum number of threads for file scanning (default: 4)
|
||||
--- @param opts.layout? table Layout configuration
|
||||
--- @param opts.layout.width? number|function Window width as ratio (0.0-1.0) or function(terminal_width, terminal_height): number (default: 0.8)
|
||||
--- @param opts.layout.height? number|function Window height as ratio (0.0-1.0) or function(terminal_width, terminal_height): number (default: 0.8)
|
||||
--- @param opts.layout.prompt_position? string|function Prompt position: 'top'|'bottom' or function(terminal_width, terminal_height): string (default: 'bottom')
|
||||
--- @param opts.layout.preview_position? string|function Preview position: 'left'|'right'|'top'|'bottom' or function(terminal_width, terminal_height): string (default: 'right')
|
||||
--- @param opts.layout.preview_size? number|function Preview size as ratio (0.0-1.0) or function(terminal_width, terminal_height): number (default: 0.5)
|
||||
--- @param opts.renderer? table Custom renderer implementing {render_line, apply_highlights} interface (default: file_renderer)
|
||||
--- @param opts.mode? string Picker mode: nil (default file picker) or 'grep' (live grep)
|
||||
--- @param opts.grep_config? table Grep-specific config overrides {max_file_size, smart_case, max_matches_per_file}
|
||||
--- @param opts? {cwd?: string, title?: string, prompt?: string, max_results?: number, max_threads?: number, layout?: {width?: number|function, height?: number|function, prompt_position?: string|function, preview_position?: string|function, preview_size?: number|function}, renderer?: table, mode?: string, grep_config?: table, query?: string} Optional configuration to override defaults
|
||||
function M.open(opts)
|
||||
if M.state.active then return end
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ function M.render(layout, config, list_win, pagination, prompt_position)
|
||||
-- rendering in a separate buffer to overflow the border
|
||||
if not scrollbar_exists then
|
||||
scrollbar_state.buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_option(scrollbar_state.buf, 'bufhidden', 'wipe')
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = scrollbar_state.buf })
|
||||
|
||||
scrollbar_state.win = vim.api.nvim_open_win(scrollbar_state.buf, false, {
|
||||
relative = 'editor',
|
||||
@@ -52,7 +52,7 @@ function M.render(layout, config, list_win, pagination, prompt_position)
|
||||
})
|
||||
|
||||
local scrollbar_hl = string.format('Normal:%s', config.hl.border)
|
||||
vim.api.nvim_win_set_option(scrollbar_state.win, 'winhighlight', scrollbar_hl)
|
||||
vim.api.nvim_set_option_value('winhighlight', scrollbar_hl, { win = scrollbar_state.win })
|
||||
|
||||
scrollbar_state.ever_shown = true
|
||||
end
|
||||
@@ -83,9 +83,9 @@ function M.render(layout, config, list_win, pagination, prompt_position)
|
||||
end
|
||||
end
|
||||
|
||||
pcall(vim.api.nvim_buf_set_option, scrollbar_state.buf, 'modifiable', true)
|
||||
pcall(vim.api.nvim_set_option_value, 'modifiable', true, { buf = scrollbar_state.buf })
|
||||
pcall(vim.api.nvim_buf_set_lines, scrollbar_state.buf, 0, -1, false, lines)
|
||||
pcall(vim.api.nvim_buf_set_option, scrollbar_state.buf, 'modifiable', false)
|
||||
pcall(vim.api.nvim_set_option_value, 'modifiable', false, { buf = scrollbar_state.buf })
|
||||
|
||||
pcall(vim.api.nvim_buf_clear_namespace, scrollbar_state.buf, ns_id, 0, -1)
|
||||
if thumb_size > 0 then
|
||||
|
||||
@@ -11,18 +11,24 @@ function M.get_triple()
|
||||
if os_name == 'darwin' then
|
||||
os_name = 'apple-darwin'
|
||||
elseif os_name == 'linux' then
|
||||
-- Detect if we're on musl or glibc
|
||||
local handle = io.popen('ldd --version 2>&1')
|
||||
if handle then
|
||||
local output = handle:read('*a')
|
||||
handle:close()
|
||||
if output and output:match('musl') then
|
||||
os_name = 'unknown-linux-musl'
|
||||
-- Detect Android/Termux before checking musl/glibc.
|
||||
-- Termux uses Bionic libc (not glibc or musl) and has no ldd.
|
||||
if os.getenv('TERMUX_VERSION') or os.getenv('ANDROID_ROOT') then
|
||||
os_name = 'linux-android'
|
||||
else
|
||||
-- Detect if we're on musl or glibc
|
||||
local handle = io.popen('ldd --version 2>&1')
|
||||
if handle then
|
||||
local output = handle:read('*a')
|
||||
handle:close()
|
||||
if output and output:match('musl') then
|
||||
os_name = 'unknown-linux-musl'
|
||||
else
|
||||
os_name = 'unknown-linux-gnu'
|
||||
end
|
||||
else
|
||||
os_name = 'unknown-linux-gnu'
|
||||
end
|
||||
else
|
||||
os_name = 'unknown-linux-gnu'
|
||||
end
|
||||
elseif os_name:match('windows') or os_name:match('mingw') or os_name:match('msys') then
|
||||
os_name = 'pc-windows-msvc'
|
||||
|
||||
Reference in New Issue
Block a user