From ccad4a335160cf224fca8cfe105ca349f19544f7 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Mon, 6 Jan 2025 01:27:45 -0500 Subject: [PATCH] feat(chat): add listFileInWorkspace and readFileContent APIs --- clients/vscode/src/chat/createClient.ts | 2 + clients/vscode/src/chat/utils.ts | 100 ++++++++++++------------ clients/vscode/src/chat/webview.ts | 59 ++++++++++++++ 3 files changed, 109 insertions(+), 52 deletions(-) diff --git a/clients/vscode/src/chat/createClient.ts b/clients/vscode/src/chat/createClient.ts index 924ee0f546d0..4020beb0875a 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -36,6 +36,8 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi openExternal: api.openExternal, readWorkspaceGitRepositories: api.readWorkspaceGitRepositories, getActiveEditorSelection: api.getActiveEditorSelection, + listFileInWorkspace: api.listFileInWorkspace, + readFileContent: api.readFileContent, }, }); } diff --git a/clients/vscode/src/chat/utils.ts b/clients/vscode/src/chat/utils.ts index e30ca06b28da..7c616fc1e4d9 100644 --- a/clients/vscode/src/chat/utils.ts +++ b/clients/vscode/src/chat/utils.ts @@ -1,15 +1,14 @@ import path from "path"; -import { - Position as VSCodePosition, - Range as VSCodeRange, - Uri, - workspace, - DocumentSymbol, - SymbolInformation, - SymbolKind, - TextEditor, -} from "vscode"; -import type { Filepath, Position as ChatPanelPosition, LineRange, PositionRange, Location } from "tabby-chat-panel"; +import { Position as VSCodePosition, Range as VSCodeRange, Uri, workspace, TextEditor, TextDocument } from "vscode"; +import type { + Filepath, + Position as ChatPanelPosition, + LineRange, + PositionRange, + Location, + ListFileItem, + FilepathInGitRepository, +} from "tabby-chat-panel"; import type { GitProvider } from "../git/GitProvider"; import { getLogger } from "../logger"; @@ -224,51 +223,48 @@ export function generateLocalNotebookCellUri(notebook: Uri, handle: number): Uri return notebook.with({ scheme: DocumentSchemes.vscodeNotebookCell, fragment }); } -export function isDocumentSymbol(symbol: DocumentSymbol | SymbolInformation): symbol is DocumentSymbol { - return "children" in symbol; -} - -// FIXME: All allow symbol kinds, could be change later -export function getAllowedSymbolKinds(): SymbolKind[] { - return [ - SymbolKind.Class, - SymbolKind.Function, - SymbolKind.Method, - SymbolKind.Interface, - SymbolKind.Enum, - SymbolKind.Struct, - ]; -} +export function uriToListFileItem(uri: Uri, gitProvider: GitProvider): ListFileItem { + const filepath = localUriToChatPanelFilepath(uri, gitProvider); + let label: string; -export function vscodeSymbolToSymbolAtInfo( - symbol: DocumentSymbol | SymbolInformation, - documentUri: Uri, - gitProvider: GitProvider, -): SymbolAtInfo { - if (isDocumentSymbol(symbol)) { - return { - atKind: "symbol", - name: symbol.name, - location: { - filepath: localUriToChatPanelFilepath(documentUri, gitProvider), - location: vscodeRangeToChatPanelPositionRange(symbol.range), - }, - }; + if (filepath.kind === "git") { + label = filepath.filepath; + } else { + const workspaceFolder = workspace.getWorkspaceFolder(uri); + if (workspaceFolder) { + label = path.relative(workspaceFolder.uri.fsPath, uri.fsPath); + } else { + label = path.basename(uri.fsPath); + } } + return { - atKind: "symbol", - name: symbol.name, - location: { - filepath: localUriToChatPanelFilepath(documentUri, gitProvider), - location: vscodeRangeToChatPanelPositionRange(symbol.location.range), - }, + label, + filepath, }; } -export function uriToFileAtFileInfo(uri: Uri, gitProvider: GitProvider): FileAtInfo { - return { - atKind: "file", - name: path.basename(uri.fsPath), - filepath: localUriToChatPanelFilepath(uri, gitProvider), - }; +export function extractTextFromRange(document: TextDocument, range: LineRange | PositionRange): string { + if (typeof range.start === "number" && typeof range.end === "number") { + const startLine = range.start - 1; + const endLine = range.end - 1; + let selectedText = ""; + + for (let i = startLine; i <= endLine; i++) { + if (i < 0 || i >= document.lineCount) { + continue; + } + selectedText += document.lineAt(i).text + "\n"; + } + return selectedText; + } + + if (typeof range.start === "object" && typeof range.end === "object") { + const startPos = new VSCodePosition(range.start.line - 1, range.start.character - 1); + const endPos = new VSCodePosition(range.end.line - 1, range.end.character - 1); + const selectedRange = new VSCodeRange(startPos, endPos); + return document.getText(selectedRange); + } + + return ""; } diff --git a/clients/vscode/src/chat/webview.ts b/clients/vscode/src/chat/webview.ts index 9cef59828d50..f38659d8f4be 100644 --- a/clients/vscode/src/chat/webview.ts +++ b/clients/vscode/src/chat/webview.ts @@ -26,6 +26,9 @@ import type { FileLocation, GitRepository, EditorFileContext, + ListFilesInWorkspaceParams, + ListFileItem, + FileRange, } from "tabby-chat-panel"; import * as semver from "semver"; import type { StatusInfo, Config } from "tabby-agent"; @@ -42,6 +45,8 @@ import { vscodeRangeToChatPanelPositionRange, chatPanelLocationToVSCodeRange, isValidForSyncActiveEditorSelection, + uriToListFileItem, + extractTextFromRange, } from "./utils"; import mainHtml from "./html/main.html"; import errorHtml from "./html/error.html"; @@ -456,6 +461,60 @@ export class ChatWebview { const fileContext = await getFileContextFromSelection(editor, this.gitProvider); return fileContext; }, + listFileInWorkspace: async (params: ListFilesInWorkspaceParams): Promise => { + const maxResults = params.limit || 50; + const query = params.query?.toLowerCase(); + + if (!query) { + const documents = workspace.textDocuments; + this.logger.info(`No query provided, listing ${documents.length} opened editors.`); + return documents + .filter((doc) => doc.uri.scheme === "file") + .map((document) => uriToListFileItem(document.uri, this.gitProvider)); + } + + const globPattern = `**/${query}*`; + this.logger.info(`Searching files with pattern: ${globPattern}, limit: ${maxResults}.`); + try { + const files = await workspace.findFiles(globPattern, null, maxResults); + this.logger.info(`Found ${files.length} files.`); + return files.map((uri) => uriToListFileItem(uri, this.gitProvider)); + } catch (error) { + this.logger.warn("Failed to find files:", error); + return []; + } + }, + + readFileContent: async (info: FileRange): Promise => { + if (info.range) { + try { + const uri = chatPanelFilepathToLocalUri(info.filepath, this.gitProvider); + if (!uri) { + this.logger.warn(`Could not resolve URI from filepath: ${JSON.stringify(info.filepath)}`); + return null; + } + const document = await workspace.openTextDocument(uri); + + return extractTextFromRange(document, info.range); + } catch (error) { + this.logger.error("Failed to get file content by range:", error); + return null; + } + } else { + try { + const uri = chatPanelFilepathToLocalUri(info.filepath, this.gitProvider); + if (!uri) { + this.logger.warn(`Could not resolve URI from filepath: ${JSON.stringify(info.filepath)}`); + return null; + } + const document = await workspace.openTextDocument(uri); + return document.getText(); + } catch (error) { + this.logger.error("Failed to get file content:", error); + return null; + } + } + }, }); }