Implement client-side file scanning with gitignore support
- Use Node.js fs APIs to recursively scan workspace folder - Load and parse .gitignore to filter files - Cache scanned files for performance - Filter files by search query on client side - Skip .git and node_modules directories - Support gitignore patterns (basic wildcards) - No server API calls needed for file listing
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js"
|
import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js"
|
||||||
|
import * as fs from "fs"
|
||||||
|
import * as path from "path"
|
||||||
|
|
||||||
interface FileItem {
|
interface FileItem {
|
||||||
path: string
|
path: string
|
||||||
@@ -15,75 +17,118 @@ interface FilePickerProps {
|
|||||||
instanceClient: any
|
instanceClient: any
|
||||||
searchQuery: string
|
searchQuery: string
|
||||||
textareaRef?: HTMLTextAreaElement
|
textareaRef?: HTMLTextAreaElement
|
||||||
|
workspaceFolder: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilePicker: Component<FilePickerProps> = (props) => {
|
const FilePicker: Component<FilePickerProps> = (props) => {
|
||||||
const [files, setFiles] = createSignal<FileItem[]>([])
|
const [files, setFiles] = createSignal<FileItem[]>([])
|
||||||
const [selectedIndex, setSelectedIndex] = createSignal(0)
|
const [selectedIndex, setSelectedIndex] = createSignal(0)
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
const [cachedGitFiles, setCachedGitFiles] = createSignal<FileItem[]>([])
|
const [allFiles, setAllFiles] = createSignal<FileItem[]>([])
|
||||||
const [isInitialized, setIsInitialized] = createSignal(false)
|
const [isInitialized, setIsInitialized] = createSignal(false)
|
||||||
|
const [gitignorePatterns, setGitignorePatterns] = createSignal<Set<string>>(new Set())
|
||||||
|
|
||||||
let containerRef: HTMLDivElement | undefined
|
let containerRef: HTMLDivElement | undefined
|
||||||
let gitFilesFetched = false
|
|
||||||
|
|
||||||
async function fetchGitFiles() {
|
|
||||||
if (!props.instanceClient) {
|
|
||||||
console.log("[FilePicker] No instance client available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gitFilesFetched) {
|
|
||||||
console.log("[FilePicker] Git files already fetched")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gitFilesFetched = true
|
|
||||||
console.log("[FilePicker] Fetching git files...")
|
|
||||||
const startTime = Date.now()
|
|
||||||
|
|
||||||
|
async function loadGitignore() {
|
||||||
try {
|
try {
|
||||||
const gitResponse = await props.instanceClient.file.status()
|
const gitignorePath = path.join(props.workspaceFolder, ".gitignore")
|
||||||
const elapsed = Date.now() - startTime
|
if (fs.existsSync(gitignorePath)) {
|
||||||
console.log(`[FilePicker] Git files response received in ${elapsed}ms:`, gitResponse)
|
const content = fs.readFileSync(gitignorePath, "utf-8")
|
||||||
|
const patterns = new Set(
|
||||||
if (gitResponse?.data && gitResponse.data.length > 0) {
|
content
|
||||||
const gitFiles: FileItem[] = gitResponse.data.map((file: any) => ({
|
.split("\n")
|
||||||
path: file.path,
|
.map((line) => line.trim())
|
||||||
added: file.added,
|
.filter((line) => line && !line.startsWith("#")),
|
||||||
removed: file.removed,
|
)
|
||||||
isGitFile: true,
|
setGitignorePatterns(patterns)
|
||||||
}))
|
console.log(`[FilePicker] Loaded ${patterns.size} gitignore patterns`)
|
||||||
console.log(`[FilePicker] Cached ${gitFiles.length} git files`)
|
|
||||||
setCachedGitFiles(gitFiles)
|
|
||||||
} else {
|
|
||||||
console.log("[FilePicker] Git response has no data or empty array")
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const elapsed = Date.now() - startTime
|
console.warn("[FilePicker] Could not load .gitignore:", error)
|
||||||
console.warn(`[FilePicker] Git files not available after ${elapsed}ms:`, error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isIgnored(relativePath: string): boolean {
|
||||||
|
const patterns = gitignorePatterns()
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern.endsWith("/") && relativePath.startsWith(pattern)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (relativePath === pattern || relativePath.startsWith(pattern + "/")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (pattern.includes("*")) {
|
||||||
|
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$")
|
||||||
|
if (regex.test(relativePath)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanDirectory(dirPath: string, baseDir: string): Promise<FileItem[]> {
|
||||||
|
const results: FileItem[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dirPath, entry.name)
|
||||||
|
const relativePath = path.relative(baseDir, fullPath)
|
||||||
|
|
||||||
|
if (entry.name === ".git" || entry.name === "node_modules") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIgnored(relativePath)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
results.push({
|
||||||
|
path: relativePath + "/",
|
||||||
|
isGitFile: false,
|
||||||
|
})
|
||||||
|
const subFiles = await scanDirectory(fullPath, baseDir)
|
||||||
|
results.push(...subFiles)
|
||||||
|
} else {
|
||||||
|
results.push({
|
||||||
|
path: relativePath,
|
||||||
|
isGitFile: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[FilePicker] Error scanning ${dirPath}:`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchFiles(searchQuery: string) {
|
async function fetchFiles(searchQuery: string) {
|
||||||
console.log(`[FilePicker] Fetching files for query: "${searchQuery}"`)
|
console.log(`[FilePicker] Fetching files for query: "${searchQuery}"`)
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const startTime = Date.now()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gitFiles = cachedGitFiles()
|
if (allFiles().length === 0) {
|
||||||
console.log(`[FilePicker] Using ${gitFiles.length} cached git files`)
|
await loadGitignore()
|
||||||
|
console.log(`[FilePicker] Scanning workspace: ${props.workspaceFolder}`)
|
||||||
|
const scannedFiles = await scanDirectory(props.workspaceFolder, props.workspaceFolder)
|
||||||
|
setAllFiles(scannedFiles)
|
||||||
|
console.log(`[FilePicker] Found ${scannedFiles.length} files`)
|
||||||
|
}
|
||||||
|
|
||||||
const filteredGitFiles = searchQuery.trim()
|
const filteredFiles = searchQuery.trim()
|
||||||
? gitFiles.filter((f) => f.path.toLowerCase().includes(searchQuery.toLowerCase()))
|
? allFiles().filter((f) => f.path.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
: gitFiles
|
: allFiles()
|
||||||
|
|
||||||
console.log(`[FilePicker] Showing ${filteredGitFiles.length} git files`)
|
console.log(`[FilePicker] Showing ${filteredFiles.length} files`)
|
||||||
setFiles(filteredGitFiles)
|
setFiles(filteredFiles)
|
||||||
setSelectedIndex(0)
|
setSelectedIndex(0)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const elapsed = Date.now() - startTime
|
console.error(`[FilePicker] Failed to fetch files:`, error)
|
||||||
console.error(`[FilePicker] Failed to search files after ${elapsed}ms:`, error)
|
|
||||||
setFiles([])
|
setFiles([])
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -94,15 +139,13 @@ const FilePicker: Component<FilePickerProps> = (props) => {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log(
|
console.log(
|
||||||
`[FilePicker] Effect triggered - open: ${props.open}, query: "${props.searchQuery}", gitFilesFetched: ${gitFilesFetched}, isInitialized: ${isInitialized()}`,
|
`[FilePicker] Effect triggered - open: ${props.open}, query: "${props.searchQuery}", isInitialized: ${isInitialized()}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (props.open && !isInitialized()) {
|
if (props.open && !isInitialized()) {
|
||||||
setIsInitialized(true)
|
setIsInitialized(true)
|
||||||
console.log("[FilePicker] First open - fetching git files and initial files")
|
console.log("[FilePicker] First open - fetching files")
|
||||||
fetchGitFiles().then(() => {
|
fetchFiles(props.searchQuery)
|
||||||
fetchFiles(props.searchQuery)
|
|
||||||
})
|
|
||||||
lastQuery = props.searchQuery
|
lastQuery = props.searchQuery
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -450,6 +450,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
instanceClient={instance()!.client}
|
instanceClient={instance()!.client}
|
||||||
searchQuery={fileSearchQuery()}
|
searchQuery={fileSearchQuery()}
|
||||||
textareaRef={textareaRef}
|
textareaRef={textareaRef}
|
||||||
|
workspaceFolder={props.instanceFolder}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user