From 27295901e1a6e2440073b31dc61a6d94f4c1cc38 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Fri, 4 Sep 2026 22:09:33 +0900 Subject: [PATCH] fix(utils): normalize path separators in resolve_chunk (semble #244) find_related compared the incoming file_path against each chunk's stored path verbatim. Chunk paths use the platform-native separator, so on Windows a path passed with the other separator failed to resolve. Compare with backslash and forward slash treated as equal on both sides, keeping the strict-inner-match / end-line-fallback behavior. Closes #88 --- .please/docs/references/semble.md | 3 ++- crates/csp/src/utils.rs | 41 +++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.please/docs/references/semble.md b/.please/docs/references/semble.md index 3c1fcc1..3c860ca 100644 --- a/.please/docs/references/semble.md +++ b/.please/docs/references/semble.md @@ -347,7 +347,8 @@ Clean two-layer split: ### 4.18 `utils.rs` — helpers - `is_git_url` (scheme prefixes + scp-style), `resolve_chunk(chunks, file_path, line) -> - Option<&Chunk>` (interior match preferred, boundary fallback), `result_to_dict` / + Option<&Chunk>` (interior match preferred, boundary fallback; `\\`/`/` separators compared + as equal on both sides — upstream #244), `result_to_dict` / `format_results` (snake_case wire dict). Model name resolution honors the env override. --- diff --git a/crates/csp/src/utils.rs b/crates/csp/src/utils.rs index a32641a..540387a 100644 --- a/crates/csp/src/utils.rs +++ b/crates/csp/src/utils.rs @@ -130,11 +130,16 @@ fn is_scp_git_url(path: &str) -> bool { /// /// A strict inner match (`line < end_line`) wins immediately; a boundary match /// (`line == end_line`) is kept only as a fallback so end-of-file lines still -/// resolve. Mirrors `semble.utils.resolve_chunk`. +/// resolve. Path separators (`\\` and `/`) are treated as equal on both sides, +/// since chunk paths are stored with the platform-native separator and callers +/// may pass either form (upstream semble #244). Mirrors `semble.utils.resolve_chunk`. pub fn resolve_chunk<'a>(chunks: &'a [Chunk], file_path: &str, line: u32) -> Option<&'a Chunk> { let mut fallback: Option<&Chunk> = None; for chunk in chunks { - if chunk.file_path == file_path && chunk.start_line <= line && line <= chunk.end_line { + if paths_eq_normalized(&chunk.file_path, file_path) + && chunk.start_line <= line + && line <= chunk.end_line + { if line < chunk.end_line { return Some(chunk); } @@ -146,6 +151,18 @@ pub fn resolve_chunk<'a>(chunks: &'a [Chunk], file_path: &str, line: u32) -> Opt fallback } +/// Compare two paths treating `\\` and `/` as the same separator, without +/// allocating. Equivalent to upstream's `a.replace("\\", "/") == b.replace("\\", "/")`. +fn paths_eq_normalized(a: &str, b: &str) -> bool { + fn is_sep(c: u8) -> bool { + c == b'/' || c == b'\\' + } + a.len() == b.len() + && a.bytes() + .zip(b.bytes()) + .all(|(x, y)| x == y || (is_sep(x) && is_sep(y))) +} + #[cfg(test)] mod tests { use super::*; @@ -280,4 +297,24 @@ mod tests { assert_eq!(resolve_chunk(&chunks, "b.ts", 3), None); assert_eq!(resolve_chunk(&chunks, "a.ts", 99), None); } + + #[test] + fn resolve_chunk_backslash_input_matches_forward_slash_chunk() { + // Windows-style input against a chunk stored with `/` (semble #244). + let chunks = [chunk("src/lib/a.ts", 1, 10)]; + assert_eq!( + resolve_chunk(&chunks, "src\\lib\\a.ts", 5), + Some(&chunks[0]) + ); + } + + #[test] + fn resolve_chunk_forward_slash_input_matches_backslash_chunk() { + // Forward-slash input (e.g. copied from docs) against a chunk stored + // with the Windows-native `\` separator; boundary fallback still applies. + let chunks = [chunk("src\\lib\\a.ts", 1, 5)]; + assert_eq!(resolve_chunk(&chunks, "src/lib/a.ts", 3), Some(&chunks[0])); + assert_eq!(resolve_chunk(&chunks, "src/lib/a.ts", 5), Some(&chunks[0])); + assert_eq!(resolve_chunk(&chunks, "src/lib/b.ts", 3), None); + } }