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
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ reflex
*.jpeg
*.gif


*.snap
# Insta pending-snapshot files — committed `.snap` files are the baseline
*.snap.new
*.pending-snap
2 changes: 1 addition & 1 deletion src/cli/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ pub(super) fn handle_index_build(

std::process::Command::new(&current_exe)
.arg("index-symbols-internal")
.arg(&path)
.arg(path)
.creation_flags(CREATE_NO_WINDOW)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
Expand Down
9 changes: 5 additions & 4 deletions src/dependency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,11 +1216,12 @@ pub fn resolve_rust_import(
}
}

// Convert to string and make relative to project root
// Convert to string and make relative to project root.
// Normalize to forward slashes so paths are deterministic across platforms.
resolved_path.and_then(|p| {
p.strip_prefix(project_root)
.ok()
.map(|rel| rel.to_string_lossy().to_string())
.map(|rel| rel.to_string_lossy().replace('\\', "/"))
})
}

Expand Down Expand Up @@ -1290,13 +1291,13 @@ pub fn resolve_rust_mod_declaration(
// Try sibling file
let sibling = current_dir.join(format!("{}.rs", mod_name));
if sibling.exists() {
return Some(sibling.to_string_lossy().to_string());
return Some(sibling.to_string_lossy().replace('\\', "/"));
}

// Try directory module
let dir_mod = current_dir.join(mod_name).join("mod.rs");
if dir_mod.exists() {
return Some(dir_mod.to_string_lossy().to_string());
return Some(dir_mod.to_string_lossy().replace('\\', "/"));
}

None
Expand Down
39 changes: 25 additions & 14 deletions src/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::cache::CacheManager;
use crate::content_store::{ContentReader, ContentWriter};
use crate::dependency::DependencyIndex;
use crate::models::{Dependency, ImportType, IndexConfig, IndexStats, Language};
#[cfg(unix)]
use crate::output;
use crate::parsers::c::CDependencyExtractor;
use crate::parsers::cpp::CppDependencyExtractor;
Expand All @@ -41,7 +42,6 @@ pub type ProgressCallback = Arc<dyn Fn(usize, usize, String) + Send + Sync>;

/// Result of processing a single file (used for parallel processing)
struct FileProcessingResult {
path: PathBuf,
path_str: String,
hash: String,
content: String,
Expand Down Expand Up @@ -202,14 +202,17 @@ impl Indexer {
let mut any_changed = false;

for file_path in &files {
// Normalize path to be relative to root (handles both ./ prefix and absolute paths)
// Normalize path to be relative to root (handles both ./ prefix and absolute paths).
// Always use forward slashes so the on-disk index is deterministic across OSes
// and downstream string lookups (file_pattern filters, dependency resolvers)
// work regardless of the host separator.
let path_str = file_path.to_string_lossy().to_string();
let normalized_path = if let Ok(rel_path) = file_path.strip_prefix(root) {
// Convert absolute path to relative
rel_path.to_string_lossy().to_string()
rel_path.to_string_lossy().replace('\\', "/")
} else {
// Already relative, just strip ./ prefix
path_str.trim_start_matches("./").to_string()
path_str.trim_start_matches("./").replace('\\', "/")
};

// Check if file exists in cache
Expand Down Expand Up @@ -404,14 +407,15 @@ impl Indexer {
batch_files
.par_iter()
.map(|file_path| {
// Normalize path to be relative to root (handles both ./ prefix and absolute paths)
// Normalize path to be relative to root (handles both ./ prefix and absolute paths).
// Always emit forward slashes so the persisted path is deterministic across OSes.
let path_str = file_path.to_string_lossy().to_string();
let normalized_path = if let Ok(rel_path) = file_path.strip_prefix(root) {
// Convert absolute path to relative
rel_path.to_string_lossy().to_string()
rel_path.to_string_lossy().replace('\\', "/")
} else {
// Already relative, just strip ./ prefix
path_str.trim_start_matches("./").to_string()
path_str.trim_start_matches("./").replace('\\', "/")
};

// Read file content once (used for hashing, trigrams, and parsing)
Expand Down Expand Up @@ -605,7 +609,6 @@ impl Indexer {
counter_clone.fetch_add(1, Ordering::Relaxed);

Some(FileProcessingResult {
path: file_path.clone(),
path_str: normalized_path.to_string(),
hash,
content,
Expand All @@ -620,14 +623,19 @@ impl Indexer {

// Process batch results immediately (streaming approach to minimize memory)
for result in results.into_iter().flatten() {
// Use the normalized (forward-slash, relative) path everywhere so
// the trigram index and content store agree with what the database
// and downstream filters expect, regardless of host separator.
let normalized_pathbuf = PathBuf::from(&result.path_str);

// Add file to trigram index (get file_id)
let file_id = trigram_index.add_file(result.path.clone());
let file_id = trigram_index.add_file(normalized_pathbuf.clone());

// Index file content directly (avoid accumulating all trigrams)
trigram_index.index_file(file_id, &result.content);

// Add to content store
content_writer.add_file(result.path.clone(), &result.content);
content_writer.add_file(normalized_pathbuf, &result.content);

files_indexed += 1;

Expand Down Expand Up @@ -1206,10 +1214,11 @@ impl Indexer {
let normalized_candidate = if let Ok(rel_path) =
std::path::Path::new(candidate_path).strip_prefix(root)
{
rel_path.to_string_lossy().to_string()
rel_path.to_string_lossy().replace('\\', "/")
} else {
// Not an absolute path or not under root - use as-is
candidate_path.to_string()
// (still normalize separators so DB lookups match).
candidate_path.replace('\\', "/")
};

log::debug!(
Expand Down Expand Up @@ -1645,10 +1654,11 @@ impl Indexer {
let normalized_candidate = if let Ok(rel_path) =
std::path::Path::new(candidate_path).strip_prefix(root)
{
rel_path.to_string_lossy().to_string()
rel_path.to_string_lossy().replace('\\', "/")
} else {
// Not an absolute path or not under root - use as-is
candidate_path.to_string()
// (still normalize separators so DB lookups match).
candidate_path.replace('\\', "/")
};

match dep_index.get_file_id_by_path(&normalized_candidate) {
Expand Down Expand Up @@ -2039,6 +2049,7 @@ impl Indexer {
///
/// Ensures there's enough free space to create the index. Warns if disk space is low.
/// This prevents partial index writes and confusing error messages.
#[cfg_attr(not(unix), allow(unused_variables))]
fn check_disk_space(&self, root: &Path) -> Result<()> {
// Get available space on the filesystem containing the cache directory
let cache_path = self.cache.path();
Expand Down
11 changes: 6 additions & 5 deletions src/parsers/c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,12 +747,13 @@ pub fn resolve_c_include_to_path(
// Resolve the include path relative to current file
let resolved = current_dir.join(include_path);

// Normalize the path
// Normalize the path. Always emit forward slashes so resolved paths are
// deterministic across platforms.
match resolved.canonicalize() {
Ok(normalized) => Some(normalized.display().to_string()),
Ok(normalized) => Some(normalized.to_string_lossy().replace('\\', "/")),
Err(_) => {
// If canonicalize fails (file doesn't exist yet), return the joined path
Some(resolved.display().to_string())
Some(resolved.to_string_lossy().replace('\\', "/"))
}
}
}
Expand All @@ -771,7 +772,7 @@ mod resolution_tests {

assert!(result.is_some());
let path = result.unwrap();
assert!(path.ends_with("src/helper.h") || path.ends_with("src\\helper.h"));
assert!(path.ends_with("src/helper.h"));
}

#[test]
Expand All @@ -780,7 +781,7 @@ mod resolution_tests {

assert!(result.is_some());
let path = result.unwrap();
assert!(path.ends_with("src/utils/helper.h") || path.ends_with("src\\utils\\helper.h"));
assert!(path.ends_with("src/utils/helper.h"));
}

#[test]
Expand Down
13 changes: 7 additions & 6 deletions src/parsers/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1228,12 +1228,13 @@ pub fn resolve_cpp_include_to_path(
// Resolve the include path relative to current file
let resolved = current_dir.join(include_path);

// Normalize the path
// Normalize the path. Always emit forward slashes so resolved paths are
// deterministic across platforms.
match resolved.canonicalize() {
Ok(normalized) => Some(normalized.display().to_string()),
Ok(normalized) => Some(normalized.to_string_lossy().replace('\\', "/")),
Err(_) => {
// If canonicalize fails (file doesn't exist yet), return the joined path
Some(resolved.display().to_string())
Some(resolved.to_string_lossy().replace('\\', "/"))
}
}
}
Expand All @@ -1252,7 +1253,7 @@ mod resolution_tests {

assert!(result.is_some());
let path = result.unwrap();
assert!(path.ends_with("src/helper.hpp") || path.ends_with("src\\helper.hpp"));
assert!(path.ends_with("src/helper.hpp"));
}

#[test]
Expand All @@ -1261,7 +1262,7 @@ mod resolution_tests {

assert!(result.is_some());
let path = result.unwrap();
assert!(path.ends_with("src/utils/helper.hpp") || path.ends_with("src\\utils\\helper.hpp"));
assert!(path.ends_with("src/utils/helper.hpp"));
}

#[test]
Expand All @@ -1280,7 +1281,7 @@ mod resolution_tests {

assert!(result.is_some());
let path = result.unwrap();
assert!(path.ends_with("src/legacy.h") || path.ends_with("src\\legacy.h"));
assert!(path.ends_with("src/legacy.h"));
}

#[test]
Expand Down
9 changes: 6 additions & 3 deletions src/parsers/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1285,8 +1285,9 @@ pub fn find_all_go_mods(index_root: &std::path::Path) -> Result<Vec<std::path::P

// Look for go.mod files
if filename == "go.mod" {
// Skip vendor directories
let path_str = path.to_string_lossy();
// Skip vendor directories. Normalize separators so the check
// works on Windows (`\vendor\`) as well as Unix (`/vendor/`).
let path_str = path.to_string_lossy().replace('\\', "/");
if path_str.contains("/vendor/") {
log::trace!("Skipping go.mod in vendor directory: {:?}", path);
continue;
Expand Down Expand Up @@ -1328,11 +1329,13 @@ pub fn parse_all_go_modules(index_root: &std::path::Path) -> Result<Vec<GoModule
.trim()
.to_string();

// Normalize to forward slashes so import resolution and
// assertions on project_root behave the same on every OS.
let relative_project_root = project_root
.strip_prefix(index_root)
.unwrap_or(project_root)
.to_string_lossy()
.to_string();
.replace('\\', "/");

log::debug!(
"Found Go module '{}' at {:?}",
Expand Down
10 changes: 7 additions & 3 deletions src/parsers/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1026,8 +1026,9 @@ pub fn find_all_python_configs(index_root: &std::path::Path) -> Result<Vec<std::

// Look for Python config files
if filename == "pyproject.toml" || filename == "setup.py" || filename == "setup.cfg" {
// Skip virtual environments and build directories
let path_str = path.to_string_lossy();
// Skip virtual environments and build directories. Normalize
// separators so these filters fire on Windows (`\venv\`) too.
let path_str = path.to_string_lossy().replace('\\', "/");
if path_str.contains("/venv/")
|| path_str.contains("/.venv/")
|| path_str.contains("/site-packages/")
Expand Down Expand Up @@ -1069,11 +1070,14 @@ pub fn parse_all_python_packages(index_root: &std::path::Path) -> Result<Vec<Pyt

// Try to extract package name from this config
if let Some(package_name) = find_python_package_name(project_root) {
// Normalize to forward slashes so `starts_with("services/")`
// style assertions and downstream import resolution behave the
// same on every OS.
let relative_project_root = project_root
.strip_prefix(index_root)
.unwrap_or(project_root)
.to_string_lossy()
.to_string();
.replace('\\', "/");

log::debug!(
"Found Python package '{}' at {:?}",
Expand Down
8 changes: 6 additions & 2 deletions src/parsers/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1496,7 +1496,8 @@ pub fn resolve_ts_import_to_path(
log::debug!(" Alias matched! {} => {}", import_path, resolved_alias);
// Alias matched! Now resolve relative to the tsconfig directory
let resolved_path = map.resolve_relative_to_config(&resolved_alias);
let path_str = resolved_path.to_string_lossy().to_string();
// Normalize to forward slashes for deterministic, cross-platform output.
let path_str = resolved_path.to_string_lossy().replace('\\', "/");
log::debug!(" After resolve_relative_to_config: {}", path_str);

// Check if resolved path has an extension
Expand Down Expand Up @@ -1576,7 +1577,10 @@ pub fn resolve_ts_import_to_path(
},
);

let normalized = normalized_path.to_string_lossy().to_string();
// Normalize to forward slashes so the resolved path is deterministic
// across platforms. `join`/`components` on Windows produces backslashes
// which would otherwise break downstream string lookups.
let normalized = normalized_path.to_string_lossy().replace('\\', "/");

// Check if the import already has a known extension
// Vue/Svelte files are imported with their extension: import Foo from './Foo.vue'
Expand Down
15 changes: 11 additions & 4 deletions src/pulse/pagefind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,17 @@ mod tests {
#[test]
fn test_get_asset_name() {
let result = get_asset_name();
assert!(result.is_ok(), "Should detect platform: {:?}", result.err());
let name = result.unwrap();
assert!(name.contains(PAGEFIND_VERSION));
assert!(name.ends_with(".tar.gz"));
if cfg!(any(target_os = "linux", target_os = "macos")) {
assert!(result.is_ok(), "Should detect platform: {:?}", result.err());
let name = result.unwrap();
assert!(name.contains(PAGEFIND_VERSION));
assert!(name.ends_with(".tar.gz"));
} else {
// Pagefind has no Windows release asset; the helper should
// explicitly report the platform as unsupported.
let err = result.expect_err("expected unsupported platform error");
assert!(err.to_string().contains("Unsupported platform"));
}
}

#[test]
Expand Down
23 changes: 19 additions & 4 deletions src/pulse/zola.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,25 @@ mod tests {
#[test]
fn test_get_asset_name() {
let result = get_asset_name();
assert!(result.is_ok(), "Should detect platform: {:?}", result.err());
let name = result.unwrap();
assert!(name.contains(ZOLA_VERSION));
assert!(name.ends_with(".tar.gz"));
// Zola only ships binaries for linux-x86_64 and both macOS arches.
// On other platforms the helper should explicitly bail.
let supported = matches!(
(std::env::consts::OS, std::env::consts::ARCH),
("linux", "x86_64") | ("macos", "x86_64") | ("macos", "aarch64")
);
if supported {
assert!(result.is_ok(), "Should detect platform: {:?}", result.err());
let name = result.unwrap();
assert!(name.contains(ZOLA_VERSION));
assert!(name.ends_with(".tar.gz"));
} else {
let err = result.expect_err("expected unsupported-platform error");
let msg = err.to_string();
assert!(
msg.contains("Unsupported platform") || msg.contains("does not have"),
"unexpected error: {msg}"
);
}
}

#[test]
Expand Down
Loading
Loading