Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Bug report
description: Report a crash, hang, or incorrect behavior in fff (any frontend — nvim plugin, Node/Bun SDK, MCP server, C SDK).
title: "[Bug]: "
labels: ["bug"]
body:
- type: dropdown
id: frontend
attributes:
label: Which fff frontend?
options:
- Neovim plugin (fff.nvim)
- MCP server (fff-mcp)
- Node SDK (@ff-labs/fff-node)
- Bun SDK
- C SDK (libfff)
- Other / multiple
validations:
required: true

- type: textarea
id: logs
attributes:
label: has logs
description: |
Attach your fff log file — the single most useful thing for debugging.

fff writes a fresh log file on every process startup, named `fff+<unix-ts>+<pid>.log`, and keeps the last 20. Find the file matching your crashed/buggy run and drag-and-drop it here (or paste its contents).

Where the log files live:

| Frontend | Linux / macOS | Windows |
|---|---|---|
| Neovim plugin | `~/.local/state/nvim/log/fff+*.log` | `%LOCALAPPDATA%\nvim-data\log\fff+*.log` |
| MCP server (`fff-mcp`) | `~/.cache/fff_mcp+*.log` (override with `--log-file`) | `%LOCALAPPDATA%\fff_mcp+*.log` |
| Node / Bun SDK | path you passed as `logFilePath` to `FileFinder.create({...})` | same |
| C SDK | path you passed as `log_file_path` in `FffCreateOptions` | same |

Neovim users: run `:FFFOpenLog` to open the current session's log directly.

Also some useful commands:

```sh
# Neovim plugin
ls -t ~/.local/state/nvim/log/fff+*.log | head -1

# MCP server
ls -t ~/.cache/fff_mcp+*.log | head -1
```
validations:
required: false

- type: textarea
id: body
attributes:
label: Description
description: Please provide as much helpful information as you can
validations:
required: true
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Discussion / question
url: https://github.com/dmtrKovalenko/fff/discussions
about: For usage questions, design discussion, or anything that's not a bug or feature request.
34 changes: 34 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Feature request
description: Suggest something new for fff
title: "[Suggestion]: "
labels: ["enhancement"]
body:
- type: dropdown
id: frontend
attributes:
label: Which fff frontend(s)?
multiple: true
options:
- Neovim plugin (fff.nvim)
- MCP server (fff-mcp)
- Node SDK (@ff-labs/fff-node)
- Bun SDK (@ff-labs/fff-bun)
- C lib (libfff)
- Core or Rust crate
validations:
required: true

- type: textarea
id: problem
attributes:
label: What problem are you trying to solve?
validations:
required: true

- type: textarea
id: proposal
attributes:
label: Proposed solution
description: If you have an idea of the shape of the API, describe it here.
validations:
required: false
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ heed = "0.22.0"
ignore = "0.4.22"
memmap2 = "0.9"
mimalloc = "0.1.47"
signal-hook-registry = "1.4"
zlob = "1.4.1"

mlua = { version = "0.11.1", features = ["module", "luajit"] }
Expand All @@ -50,7 +51,7 @@ tracing = "0.1"
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true
strip = "debuginfo"

