mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: improve file search to handle filenames with spaces
- Enhanced searchWorkspaceFiles function to create multiple search string variations - Files with spaces now appear in autocomplete without needing to be opened first - Added comprehensive unit tests for the fix Fixes #7272
This commit is contained in:
parent
6fd261d3b6
commit
2967fbe594
4 changed files with 353 additions and 4 deletions
272
src/services/search/__tests__/file-search.spec.ts
Normal file
272
src/services/search/__tests__/file-search.spec.ts
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { searchWorkspaceFiles } from "../file-search"
|
||||
|
||||
// Mock child_process module
|
||||
vi.mock("child_process", () => ({
|
||||
spawn: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock readline module
|
||||
vi.mock("readline", () => ({
|
||||
createInterface: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock the getBinPath function
|
||||
vi.mock("../../ripgrep", () => ({
|
||||
getBinPath: vi.fn().mockResolvedValue("/mock/path/to/rg"),
|
||||
}))
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("vscode", () => ({
|
||||
env: {
|
||||
appRoot: "/mock/app/root",
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock fs module
|
||||
vi.mock("fs", () => ({
|
||||
existsSync: vi.fn(),
|
||||
lstatSync: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("searchWorkspaceFiles", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should find files with spaces when searching with partial name without spaces", async () => {
|
||||
const childProcess = await import("child_process")
|
||||
const readline = await import("readline")
|
||||
const fs = await import("fs")
|
||||
|
||||
// Mock ripgrep output with files containing spaces
|
||||
const mockFiles = [
|
||||
"/workspace/test file with spaces.md",
|
||||
"/workspace/another test file.ts",
|
||||
"/workspace/normalfile.js",
|
||||
"/workspace/test-no-spaces.md",
|
||||
]
|
||||
|
||||
// Mock child_process.spawn
|
||||
const mockStdout = {
|
||||
on: vi.fn(),
|
||||
pipe: vi.fn(),
|
||||
}
|
||||
const mockStderr = {
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "data") {
|
||||
// No error output
|
||||
}
|
||||
}),
|
||||
}
|
||||
const mockProcess = {
|
||||
stdout: mockStdout,
|
||||
stderr: mockStderr,
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "error") {
|
||||
// No error
|
||||
}
|
||||
}),
|
||||
kill: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any)
|
||||
|
||||
// Mock readline interface
|
||||
const mockReadline = {
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "line") {
|
||||
// Simulate ripgrep outputting file paths
|
||||
mockFiles.forEach((file) => callback(file))
|
||||
}
|
||||
if (event === "close") {
|
||||
// Simulate process closing
|
||||
setTimeout(() => callback(), 0)
|
||||
}
|
||||
}),
|
||||
close: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(readline.createInterface).mockReturnValue(mockReadline as any)
|
||||
|
||||
// Mock fs functions
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
vi.mocked(fs.lstatSync).mockReturnValue({
|
||||
isDirectory: () => false,
|
||||
} as any)
|
||||
|
||||
// Test searching for "testfile" (without spaces) should find "test file with spaces.md"
|
||||
const results = await searchWorkspaceFiles("testfile", "/workspace", 20)
|
||||
|
||||
// The results should include files with spaces that match the query
|
||||
const fileNames = results.map((r) => r.path)
|
||||
|
||||
// "test file with spaces.md" should be found when searching for "testfile"
|
||||
expect(fileNames).toContain("test file with spaces.md")
|
||||
expect(fileNames).toContain("another test file.ts")
|
||||
})
|
||||
|
||||
it("should find files when searching with exact name including spaces", async () => {
|
||||
const childProcess = await import("child_process")
|
||||
const readline = await import("readline")
|
||||
const fs = await import("fs")
|
||||
|
||||
// Mock ripgrep output
|
||||
const mockFiles = [
|
||||
"/workspace/test file with spaces.md",
|
||||
"/workspace/another test file.ts",
|
||||
"/workspace/normalfile.js",
|
||||
]
|
||||
|
||||
const mockStdout = {
|
||||
on: vi.fn(),
|
||||
pipe: vi.fn(),
|
||||
}
|
||||
const mockStderr = {
|
||||
on: vi.fn(),
|
||||
}
|
||||
const mockProcess = {
|
||||
stdout: mockStdout,
|
||||
stderr: mockStderr,
|
||||
on: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any)
|
||||
|
||||
const mockReadline = {
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "line") {
|
||||
mockFiles.forEach((file) => callback(file))
|
||||
}
|
||||
if (event === "close") {
|
||||
setTimeout(() => callback(), 0)
|
||||
}
|
||||
}),
|
||||
close: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(readline.createInterface).mockReturnValue(mockReadline as any)
|
||||
|
||||
// Mock fs functions
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
vi.mocked(fs.lstatSync).mockReturnValue({
|
||||
isDirectory: () => false,
|
||||
} as any)
|
||||
|
||||
// Test searching for "test file" (with space) should find matching files
|
||||
const results = await searchWorkspaceFiles("test file", "/workspace", 20)
|
||||
|
||||
const fileNames = results.map((r) => r.path)
|
||||
expect(fileNames).toContain("test file with spaces.md")
|
||||
expect(fileNames).toContain("another test file.ts")
|
||||
})
|
||||
|
||||
it("should find files when searching with partial words", async () => {
|
||||
const childProcess = await import("child_process")
|
||||
const readline = await import("readline")
|
||||
const fs = await import("fs")
|
||||
|
||||
// Mock ripgrep output
|
||||
const mockFiles = [
|
||||
"/workspace/test file with spaces.md",
|
||||
"/workspace/documentation file.md",
|
||||
"/workspace/config.json",
|
||||
]
|
||||
|
||||
const mockStdout = {
|
||||
on: vi.fn(),
|
||||
pipe: vi.fn(),
|
||||
}
|
||||
const mockStderr = {
|
||||
on: vi.fn(),
|
||||
}
|
||||
const mockProcess = {
|
||||
stdout: mockStdout,
|
||||
stderr: mockStderr,
|
||||
on: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any)
|
||||
|
||||
const mockReadline = {
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "line") {
|
||||
mockFiles.forEach((file) => callback(file))
|
||||
}
|
||||
if (event === "close") {
|
||||
setTimeout(() => callback(), 0)
|
||||
}
|
||||
}),
|
||||
close: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(readline.createInterface).mockReturnValue(mockReadline as any)
|
||||
|
||||
// Mock fs functions
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
vi.mocked(fs.lstatSync).mockReturnValue({
|
||||
isDirectory: () => false,
|
||||
} as any)
|
||||
|
||||
// Test searching for just "test" should find files with "test" in the name
|
||||
const results = await searchWorkspaceFiles("test", "/workspace", 20)
|
||||
|
||||
const fileNames = results.map((r) => r.path)
|
||||
expect(fileNames).toContain("test file with spaces.md")
|
||||
|
||||
// Should not contain files without "test" in the name
|
||||
expect(fileNames).not.toContain("config.json")
|
||||
})
|
||||
|
||||
it("should return all items when query is empty", async () => {
|
||||
const childProcess = await import("child_process")
|
||||
const readline = await import("readline")
|
||||
const fs = await import("fs")
|
||||
|
||||
const mockFiles = ["/workspace/file1.ts", "/workspace/file2.js", "/workspace/file3.md"]
|
||||
|
||||
const mockStdout = {
|
||||
on: vi.fn(),
|
||||
pipe: vi.fn(),
|
||||
}
|
||||
const mockStderr = {
|
||||
on: vi.fn(),
|
||||
}
|
||||
const mockProcess = {
|
||||
stdout: mockStdout,
|
||||
stderr: mockStderr,
|
||||
on: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any)
|
||||
|
||||
const mockReadline = {
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "line") {
|
||||
mockFiles.forEach((file) => callback(file))
|
||||
}
|
||||
if (event === "close") {
|
||||
setTimeout(() => callback(), 0)
|
||||
}
|
||||
}),
|
||||
close: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mocked(readline.createInterface).mockReturnValue(mockReadline as any)
|
||||
|
||||
// Mock fs functions
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
vi.mocked(fs.lstatSync).mockReturnValue({
|
||||
isDirectory: () => false,
|
||||
} as any)
|
||||
|
||||
// Test with empty query
|
||||
const results = await searchWorkspaceFiles("", "/workspace", 2)
|
||||
|
||||
// Should return limited number of results
|
||||
expect(results.length).toBeLessThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
|
|
@ -122,10 +122,31 @@ export async function searchWorkspaceFiles(
|
|||
}
|
||||
|
||||
// Create search items for all files AND directories
|
||||
const searchItems = allItems.map((item) => ({
|
||||
original: item,
|
||||
searchStr: `${item.path} ${item.label || ""}`,
|
||||
}))
|
||||
// For better matching of files with spaces, we create multiple search variations:
|
||||
// 1. The original path as-is
|
||||
// 2. The path with spaces removed (for matching when user types without spaces)
|
||||
// 3. The label/basename with and without spaces
|
||||
const searchItems = allItems.map((item) => {
|
||||
const pathWithoutSpaces = item.path.replace(/\s+/g, "")
|
||||
const labelWithoutSpaces = (item.label || "").replace(/\s+/g, "")
|
||||
|
||||
// Create a search string that includes multiple variations to improve matching
|
||||
// This allows "testfile" to match "test file with spaces.md"
|
||||
const searchStr = [
|
||||
item.path,
|
||||
pathWithoutSpaces,
|
||||
item.label || "",
|
||||
labelWithoutSpaces,
|
||||
// Also include individual words from the path for better partial matching
|
||||
...item.path.split(/[\s\-_\.\/\\]+/).filter(Boolean),
|
||||
...(item.label || "").split(/[\s\-_\.]+/).filter(Boolean),
|
||||
].join(" ")
|
||||
|
||||
return {
|
||||
original: item,
|
||||
searchStr,
|
||||
}
|
||||
})
|
||||
|
||||
// Run fzf search on all items
|
||||
const fzf = new Fzf(searchItems, {
|
||||
|
|
|
|||
13
test file with spaces.md
Normal file
13
test file with spaces.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Test File with Spaces
|
||||
|
||||
This is a test file to reproduce the issue where files with spaces in their names don't appear in the @ autocomplete suggestions unless they are opened in VS Code first.
|
||||
|
||||
## Issue Details
|
||||
|
||||
- Files with spaces should appear in autocomplete
|
||||
- Currently they only appear after being opened in a tab
|
||||
- This is a regression from previous fixes
|
||||
|
||||
## Test Content
|
||||
|
||||
This file should be discoverable when typing `@test` in the Roo Code chat input.
|
||||
43
test-file-search.js
Normal file
43
test-file-search.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
const { searchWorkspaceFiles } = require("./src/services/search/file-search")
|
||||
const path = require("path")
|
||||
|
||||
async function testFileSearch() {
|
||||
console.log("Testing file search with spaces in filenames...\n")
|
||||
|
||||
const testQueries = [
|
||||
"testfile", // Should match "test file with spaces.md"
|
||||
"test file", // Should match "test file with spaces.md"
|
||||
"spaces", // Should match "test file with spaces.md"
|
||||
"withspaces", // Should match "test file with spaces.md"
|
||||
]
|
||||
|
||||
const cwd = process.cwd()
|
||||
|
||||
for (const query of testQueries) {
|
||||
console.log(`\nSearching for: "${query}"`)
|
||||
console.log("-".repeat(40))
|
||||
|
||||
try {
|
||||
const results = await searchWorkspaceFiles(cwd, query)
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log("No results found")
|
||||
} else {
|
||||
console.log(`Found ${results.length} result(s):`)
|
||||
results.forEach((result) => {
|
||||
console.log(` - ${result.path}`)
|
||||
if (result.label && result.label !== path.basename(result.path)) {
|
||||
console.log(` Label: ${result.label}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n" + "=".repeat(40))
|
||||
console.log("Test completed!")
|
||||
}
|
||||
|
||||
testFileSearch().catch(console.error)
|
||||
Loading…
Add table
Reference in a new issue