Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 9 additions & 2 deletions packages/core/src/codewhispererChat/tools/listDirectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import path from 'path'

export interface ListDirectoryParams {
path: string
maxDepth: number
}

export class ListDirectory {
private fsPath: string
private maxDepth: number
private readonly logger = getLogger('listDirectory')

constructor(params: ListDirectoryParams) {
this.fsPath = params.path
this.maxDepth = params.maxDepth
}

public async validate(): Promise<void> {
Expand All @@ -44,14 +47,18 @@ export class ListDirectory {

public queueDescription(updates: Writable): void {
const fileName = path.basename(this.fsPath)
updates.write(`Listing directory: ${fileName}`)
if (this.maxDepth === -1) {
updates.write(`Listing directory recursively: ${fileName}`)
} else {
updates.write(`Listing directory: ${fileName} with a depth of ${this.maxDepth}`)
}
updates.end()
}

public async invoke(updates?: Writable): Promise<InvokeOutput> {
try {
const fileUri = vscode.Uri.file(this.fsPath)
const listing = await readDirectoryRecursively(fileUri, 0)
const listing = await readDirectoryRecursively(fileUri, this.maxDepth)
return this.createOutput(listing.join('\n'))
} catch (error: any) {
this.logger.error(`Failed to list directory "${this.fsPath}": ${error.message || error}`)
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/codewhispererChat/tools/tool_index.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"fsRead": {
"name": "fsRead",
"description": "A tool for reading a file. \n* This tool returns the contents of a file, and the optional `readRange` determines what range of lines will be read from the specified file.",
"description": "A tool for reading a file.\n * This tool returns the contents of a file, and the optional `readRange` determines what range of lines will be read from the specified file.",
"inputSchema": {
"type": "object",
"properties": {
Expand All @@ -10,7 +10,7 @@
"type": "string"
},
"readRange": {
"description": "Optional parameter when reading files.\n* If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.",
"description": "Optional parameter when reading files.\n * If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.",
"items": {
"type": "integer"
},
Expand Down Expand Up @@ -75,7 +75,7 @@
},
"listDirectory": {
"name": "listDirectory",
"description": "List the contents of a directory.\n * Use this tool for discovery, before using more targeted tools like fsRead.\n *Useful to try to understand the file structure before diving deeper into specific files.\n *Can be used to explore the codebase.\n *Results clearly distinguish between files, directories or symlinks with [FILE], [DIR] and [LINK] prefixes.",
"description": "List the contents of a directory and its subdirectories.\n * Use this tool for discovery, before using more targeted tools like fsRead.\n *Useful to try to understand the file structure before diving deeper into specific files.\n *Can be used to explore the codebase.\n *Results clearly distinguish between files, directories or symlinks with [FILE], [DIR] and [LINK] prefixes.",
"inputSchema": {
"type": "object",
"properties": {
Expand All @@ -86,9 +86,13 @@
"path": {
"type": "string",
"description": "Absolute path to a directory, e.g., `/repo`."
},
"maxDepth": {
"type": "integer",
"description": "Maximum depth to traverse when listing directories. Use `0` to list only the specified directory, `1` to include immediate subdirectories, etc. Use `-1` for unlimited depth (to list all subdirectories recursively)."
}
},
"required": ["path"]
"required": ["path", "maxDepth"]
}
}
}
9 changes: 5 additions & 4 deletions packages/core/src/shared/utilities/workspaceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ export function formatListing(name: string, fileType: vscode.FileType, fullPath:
* You can either pass a custom callback or rely on the default `formatListing`.
*
* @param dirUri The folder to begin traversing
* @param maxDepth Maximum depth to descend (0 => just this folder)
* @param maxDepth Maximum depth to descend (0 => just this folder, -1 => unlimited depth)
* @param customFormatCallback Optional. If given, it will override the default line-formatting
*/
export async function readDirectoryRecursively(
Expand All @@ -702,7 +702,8 @@ export async function readDirectoryRecursively(
customFormatCallback?: (name: string, fileType: vscode.FileType, fullPath: string) => string
): Promise<string[]> {
const logger = getLogger()
logger.info(`Reading directory: ${dirUri.fsPath} to max depth: ${maxDepth}`)
const depthDescription = maxDepth < 0 ? 'unlimited' : maxDepth
logger.info(`Reading directory: ${dirUri.fsPath} to max depth: ${depthDescription}`)

const queue: Array<{ uri: vscode.Uri; depth: number }> = [{ uri: dirUri, depth: 0 }]
const results: string[] = []
Expand All @@ -711,7 +712,7 @@ export async function readDirectoryRecursively(

while (queue.length > 0) {
const { uri, depth } = queue.shift()!
if (depth > maxDepth) {
if (maxDepth >= 0 && depth > maxDepth) {
logger.info(`Skipping directory: ${uri.fsPath} (depth ${depth} > max ${maxDepth})`)
continue
}
Expand All @@ -729,7 +730,7 @@ export async function readDirectoryRecursively(
const childUri = vscode.Uri.joinPath(uri, name)
results.push(formatter(name, fileType, childUri.fsPath))

if (fileType === vscode.FileType.Directory && depth < maxDepth) {
if (fileType === vscode.FileType.Directory && (maxDepth < 0 || depth < maxDepth)) {
queue.push({ uri: childUri, depth: depth + 1 })
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,34 @@ describe('ListDirectory Tool', () => {
})

it('throws if path is empty', async () => {
const listDirectory = new ListDirectory({ path: '' })
const listDirectory = new ListDirectory({ path: '', maxDepth: 0 })
await assert.rejects(listDirectory.validate(), /Path cannot be empty/i, 'Expected an error about empty path')
})

it('lists directory contents', async () => {
await testFolder.mkdir('subfolder')
await testFolder.write('fileA.txt', 'fileA content')

const listDirectory = new ListDirectory({ path: testFolder.path, maxDepth: 0 })
await listDirectory.validate()
const result = await listDirectory.invoke(process.stdout)

const lines = result.output.content.split('\n')
const hasFileA = lines.some((line: string | string[]) => line.includes('[FILE] ') && line.includes('fileA.txt'))
const hasSubfolder = lines.some(
(line: string | string[]) => line.includes('[DIR] ') && line.includes('subfolder')
)

assert.ok(hasFileA, 'Should list fileA.txt in the directory output')
assert.ok(hasSubfolder, 'Should list the subfolder in the directory output')
})

it('lists directory contents recursively', async () => {
await testFolder.mkdir('subfolder')
await testFolder.write('fileA.txt', 'fileA content')
await testFolder.write(path.join('subfolder', 'fileB.md'), '# fileB')

const listDirectory = new ListDirectory({ path: testFolder.path })
const listDirectory = new ListDirectory({ path: testFolder.path, maxDepth: -1 })
await listDirectory.validate()
const result = await listDirectory.invoke(process.stdout)

Expand All @@ -33,14 +51,16 @@ describe('ListDirectory Tool', () => {
const hasSubfolder = lines.some(
(line: string | string[]) => line.includes('[DIR] ') && line.includes('subfolder')
)
const hasFileB = lines.some((line: string | string[]) => line.includes('[FILE] ') && line.includes('fileB.md'))

assert.ok(hasFileA, 'Should list fileA.txt in the directory output')
assert.ok(hasSubfolder, 'Should list the subfolder in the directory output')
assert.ok(hasFileB, 'Should list fileB.md in the subfolder in the directory output')
})

it('throws error if path does not exist', async () => {
const missingPath = path.join(testFolder.path, 'no_such_file.txt')
const listDirectory = new ListDirectory({ path: missingPath })
const listDirectory = new ListDirectory({ path: missingPath, maxDepth: 0 })

await assert.rejects(
listDirectory.validate(),
Expand All @@ -50,7 +70,7 @@ describe('ListDirectory Tool', () => {
})

it('expands ~ path', async () => {
const listDirectory = new ListDirectory({ path: '~' })
const listDirectory = new ListDirectory({ path: '~', maxDepth: 0 })
await listDirectory.validate()
const result = await listDirectory.invoke(process.stdout)

Expand Down
Loading