diff --git a/crates/pi-natives/src/chunk/common.rs b/crates/pi-natives/src/chunk/common.rs index 96774c974..936be0ead 100644 --- a/crates/pi-natives/src/chunk/common.rs +++ b/crates/pi-natives/src/chunk/common.rs @@ -4,28 +4,21 @@ //! [`RawChunkCandidate`] values. They are also used by the default (shared) //! classification in [`super::defaults`]. -use std::sync::LazyLock; - use tree_sitter::Node; use super::types::ChunkNode; +use crate::env_uint; // ── Configuration (environment overrides) ──────────────────────────────── -// -// - `PI_CHUNK_LEAF_THRESHOLD` – default `15` -// - `PI_CHUNK_MAX_LINES` – default `25` - -fn env_usize(name: &str, default: usize) -> usize { - std::env::var(name) - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(default) +env_uint! { + // Configured leaf threshold. + pub static LEAF_THRESHOLD: usize = "PI_CHUNK_LEAF_THRESHOLD" or 15 => [1, usize::MAX]; + // Configured max chunk lines. + pub static MAX_CHUNK_LINES: usize = "PI_CHUNK_MAX_LINES" or 25 => [1, usize::MAX]; + // Configured min recurse savings. + pub static MIN_RECURSE_SAVINGS: usize = "PI_CHUNK_MIN_SAVINGS" or 5 => [1, usize::MAX]; } -pub static LEAF_THRESHOLD: LazyLock = - LazyLock::new(|| env_usize("PI_CHUNK_LEAF_THRESHOLD", 15)); -pub static MAX_CHUNK_LINES: LazyLock = LazyLock::new(|| env_usize("PI_CHUNK_MAX_LINES", 25)); - // ── Internal types ─────────────────────────────────────────────────────── #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/pi-natives/src/chunk/defaults.rs b/crates/pi-natives/src/chunk/defaults.rs index 00942f0b6..7cb46e0d6 100644 --- a/crates/pi-natives/src/chunk/defaults.rs +++ b/crates/pi-natives/src/chunk/defaults.rs @@ -48,6 +48,26 @@ pub fn classify_root_default<'tree>(node: Node<'tree>, source: &str) -> RawChunk group_candidate(node, "stmts", source) }, + // ── Control flow (top-level scripts) ── + "if_statement" + | "unless" + | "guard_statement" + | "switch_statement" + | "switch_expression" + | "case_statement" + | "expression_switch_statement" + | "type_switch_statement" + | "select_statement" + | "try_statement" + | "try_block" + | "for_statement" + | "for_in_statement" + | "for_of_statement" + | "foreach_statement" + | "while_statement" + | "do_statement" + | "with_statement" => classify_function_default(node, source), + // ── Containers / namespaces / modules ── "class_declaration" | "class_definition" @@ -241,8 +261,11 @@ pub fn classify_function_default<'tree>( node: Node<'tree>, source: &str, ) -> RawChunkCandidate<'tree> { + let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody); match node.kind() { - "if_statement" | "unless" | "guard_statement" => positional_candidate(node, "if", source), + "if_statement" | "unless" | "guard_statement" => { + make_candidate(node, "if".to_string(), NameStyle::Named, None, fn_recurse(), false, source) + }, "switch_statement" | "switch_expression" | "case_statement" @@ -251,29 +274,61 @@ pub fn classify_function_default<'tree>( | "type_switch_statement" | "select_statement" | "receive_statement" - | "yul_switch_statement" => positional_candidate(node, "switch", source), + | "yul_switch_statement" => make_candidate( + node, + "switch".to_string(), + NameStyle::Named, + None, + fn_recurse(), + false, + source, + ), "try_statement" | "try_block" | "catch_clause" | "finally_clause" | "assembly_statement" => { - positional_candidate(node, "try", source) + make_candidate( + node, + "try".to_string(), + NameStyle::Named, + None, + fn_recurse(), + false, + source, + ) }, "for_statement" | "for_in_statement" | "for_of_statement" => { - if looks_like_python_statement(node, source) { - positional_candidate(node, "loop", source) + let name = if looks_like_python_statement(node, source) { + "loop".to_string() } else { - positional_candidate(node, "for", source) - } + sanitize_node_kind(node.kind()) + }; + make_candidate(node, name, NameStyle::Named, None, fn_recurse(), false, source) }, "while_statement" => { - if looks_like_python_statement(node, source) { - positional_candidate(node, "loop", source) + let name = if looks_like_python_statement(node, source) { + "loop" } else { - positional_candidate(node, "while", source) - } + "while" + }; + make_candidate(node, name.to_string(), NameStyle::Named, None, fn_recurse(), false, source) }, "do_statement" | "with_statement" | "do_block" | "subshell" | "async_block" - | "unsafe_block" | "const_block" | "block_expression" => { - positional_candidate(node, "block", source) - }, - "foreach_statement" => positional_candidate(node, "for", source), + | "unsafe_block" | "const_block" | "block_expression" => make_candidate( + node, + "block".to_string(), + NameStyle::Named, + None, + fn_recurse(), + false, + source, + ), + "foreach_statement" => make_candidate( + node, + "for".to_string(), + NameStyle::Named, + None, + fn_recurse(), + false, + source, + ), "defer_statement" | "go_statement" | "send_statement" => { group_candidate(node, "stmts", source) }, diff --git a/crates/pi-natives/src/chunk/mod.rs b/crates/pi-natives/src/chunk/mod.rs index 3f62200e6..0dc950060 100644 --- a/crates/pi-natives/src/chunk/mod.rs +++ b/crates/pi-natives/src/chunk/mod.rs @@ -299,7 +299,10 @@ fn build_chunk( let should_recurse = !candidate.error && recurse.is_some() && !should_collapse - && (candidate.force_recurse || line_count > *LEAF_THRESHOLD || recurse_parse_errors > 0); + && (candidate.force_recurse + || recurse_parse_errors > 0 + || (line_count > *LEAF_THRESHOLD + && recursion_narrows_scope(line_count, &child_candidates))); let children = if should_recurse { child_candidates .into_iter() @@ -520,6 +523,22 @@ fn assign_unique_names(mut candidates: Vec>) -> Vec]) -> bool { + if children.is_empty() { + return false; + } + let max_child_lines = children + .iter() + .map(|c| line_span(c.range_start_line, c.range_end_line)) + .max() + .unwrap_or(0); + parent_lines.saturating_sub(max_child_lines) >= *MIN_RECURSE_SAVINGS +} + fn should_collapse_trivial_children( parent: &RawChunkCandidate<'_>, children: &[RawChunkCandidate<'_>], diff --git a/crates/pi-natives/src/chunk/render.rs b/crates/pi-natives/src/chunk/render.rs index d76d90a57..8d138ea54 100644 --- a/crates/pi-natives/src/chunk/render.rs +++ b/crates/pi-natives/src/chunk/render.rs @@ -1,17 +1,20 @@ use std::collections::HashMap; -use crate::chunk::types::{ChunkNode, ChunkTree, RenderChunkTreeParams, VisibleLineRange}; +use crate::{ + chunk::types::{ChunkNode, ChunkTree, RenderChunkTreeParams, VisibleLineRange}, + env_uint, +}; type ChunkLookup<'a> = HashMap<&'a str, &'a ChunkNode>; -use std::sync::LazyLock; - -pub static DEFAULT_FULL_DISPLAY_THRESHOLD: LazyLock = - LazyLock::new(|| read_env_usize("PI_CHUNK_FULL_DISPLAY_THRESHOLD", 80)); -pub static DEFAULT_PREVIEW_HEAD_LINES: LazyLock = - LazyLock::new(|| read_env_usize("PI_CHUNK_PREVIEW_HEAD_LINES", 20)); -pub static DEFAULT_PREVIEW_TAIL_LINES: LazyLock = - LazyLock::new(|| read_env_usize("PI_CHUNK_PREVIEW_TAIL_LINES", 8)); +env_uint! { + // Configured full display threshold. + static FULL_DISPLAY_THRESHOLD: usize = "PI_CHUNK_FULL_DISPLAY_THRESHOLD" or 80 => [1, usize::MAX]; + // Configured preview head lines. + static PREVIEW_HEAD_LINES: usize = "PI_CHUNK_PREVIEW_HEAD_LINES" or 20 => [1, usize::MAX]; + // Configured preview tail lines. + static PREVIEW_TAIL_LINES: usize = "PI_CHUNK_PREVIEW_TAIL_LINES" or 8 => [1, usize::MAX]; +} pub fn line_to_containing_chunk_path(tree: &ChunkTree, line: u32) -> Option { line_to_containing_chunk(tree, line).map(|chunk| chunk.path.clone()) @@ -23,9 +26,9 @@ pub fn render_chunk_tree(params: &RenderChunkTreeParams) -> String { return String::new(); }; let source_lines: Vec<&str> = params.source.split('\n').collect(); - let full_display_threshold = *DEFAULT_FULL_DISPLAY_THRESHOLD; - let preview_head_lines = *DEFAULT_PREVIEW_HEAD_LINES; - let preview_tail_lines = *DEFAULT_PREVIEW_TAIL_LINES; + let full_display_threshold = *FULL_DISPLAY_THRESHOLD; + let preview_head_lines = *PREVIEW_HEAD_LINES; + let preview_tail_lines = *PREVIEW_TAIL_LINES; let tab_replacement = params.tab_replacement.as_deref().unwrap_or(" "); let num_width = compute_num_width( ¶ms.tree, @@ -117,14 +120,6 @@ pub fn render_chunk_tree(params: &RenderChunkTreeParams) -> String { ctx.out } -fn read_env_usize(name: &str, default_value: usize) -> usize { - std::env::var(name) - .ok() - .and_then(|value| value.parse::().ok()) - .filter(|value| *value > 0) - .unwrap_or(default_value) -} - fn build_lookup(tree: &ChunkTree) -> ChunkLookup<'_> { tree .chunks diff --git a/crates/pi-natives/src/fs_cache.rs b/crates/pi-natives/src/fs_cache.rs index 378b40b69..c4442d914 100644 --- a/crates/pi-natives/src/fs_cache.rs +++ b/crates/pi-natives/src/fs_cache.rs @@ -22,7 +22,7 @@ use ignore::WalkBuilder; use napi::bindgen_prelude::*; use napi_derive::napi; -use crate::task; +use crate::{env_uint, task}; // ═══════════════════════════════════════════════════════════════════════════ // Public types (re-exported by glob for backward compatibility) @@ -57,36 +57,25 @@ pub struct GlobMatch { // Cache policy // ═══════════════════════════════════════════════════════════════════════════ -const DEFAULT_CACHE_TTL_MS: u64 = 1_000; -const DEFAULT_EMPTY_RECHECK_MS: u64 = 200; -const DEFAULT_MAX_CACHE_ENTRIES: usize = 16; - -fn env_u64(name: &str, default: u64) -> u64 { - std::env::var(name) - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(default) +env_uint! { + // Configured cache TTL in milliseconds. + static CACHE_TTL_MS: u64 = "FS_SCAN_CACHE_TTL_MS" or 1_000 => [0, u64::MAX]; + // Configured empty-result recheck threshold in milliseconds. + static EMPTY_RECHECK_MS: u64 = "FS_SCAN_EMPTY_RECHECK_MS" or 200 => [0, u64::MAX]; + // Configured maximum number of cache entries. + static MAX_CACHE_ENTRIES: usize = "FS_SCAN_CACHE_MAX_ENTRIES" or 16 => [0, usize::MAX]; } -fn env_usize(name: &str, default: usize) -> usize { - std::env::var(name) - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(default) -} - -/// Configured cache TTL in milliseconds. pub fn cache_ttl_ms() -> u64 { - env_u64("FS_SCAN_CACHE_TTL_MS", DEFAULT_CACHE_TTL_MS) + *CACHE_TTL_MS } -/// Configured empty-result recheck threshold in milliseconds. pub fn empty_recheck_ms() -> u64 { - env_u64("FS_SCAN_EMPTY_RECHECK_MS", DEFAULT_EMPTY_RECHECK_MS) + *EMPTY_RECHECK_MS } -fn max_cache_entries() -> usize { - env_usize("FS_SCAN_CACHE_MAX_ENTRIES", DEFAULT_MAX_CACHE_ENTRIES) +pub fn max_cache_entries() -> usize { + *MAX_CACHE_ENTRIES } // ═══════════════════════════════════════════════════════════════════════════ @@ -117,8 +106,7 @@ pub struct ScanResult { } fn evict_oldest() { - let max = max_cache_entries(); - if FS_CACHE.len() > max + if FS_CACHE.len() > *MAX_CACHE_ENTRIES && let Some(oldest_key) = FS_CACHE .iter() .min_by_key(|entry| entry.value().created_at) @@ -295,7 +283,7 @@ pub fn get_or_scan( use_gitignore: bool, ct: &task::CancelToken, ) -> Result { - let ttl = cache_ttl_ms(); + let ttl = *CACHE_TTL_MS; if ttl == 0 { // Caching disabled – always scan fresh. let entries = collect_entries(root, include_hidden, use_gitignore, ct)?; diff --git a/crates/pi-natives/src/utils.rs b/crates/pi-natives/src/utils.rs index d77787682..b14970fbc 100644 --- a/crates/pi-natives/src/utils.rs +++ b/crates/pi-natives/src/utils.rs @@ -1,4 +1,35 @@ -/// Saturating cast from `u64` to `u32`, clamping at [`u32::MAX`]. -pub fn clamp_u32(value: u64) -> u32 { - value.min(u32::MAX as u64) as u32 +#[macro_export] +macro_rules! env_uint { + // With clamp range: ... => [$min, $max]; + ($( $vis:vis static $name:ident : $type:ty = $env:literal or $default:expr => [$min:expr, $max:expr];)*) => { + $( + $vis static $name: std::sync::LazyLock<$type> = std::sync::LazyLock::new(|| { + std::env::var($env) + .ok() + .and_then(|v| std::str::FromStr::from_str(&v).ok()) + .unwrap_or($default) + .clamp($min, $max) + }); + )* + }; + // Without clamp range: ...; (no => []) + ($( $vis:vis static $name:ident : $type:ty = $env:literal or $default:expr;)*) => { + $( + $vis static $name: std::sync::LazyLock<$type> = std::sync::LazyLock::new(|| { + std::env::var($env) + .ok() + .and_then(|v| std::str::FromStr::from_str(&v).ok()) + .unwrap_or($default) + }); + )* + }; +} + +/// Saturating cast from `u64` to `u32`, clamping at [`u32::MAX`]. +pub const fn clamp_u32(value: u64) -> u32 { + if value > u32::MAX as u64 { + u32::MAX + } else { + value as u32 + } }