[profile.ci]
inherits = "release"
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ require('fff').setup({
max_threads = 4,
lazy_sync = true,
prompt_vim_mode = false,
follow_symlinks = false,
-- Allow indexing the user's $HOME directory. Enabled by default.
-- Disable if you strictly sure you don't want this, as it makes whole fff error hard
enable_home_dir_scanning = true,
-- Allow indexing a filesystem root (e.g. `/`, `C:\`). Disabled by default
enable_fs_root_scanning = false,
layout = {
height = 0.8,
width = 0.8,
Expand Down Expand Up @@ -340,9 +346,11 @@ require('fff').setup({
},
},
logging = {
enabled = true,
-- logs will be written in a parent directory of this file path in files like
-- `<stem>+<UTC-timestamp>+<pid>.<ext>`. Run :FFFOpenLog to open current one
log_file = vim.fn.stdpath('log') .. '/fff.log',
log_level = 'info',
retain_runs = 20,
},
})
```
Expand Down Expand Up @@ -442,7 +450,9 @@ Run `:FFFScan` to force a rescan.
### Troubleshooting

- `:FFFHealth` verifies picker init, optional dependencies, and DB connectivity.
- `:FFFOpenLog` opens the log file.
- `:FFFOpenLog` opens the current session's log file.
- Historical log files are stored near the main log file `<state>/log/fff+<UTC-timestamp>+<pid>.log` (up to 20 files)
- For a crash backtrace, run `lldb -- nvim` or `gdb -- nvim` and reproduce

</details>

Expand Down
5 changes: 4 additions & 1 deletion crates/fff-c/include/fff.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ typedef struct FffCreateOptions {
*/
bool ai_mode;
/**
* Tracing log file path. NULL/empty to skip log init.
* Path-shape hint for the per-session log file. Each call writes a fresh
* sibling file `<stem>+<UTC-timestamp>+<pid>.<ext>` next to this path.
* The literal path is never written to, so concurrent processes get
* unique per-pid files. NULL/empty to skip log init.
*/
const char *log_file_path;
/**
Expand Down
2 changes: 1 addition & 1 deletion crates/fff-c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ pub unsafe extern "C" fn fff_create_instance_with(opts: *const FffCreateOptions)

if let Some(log_path) = unsafe { optional_cstr(opts.log_file_path) } {
let level = unsafe { optional_cstr(opts.log_level) };
if let Err(e) = fff::log::init_tracing(log_path, level) {
if let Err(e) = fff::log::init_tracing(log_path, level, None) {
return FffResult::err(&format!("Failed to init tracing: {}", e));
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/fff-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ mimalloc = { version = "0.1", optional = true, features = ["local_dynamic_tls"]
[target.'cfg(windows)'.dependencies]
dunce = { workspace = true }

# signal-hook only compiles on unix; we wrap the SIGSEGV handler behind cfg(unix)
[target.'cfg(unix)'.dependencies]
signal-hook-registry = { workspace = true }

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.2"
Expand Down
4 changes: 4 additions & 0 deletions crates/fff-core/src/background_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const MAX_MACOS_NONRECURSIVE_WATCHES: usize = 4096;
const AI_MODE_COOLDOWN_SECS: u64 = 5 * 60;

impl BackgroundWatcher {
#[allow(clippy::too_many_arguments)]
pub fn new(
base_path: PathBuf,
git_workdir: Option<PathBuf>,
Expand All @@ -43,6 +44,7 @@ impl BackgroundWatcher {
mode: FFFMode,
enable_fs_root_scanning: bool,
enable_home_dir_scanning: bool,
trace_span: tracing::Span,
) -> Result<Self, Error> {
info!(
"Initializing background watcher for path: {}, mode: {:?}",
Expand Down Expand Up @@ -103,9 +105,11 @@ impl BackgroundWatcher {
#[cfg(target_os = "linux")]
let owner_debouncer = Arc::clone(&debouncer);

let owner_span = trace_span.clone();
let owner_thread = std::thread::Builder::new()
.name("fff-watcher-own".into())
.spawn(move || {
let _g = owner_span.enter();
while let Ok(dir) = watch_rx.recv() {
// if the picker is dropped we do need to exit the loop
let Some(strong_picker) = owner_weak_picker.upgrade() else {
Expand Down
24 changes: 20 additions & 4 deletions crates/fff-core/src/file_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,8 @@ pub struct FilePicker {
follow_symlinks: bool,
enable_fs_root_scanning: bool,
enable_home_dir_scanning: bool,
trace_span: tracing::Span,
trace_id: String,
}

impl std::fmt::Debug for FilePicker {
Expand Down Expand Up @@ -511,6 +513,14 @@ impl FilePicker {
self.enable_home_dir_scanning
}

pub fn trace_id(&self) -> &str {
&self.trace_id
}

pub fn trace_span(&self) -> tracing::Span {
self.trace_span.clone()
}

pub fn mode(&self) -> FFFMode {
self.mode
}
Expand Down Expand Up @@ -698,6 +708,9 @@ impl FilePicker {
let has_explicit_budget = options.cache_budget.is_some();
let initial_budget = options.cache_budget.unwrap_or_default();

let trace_id = crate::log::generate_trace_id();
let trace_span = crate::log::trace_span(&trace_id, "picker");

Ok(FilePicker {
background_watcher: None,
base_path: path,
Expand All @@ -713,11 +726,13 @@ impl FilePicker {
follow_symlinks: options.follow_symlinks,
enable_fs_root_scanning: options.enable_fs_root_scanning,
enable_home_dir_scanning: options.enable_home_dir_scanning,
trace_span,
trace_id,
})
}

/// Create a picker, place it into the shared handle, and spawn background
/// indexing + file-system watcher. This is the default entry point.
/// indexing + file-system watcgenerate_trace_id the default entry point.
pub fn new_with_shared_state(
shared_picker: SharedFilePicker,
shared_frecency: SharedFrecency,
Expand All @@ -744,13 +759,12 @@ impl FilePicker {
let signals = picker.scan_signals();
let scanned_files_counter = picker.scanned_files_counter();
let path = picker.base_path.clone();
let trace_span = picker.trace_span.clone();

{
let mut guard = shared_picker.write()?;
*guard = Some(picker);
// by dropping the old picker if it exists we triggering
// it's internal `cancelled` flag flip which will automatically clean
// any thread that might be capturing the reference safely & unsfaely
// dropping old picker flips its `cancelled` flag → bg threads exit cleanly
}

ScanJob::new_initial(
Expand All @@ -760,6 +774,7 @@ impl FilePicker {
mode,
signals,
scanned_files_counter,
trace_span,
ScanConfig {
warmup,
content_indexing,
Expand Down Expand Up @@ -848,6 +863,7 @@ impl FilePicker {
self.mode,
self.enable_fs_root_scanning,
self.enable_home_dir_scanning,
self.trace_span.clone(),
)?;
self.background_watcher = Some(watcher);
self.signals.watcher_ready.store(true, Ordering::Release);
Expand Down
Loading
Loading