diff --git a/clients/vscode/src/chat/WebviewHelper.ts b/clients/vscode/src/chat/WebviewHelper.ts index 3b2789748b4f..b4c9fc3934f8 100644 --- a/clients/vscode/src/chat/WebviewHelper.ts +++ b/clients/vscode/src/chat/WebviewHelper.ts @@ -14,6 +14,8 @@ import { Location, LocationLink, workspace, + DocumentSymbol, + SymbolInformation, } from "vscode"; import type { ServerApi, @@ -25,6 +27,9 @@ import type { SymbolInfo, FileLocation, GitRepository, + SymbolAtInfo, + AtInputOpts, + FileAtInfo, } from "tabby-chat-panel"; import { TABBY_CHAT_PANEL_API_VERSION } from "tabby-chat-panel"; import hashObject from "object-hash"; @@ -42,6 +47,10 @@ import { vscodePositionToChatPanelPosition, vscodeRangeToChatPanelPositionRange, chatPanelLocationToVSCodeRange, + getAllowedSymbolKinds, + isDocumentSymbol, + vscodeSymbolToSymbolAtInfo, + uriToFileAtFileInfo, } from "./utils"; export class WebviewHelper { @@ -728,6 +737,97 @@ export class WebviewHelper { } return infoList; }, + provideSymbolAtInfo: async (opts?: AtInputOpts): Promise => { + const maxResults = opts?.limit || 50; + const query = opts?.query?.toLowerCase(); + + const editor = window.activeTextEditor; + if (!editor) return null; + const document = editor.document; + + // Try document symbols first + const documentSymbols = await commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + document.uri, + ); + + let results: SymbolAtInfo[] = []; + + if (documentSymbols && documentSymbols.length > 0) { + const processSymbol = (symbol: DocumentSymbol | SymbolInformation) => { + if (results.length >= maxResults) return; + + const symbolName = symbol.name.toLowerCase(); + if (query && !symbolName.includes(query)) return; + + if (getAllowedSymbolKinds().includes(symbol.kind)) { + results.push(vscodeSymbolToSymbolAtInfo(symbol, document.uri, this.gitProvider)); + } + if (isDocumentSymbol(symbol)) { + symbol.children.forEach(processSymbol); + } + }; + documentSymbols.forEach(processSymbol); + } + + // Try workspace symbols if no document symbols found + if (results.length === 0 && query) { + const workspaceSymbols = await commands.executeCommand( + "vscode.executeWorkspaceSymbolProvider", + query, + ); + + if (workspaceSymbols) { + results = workspaceSymbols + .filter((symbol) => getAllowedSymbolKinds().includes(symbol.kind)) + .slice(0, maxResults) + .map((symbol) => vscodeSymbolToSymbolAtInfo(symbol, symbol.location.uri, this.gitProvider)); + } + } + + return results.length > 0 ? results : null; + }, + + provideFileAtInfo: async (opts?: AtInputOpts): Promise => { + const maxResults = opts?.limit || 50; + const query = opts?.query?.toLowerCase(); + + const globPattern = query ? `**/${query}*` : "**/*"; + try { + const files = await workspace.findFiles(globPattern, null, maxResults); + return files.map((uri) => uriToFileAtFileInfo(uri, this.gitProvider)); + } catch (error) { + this.logger.error("Failed to find files:", error); + return null; + } + }, + getSymbolAtInfoContent: async (info: SymbolAtInfo): Promise => { + try { + const uri = chatPanelFilepathToLocalUri(info.location.filepath, this.gitProvider); + if (!uri) return null; + + const document = await workspace.openTextDocument(uri); + const range = chatPanelLocationToVSCodeRange(info.location.location); + if (!range) return null; + + return document.getText(range); + } catch (error) { + this.logger.error("Failed to get symbol content:", error); + return null; + } + }, + getFileAtInfoContent: async (info: FileAtInfo): Promise => { + try { + const uri = chatPanelFilepathToLocalUri(info.filepath, this.gitProvider); + if (!uri) return null; + + const document = await workspace.openTextDocument(uri); + return document.getText(); + } catch (error) { + this.logger.error("Failed to get file content:", error); + return null; + } + }, }); } } diff --git a/clients/vscode/src/chat/chatPanel.ts b/clients/vscode/src/chat/chatPanel.ts index 356a85a2d343..afe4237685f6 100644 --- a/clients/vscode/src/chat/chatPanel.ts +++ b/clients/vscode/src/chat/chatPanel.ts @@ -36,6 +36,10 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi lookupSymbol: api.lookupSymbol, openInEditor: api.openInEditor, readWorkspaceGitRepositories: api.readWorkspaceGitRepositories, + provideSymbolAtInfo: api.provideSymbolAtInfo, + getSymbolAtInfoContent: api.getSymbolAtInfoContent, + provideFileAtInfo: api.provideFileAtInfo, + getFileAtInfoContent: api.getFileAtInfoContent, }, }); } diff --git a/clients/vscode/src/chat/utils.ts b/clients/vscode/src/chat/utils.ts index 1721277345a9..aec01c49eebc 100644 --- a/clients/vscode/src/chat/utils.ts +++ b/clients/vscode/src/chat/utils.ts @@ -1,6 +1,22 @@ import path from "path"; -import { Position as VSCodePosition, Range as VSCodeRange, Uri, workspace } from "vscode"; -import type { Filepath, Position as ChatPanelPosition, LineRange, PositionRange, Location } from "tabby-chat-panel"; +import { + Position as VSCodePosition, + Range as VSCodeRange, + Uri, + workspace, + DocumentSymbol, + SymbolInformation, + SymbolKind, +} from "vscode"; +import type { + Filepath, + Position as ChatPanelPosition, + LineRange, + PositionRange, + Location, + SymbolAtInfo, + FileAtInfo, +} from "tabby-chat-panel"; import type { GitProvider } from "../git/GitProvider"; import { getLogger } from "../logger"; @@ -100,3 +116,52 @@ export function chatPanelLocationToVSCodeRange(location: Location): VSCodeRange logger.warn(`Invalid location params.`, location); return null; } + +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 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), + }, + }; + } + return { + atKind: "symbol", + name: symbol.name, + location: { + filepath: localUriToChatPanelFilepath(documentUri, gitProvider), + location: vscodeRangeToChatPanelPositionRange(symbol.location.range), + }, + }; +} + +export function uriToFileAtFileInfo(uri: Uri, gitProvider: GitProvider): FileAtInfo { + return { + atKind: "file", + name: path.basename(uri.fsPath), + filepath: localUriToChatPanelFilepath(uri, gitProvider), + }; +}