Skip to content

Commit

Permalink
feat: connect remote api to select file or folders
Browse files Browse the repository at this point in the history
  • Loading branch information
2214962083 committed Sep 8, 2024
1 parent ca9fa9b commit 597b4f0
Show file tree
Hide file tree
Showing 34 changed files with 730 additions and 239 deletions.
11 changes: 6 additions & 5 deletions src/extension/ai/get-reference-file-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ export const getReferenceFilePaths = async ({
const workspaceFolder = getWorkspaceFolder()
const allRelativePaths: string[] = []

await traverseFileOrFolders(
[workspaceFolder.uri.fsPath],
workspaceFolder.uri.fsPath,
fileInfo => {
await traverseFileOrFolders({
type: 'file',
filesOrFolders: [workspaceFolder.uri.fsPath],
workspacePath: workspaceFolder.uri.fsPath,
itemCallback: fileInfo => {
allRelativePaths.push(fileInfo.relativePath)
}
)
})

const currentFileRelativePath =
vscode.workspace.asRelativePath(currentFilePath)
Expand Down
11 changes: 6 additions & 5 deletions src/extension/commands/ask-ai/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ export class AskAICommand extends BaseCommand {
filesFullPath += ` "${quote([fullPath.trim()])}" `
}

await traverseFileOrFolders(
selectedFileOrFolders,
workspaceFolder.uri.fsPath,
processFile
)
await traverseFileOrFolders({
type: 'file',
filesOrFolders: selectedFileOrFolders,
workspacePath: workspaceFolder.uri.fsPath,
itemCallback: processFile
})

const aiCommand = await getConfigKey('aiCommand')
const aiCommandCopyBeforeRun = await getConfigKey('aiCommandCopyBeforeRun')
Expand Down
12 changes: 7 additions & 5 deletions src/extension/commands/batch-processor/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ export class BatchProcessorCommand extends BaseCommand {
if (selectedItems.length === 0) throw new Error(t('error.noSelection'))

const selectedFileOrFolders = selectedItems.map(item => item.fsPath)
const filesInfo = await traverseFileOrFolders(
selectedFileOrFolders,
workspaceFolder.uri.fsPath,
fileInfo => fileInfo
)
const filesInfo = await traverseFileOrFolders({
type: 'file',
filesOrFolders: selectedFileOrFolders,
workspacePath: workspaceFolder.uri.fsPath,
itemCallback: fileInfo => fileInfo
})

const fileRelativePathsForProcess = filesInfo
.filter(fileInfo => !isTmpFileUri(vscode.Uri.file(fileInfo.fullPath)))
.map(fileInfo => fileInfo.relativePath)
Expand Down
11 changes: 6 additions & 5 deletions src/extension/commands/batch-processor/get-pre-process-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ export const getPreProcessInfo = async ({
const workspaceFolder = getWorkspaceFolder()
const allFileRelativePaths: string[] = []

await traverseFileOrFolders(
[workspaceFolder.uri.fsPath],
workspaceFolder.uri.fsPath,
fileInfo => {
await traverseFileOrFolders({
type: 'file',
filesOrFolders: [workspaceFolder.uri.fsPath],
workspacePath: workspaceFolder.uri.fsPath,
itemCallback: fileInfo => {
allFileRelativePaths.push(fileInfo.relativePath)
}
)
})

const modelProvider = await createModelProvider()
const aiRunnable = await modelProvider.createStructuredOutputRunnable({
Expand Down
7 changes: 6 additions & 1 deletion src/extension/file-utils/get-fs-prompt-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ export const getFileOrFoldersPromptInfo = async (
result.promptFullContent += promptFullContent
}

await traverseFileOrFolders(fileOrFolders, workspacePath, processFile)
await traverseFileOrFolders({
type: 'file',
filesOrFolders: fileOrFolders,
workspacePath,
itemCallback: processFile
})

return result
}
43 changes: 43 additions & 0 deletions src/extension/file-utils/ignore-patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,46 @@ export const getAllValidFiles = async (
}
})
}

/**
* Retrieves all valid folders in the specified directory path.
* @param fullDirPath - The full path of the directory.
* @returns A promise that resolves to an array of strings representing the absolute paths of the valid folders.
*/
export const getAllValidFolders = async (
fullDirPath: string
): Promise<string[]> => {
const shouldIgnore = await createShouldIgnore(fullDirPath)

const filesOrFolders = await glob('**/*', {
cwd: fullDirPath,
nodir: false,
absolute: true,
follow: false,
dot: true,
ignore: {
ignored(p) {
return shouldIgnore(p.fullpath())
},
childrenIgnored(p) {
try {
return shouldIgnore(p.fullpath())
} catch {
return false
}
}
}
})

const folders: string[] = []
const promises = filesOrFolders.map(async fileOrFolder => {
const stat = await VsCodeFS.stat(fileOrFolder)
if (stat.type === vscode.FileType.Directory) {
folders.push(fileOrFolder)
}
})

await Promise.allSettled(promises)

return folders
}
126 changes: 69 additions & 57 deletions src/extension/file-utils/traverse-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,33 @@ import * as path from 'path'
import type { MaybePromise } from '@extension/types/common'
import * as vscode from 'vscode'

import { getAllValidFiles } from './ignore-patterns'
import { getAllValidFiles, getAllValidFolders } from './ignore-patterns'
import { VsCodeFS } from './vscode-fs'

/**
* Represents information about a file.
*/
export interface FileInfo {
/**
* The content of the file.
*/
type: 'file'
content: string

/**
* The relative path of the file.
*/
relativePath: string
fullPath: string
}

/**
* The full path of the file.
*/
export interface FolderInfo {
type: 'folder'
relativePath: string
fullPath: string
}

/**
* Retrieves information about a file.
* @param filePath - The path of the file.
* @param workspacePath - The path of the workspace.
* @returns A Promise that resolves to the file information, or null if the file does not exist.
*/
type FsType = 'file' | 'folder'

interface TraverseOptions<T, Type extends FsType = 'file'> {
type: Type
filesOrFolders: string[]
workspacePath: string
itemCallback: (
itemInfo: Type extends 'file' ? FileInfo : FolderInfo
) => MaybePromise<T>
}

const getFileInfo = async (
filePath: string,
workspacePath: string
Expand All @@ -42,62 +40,76 @@ const getFileInfo = async (
const relativePath = path.relative(workspacePath, filePath)

return {
type: 'file',
content: fileContent,
relativePath,
fullPath: filePath
}
}

/**
* Traverses through an array of file or folder paths and performs a callback function on each file.
* Returns an array of results from the callback function.
*
* @param filesOrFolders - An array of file or folder paths.
* @param workspacePath - The path of the workspace.
* @param fileCallback - The callback function to be performed on each file.
* @returns An array of results from the callback function.
*/
export const traverseFileOrFolders = async <T>(
filesOrFolders: string[],
workspacePath: string,
fileCallback: (fileInfo: FileInfo) => MaybePromise<T>
const getFolderInfo = async (
folderPath: string,
workspacePath: string
): Promise<FolderInfo> => {
const relativePath = path.relative(workspacePath, folderPath)

return {
type: 'folder',
relativePath,
fullPath: folderPath
}
}

export const traverseFileOrFolders = async <T, Type extends FsType>(
props: TraverseOptions<T, Type>
): Promise<T[]> => {
const filePathSet = new Set<string>()
const { type = 'file', filesOrFolders, workspacePath, itemCallback } = props
const itemPathSet = new Set<string>()
const results: T[] = []

const processFolder = async (folderPath: string) => {
if (itemPathSet.has(folderPath)) return

itemPathSet.add(folderPath)
const folderInfo = await getFolderInfo(folderPath, workspacePath)
results.push(await itemCallback(folderInfo as any))
}

const processFile = async (filePath: string) => {
if (itemPathSet.has(filePath)) return

itemPathSet.add(filePath)
const fileInfo = await getFileInfo(filePath, workspacePath)
results.push(await itemCallback(fileInfo as any))
}

await Promise.allSettled(
filesOrFolders.map(async fileOrFolder => {
// Convert relative path to absolute path
const absolutePath = path.isAbsolute(fileOrFolder)
? fileOrFolder
: path.join(workspacePath, fileOrFolder)
const stat = await VsCodeFS.stat(absolutePath)

if (stat.type === vscode.FileType.Directory) {
const allFiles = await getAllValidFiles(absolutePath)

await Promise.allSettled(
allFiles.map(async filePath => {
if (filePathSet.has(filePath)) return
filePathSet.add(filePath)

const fileInfo = await getFileInfo(filePath, workspacePath)
if (fileInfo) {
results.push(await fileCallback(fileInfo))
}
})
)
if (type === 'folder') {
const allFolders = await getAllValidFolders(absolutePath)
await Promise.allSettled(
allFolders.map(async folderPath => {
await processFolder(folderPath)
})
)
} else if (type === 'file') {
const allFiles = await getAllValidFiles(absolutePath)
await Promise.allSettled(
allFiles.map(async filePath => {
await processFile(filePath)
})
)
}
}

if (stat.type === vscode.FileType.File) {
if (filePathSet.has(absolutePath)) return
filePathSet.add(absolutePath)

const fileInfo = await getFileInfo(absolutePath, workspacePath)

if (fileInfo) {
results.push(await fileCallback(fileInfo))
}
if (stat.type === vscode.FileType.File && type === 'file') {
await processFile(absolutePath)
}
})
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ export class FileProcessor implements ContextProcessor<FileContext> {
const workspacePath = getWorkspaceFolder().uri.fsPath

const processFolder = async (folder: string): Promise<string> => {
const files = await traverseFileOrFolders(
[folder],
const files = await traverseFileOrFolders({
type: 'file',
filesOrFolders: [folder],
workspacePath,
async (fileInfo: FileInfo) => {
itemCallback: async (fileInfo: FileInfo) => {
const { relativePath, fullPath } = fileInfo
const languageId = getLanguageId(path.extname(relativePath).slice(1))
const content = await VsCodeFS.readFileOrOpenDocumentContent(fullPath)
return `\`\`\`${languageId}:${relativePath}\n${content}\n\`\`\`\n\n`
}
)
})
return files.join('')
}

Expand Down
30 changes: 30 additions & 0 deletions src/extension/webview-api/controllers/file.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import {
traverseFileOrFolders,
type FileInfo,
type FolderInfo
} from '@extension/file-utils/traverse-fs'
import { VsCodeFS } from '@extension/file-utils/vscode-fs'
import { getWorkspaceFolder } from '@extension/utils'
import * as vscode from 'vscode'

import { Controller } from '../types'
Expand Down Expand Up @@ -44,4 +50,28 @@ export class FileController extends Controller {
async readdir(req: { path: string }): Promise<string[]> {
return await VsCodeFS.readdir(req.path)
}

async traverseWorkspaceFiles(req: {
filesOrFolders: string[]
}): Promise<FileInfo[]> {
const workspaceFolder = getWorkspaceFolder()
return await traverseFileOrFolders({
type: 'file',
filesOrFolders: req.filesOrFolders,
workspacePath: workspaceFolder.uri.fsPath,
itemCallback: fileInfo => fileInfo
})
}

async traverseWorkspaceFolders(req: {
folders: string[]
}): Promise<FolderInfo[]> {
const workspaceFolder = getWorkspaceFolder()
return await traverseFileOrFolders({
type: 'folder',
filesOrFolders: req.folders,
workspacePath: workspaceFolder.uri.fsPath,
itemCallback: folderInfo => folderInfo
})
}
}
3 changes: 2 additions & 1 deletion src/extension/webview-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getErrorMsg } from '@extension/utils'
import * as vscode from 'vscode'

import { ChatController } from './controllers/chat.controller'
import { FileController } from './controllers/file.controller'
import type {
Controller,
ControllerClass,
Expand Down Expand Up @@ -81,7 +82,7 @@ class APIManager {
}
}

export const controllers = [ChatController] as const
export const controllers = [ChatController, FileController] as const
export type Controllers = typeof controllers

export const setupWebviewAPIManager = (
Expand Down
Loading

0 comments on commit 597b4f0

Please sign in to comment.