From 36906787caaba074552b5b603e3f947c05d5bc5e Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Thu, 19 Dec 2024 23:39:44 -0600 Subject: [PATCH 1/6] feat(symbols): add SymbolKind enum and AtInfo types for enhanced file info representation --- clients/tabby-chat-panel/src/index.ts | 139 ++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 08f419e295f7..0c880b1c2cc7 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -184,6 +184,138 @@ export interface GitRepository { url: string } +/** + * A symbol kind from vscode standard + */ +export enum SymbolKind { + /** + * The `File` symbol kind. + */ + File = 0, + /** + * The `Module` symbol kind. + */ + Module = 1, + /** + * The `Namespace` symbol kind. + */ + Namespace = 2, + /** + * The `Package` symbol kind. + */ + Package = 3, + /** + * The `Class` symbol kind. + */ + Class = 4, + /** + * The `Method` symbol kind. + */ + Method = 5, + /** + * The `Property` symbol kind. + */ + Property = 6, + /** + * The `Field` symbol kind. + */ + Field = 7, + /** + * The `Constructor` symbol kind. + */ + Constructor = 8, + /** + * The `Enum` symbol kind. + */ + Enum = 9, + /** + * The `Interface` symbol kind. + */ + Interface = 10, + /** + * The `Function` symbol kind. + */ + Function = 11, + /** + * The `Variable` symbol kind. + */ + Variable = 12, + /** + * The `Constant` symbol kind. + */ + Constant = 13, + /** + * The `String` symbol kind. + */ + String = 14, + /** + * The `Number` symbol kind. + */ + Number = 15, + /** + * The `Boolean` symbol kind. + */ + Boolean = 16, + /** + * The `Array` symbol kind. + */ + Array = 17, + /** + * The `Object` symbol kind. + */ + Object = 18, + /** + * The `Key` symbol kind. + */ + Key = 19, + /** + * The `Null` symbol kind. + */ + Null = 20, + /** + * The `EnumMember` symbol kind. + */ + EnumMember = 21, + /** + * The `Struct` symbol kind. + */ + Struct = 22, + /** + * The `Event` symbol kind. + */ + Event = 23, + /** + * The `Operator` symbol kind. + */ + Operator = 24, + /** + * The `TypeParameter` symbol kind. + */ + TypeParameter = 25, +} + +export type AtKind = 'symbol' | 'file' + +/** + * Represents as Symbol At Info in the file. + */ +export interface SymbolAtInfo { + kind: 'symbol' + name: string + location: FileLocation +} + +/** + * Represents as File At Info in the file. + */ +export interface FileAtInfo { + kind: 'file' + name: string + filepath: Filepath +} + +export type AtInfo = SymbolAtInfo | FileAtInfo + export interface ServerApi { init: (request: InitRequest) => void sendMessage: (message: ChatMessage) => void @@ -234,6 +366,13 @@ export interface ClientApiMethods { // Provide all repos found in workspace folders. readWorkspaceGitRepositories?: () => Promise + + /** + * Return a AtInfo List with kind of file + * @param kind passing what kind of At info client want to get + * @returns AtInfo array + */ + provideAtInfo?: (kind: 'symbol' | 'file') => Promise } export interface ClientApi extends ClientApiMethods { From c703a2c6b19ab22b267101333c1700c59a64a09f Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Thu, 19 Dec 2024 23:39:55 -0600 Subject: [PATCH 2/6] feat(chat): integrate FilesMonitor for enhanced file tracking and provideAtInfo API --- .../vscode/src/chat/ChatPanelViewProvider.ts | 2 +- .../vscode/src/chat/ChatSideViewProvider.ts | 4 +- clients/vscode/src/chat/WebviewHelper.ts | 48 ++++++++++ clients/vscode/src/chat/chatPanel.ts | 1 + clients/vscode/src/chat/filesMonitor.ts | 87 +++++++++++++++++++ clients/vscode/src/chat/utils.ts | 69 ++++++++++++++- clients/vscode/src/extension.ts | 4 +- 7 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 clients/vscode/src/chat/filesMonitor.ts diff --git a/clients/vscode/src/chat/ChatPanelViewProvider.ts b/clients/vscode/src/chat/ChatPanelViewProvider.ts index e0a5de7509bc..67fc56017e7b 100644 --- a/clients/vscode/src/chat/ChatPanelViewProvider.ts +++ b/clients/vscode/src/chat/ChatPanelViewProvider.ts @@ -16,7 +16,7 @@ export class ChatPanelViewProvider { gitProvider: GitProvider, ) { const logger = getLogger(); - this.webviewHelper = new WebviewHelper(context, client, logger, gitProvider); + this.webviewHelper = new WebviewHelper(context, client, logger, gitProvider, undefined); } // The method is called when the chat panel first opened diff --git a/clients/vscode/src/chat/ChatSideViewProvider.ts b/clients/vscode/src/chat/ChatSideViewProvider.ts index ca548ae1694d..42fa4b60fcf4 100644 --- a/clients/vscode/src/chat/ChatSideViewProvider.ts +++ b/clients/vscode/src/chat/ChatSideViewProvider.ts @@ -4,6 +4,7 @@ import { WebviewHelper } from "./WebviewHelper"; import { Client } from "../lsp/Client"; import type { LogOutputChannel } from "../logger"; import { GitProvider } from "../git/GitProvider"; +import { FilesMonitor } from "./filesMonitor"; export class ChatSideViewProvider implements WebviewViewProvider { webview?: WebviewView; @@ -15,8 +16,9 @@ export class ChatSideViewProvider implements WebviewViewProvider { client: Client, logger: LogOutputChannel, gitProvider: GitProvider, + filesMonitor: FilesMonitor, ) { - this.webviewHelper = new WebviewHelper(context, client, logger, gitProvider); + this.webviewHelper = new WebviewHelper(context, client, logger, gitProvider, filesMonitor); } // The method is called when the chat panel first opened diff --git a/clients/vscode/src/chat/WebviewHelper.ts b/clients/vscode/src/chat/WebviewHelper.ts index 3b2789748b4f..5bdf19160aa9 100644 --- a/clients/vscode/src/chat/WebviewHelper.ts +++ b/clients/vscode/src/chat/WebviewHelper.ts @@ -14,6 +14,9 @@ import { Location, LocationLink, workspace, + SymbolKind, + DocumentSymbol, + SymbolInformation, } from "vscode"; import type { ServerApi, @@ -25,6 +28,8 @@ import type { SymbolInfo, FileLocation, GitRepository, + AtInfo, + AtKind, } from "tabby-chat-panel"; import { TABBY_CHAT_PANEL_API_VERSION } from "tabby-chat-panel"; import hashObject from "object-hash"; @@ -42,7 +47,12 @@ import { vscodePositionToChatPanelPosition, vscodeRangeToChatPanelPositionRange, chatPanelLocationToVSCodeRange, + getAllowedSymbolKinds, + vscodeSymbolToAtInfo, + isDocumentSymbol, + fileInfoToAtInfo, } from "./utils"; +import { FilesMonitor } from "./filesMonitor"; export class WebviewHelper { webview?: Webview; @@ -56,6 +66,7 @@ export class WebviewHelper { private readonly lspClient: LspClient, private readonly logger: LogOutputChannel, private readonly gitProvider: GitProvider, + private readonly filesMonitor: FilesMonitor | undefined, ) {} static getColorThemeString(kind: ColorThemeKind) { @@ -71,6 +82,7 @@ export class WebviewHelper { public setWebview(webview: Webview) { this.webview = webview; + SymbolKind; } public setClient(client: ServerApi) { @@ -728,6 +740,42 @@ export class WebviewHelper { } return infoList; }, + provideAtInfo: async (kind: AtKind): Promise => { + switch (kind) { + case "symbol": { + const editor = window.activeTextEditor; + if (!editor) return null; + + const document = editor.document; + const symbols = await commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + document.uri, + ); + + if (!symbols || symbols.length === 0) return null; + + const results: AtInfo[] = []; + const processSymbol = (symbol: DocumentSymbol | SymbolInformation) => { + if (getAllowedSymbolKinds().includes(symbol.kind)) { + results.push(vscodeSymbolToAtInfo(symbol, document.uri, this.gitProvider)); + } + if (isDocumentSymbol(symbol)) { + symbol.children.forEach(processSymbol); + } + }; + symbols.forEach(processSymbol); + + return results; + } + case "file": { + if (this.filesMonitor) { + const files = this.filesMonitor.getWorkspaceFiles(); + return files.map((file) => fileInfoToAtInfo(file, this.gitProvider)); + } + return null; + } + } + }, }); } } diff --git a/clients/vscode/src/chat/chatPanel.ts b/clients/vscode/src/chat/chatPanel.ts index 356a85a2d343..49de62e89565 100644 --- a/clients/vscode/src/chat/chatPanel.ts +++ b/clients/vscode/src/chat/chatPanel.ts @@ -36,6 +36,7 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi lookupSymbol: api.lookupSymbol, openInEditor: api.openInEditor, readWorkspaceGitRepositories: api.readWorkspaceGitRepositories, + provideAtInfo: api.provideAtInfo, }, }); } diff --git a/clients/vscode/src/chat/filesMonitor.ts b/clients/vscode/src/chat/filesMonitor.ts new file mode 100644 index 000000000000..6641d28d9132 --- /dev/null +++ b/clients/vscode/src/chat/filesMonitor.ts @@ -0,0 +1,87 @@ +import { workspace, ExtensionContext, FileSystemWatcher, RelativePattern, Uri } from "vscode"; +import EventEmitter from "events"; +import path from "path"; + +/** + * WorkspaceFile is a file in the workspace + */ +export interface WorkspaceFile { + filename: string; + fsPath: string; +} +/** + * Keep Tracking all files in workspace with cache set + */ +export class FilesMonitor extends EventEmitter { + private fileCache: Set; + private watcher: FileSystemWatcher | undefined; + + constructor(private readonly context: ExtensionContext) { + super(); + this.fileCache = new Set(); + this.initialize(); + } + + private initialize(): void { + // Initial file scan + this.scanWorkspace(); + + // Setup file watchers + const workspaceFolders = workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders[0]) { + this.watcher = workspace.createFileSystemWatcher(new RelativePattern(workspaceFolders[0], "**/*")); + + this.watcher.onDidCreate((uri) => this.handleFileCreated(uri)); + this.watcher.onDidDelete((uri) => this.handleFileDeleted(uri)); + this.watcher.onDidChange((uri) => this.handleFileChanged(uri)); + + this.context.subscriptions.push(this.watcher); + } + } + + private async scanWorkspace() { + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders) return; + + for (const folder of workspaceFolders) { + const files = await workspace.findFiles(new RelativePattern(folder, "**/*"), "**/node_modules/**"); + files.forEach((uri) => { + this.fileCache.add(uri.fsPath); + }); + } + } + + private handleFileCreated(uri: Uri) { + this.fileCache.add(uri.fsPath); + } + + private handleFileDeleted(uri: Uri) { + this.fileCache.delete(uri.fsPath); + } + + private handleFileChanged(uri: Uri) { + if (!this.fileCache.has(uri.fsPath)) { + this.fileCache.add(uri.fsPath); + } + } + + /** + * Get all files in the workspace + * @returns WorkspaceFile is a file struct represent filename and fsPath in current workspace + */ + public getWorkspaceFiles(): WorkspaceFile[] { + return Array.from(this.fileCache).map((fsPath) => { + const relativePath = workspace.asRelativePath(fsPath); + const parsed = path.parse(relativePath); + return { + filename: parsed.base, + fsPath: fsPath, + } as WorkspaceFile; + }); + } + + public dispose(): void { + this.fileCache.clear(); + this.watcher?.dispose(); + } +} diff --git a/clients/vscode/src/chat/utils.ts b/clients/vscode/src/chat/utils.ts index 1721277345a9..01b8dffdf013 100644 --- a/clients/vscode/src/chat/utils.ts +++ b/clients/vscode/src/chat/utils.ts @@ -1,8 +1,24 @@ 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, + AtInfo, +} from "tabby-chat-panel"; import type { GitProvider } from "../git/GitProvider"; import { getLogger } from "../logger"; +import { WorkspaceFile } from "./filesMonitor"; const logger = getLogger("chat/utils"); @@ -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 vscodeSymbolToAtInfo( + symbol: DocumentSymbol | SymbolInformation, + documentUri: Uri, + gitProvider: GitProvider, +): AtInfo { + if (isDocumentSymbol(symbol)) { + return { + kind: "symbol", + name: symbol.name, + location: { + filepath: localUriToChatPanelFilepath(documentUri, gitProvider), + location: vscodeRangeToChatPanelPositionRange(symbol.range), + }, + }; + } + return { + kind: "symbol", + name: symbol.name, + location: { + filepath: localUriToChatPanelFilepath(documentUri, gitProvider), + location: vscodeRangeToChatPanelPositionRange(symbol.location.range), + }, + }; +} + +export function fileInfoToAtInfo(file: WorkspaceFile, gitProvider: GitProvider): AtInfo { + return { + kind: "file", + name: file.filename, + filepath: localUriToChatPanelFilepath(Uri.file(file.fsPath), gitProvider), + }; +} diff --git a/clients/vscode/src/extension.ts b/clients/vscode/src/extension.ts index aa1ac5ed758a..ce8077c0be20 100644 --- a/clients/vscode/src/extension.ts +++ b/clients/vscode/src/extension.ts @@ -13,6 +13,7 @@ import { ChatSideViewProvider } from "./chat/ChatSideViewProvider"; import { Commands } from "./commands"; import { CodeActions } from "./CodeActions"; import { isBrowser } from "./env"; +import { FilesMonitor } from "./chat/filesMonitor"; const logger = getLogger(); let client: Client | undefined = undefined; @@ -57,8 +58,9 @@ export async function activate(context: ExtensionContext) { client.registerInlineCompletionProvider(inlineCompletionProvider); client.registerGitProvider(gitProvider); + const filesMonitor = new FilesMonitor(context); // Register chat panel - const chatViewProvider = new ChatSideViewProvider(context, client, logger, gitProvider); + const chatViewProvider = new ChatSideViewProvider(context, client, logger, gitProvider, filesMonitor); context.subscriptions.push( window.registerWebviewViewProvider("tabby.chatView", chatViewProvider, { webviewOptions: { retainContextWhenHidden: true }, From 3dc8a8514ebc5d1b75b4fbc6cd4ca2fb93c2a6e6 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Fri, 20 Dec 2024 02:10:08 -0600 Subject: [PATCH 3/6] feat(at-info): update SymbolAtInfo and FileAtInfo interfaces to use atKind and add AtInputOpts for enhanced querying --- clients/tabby-chat-panel/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 0c880b1c2cc7..2c3cd9ac458d 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -300,7 +300,9 @@ export type AtKind = 'symbol' | 'file' * Represents as Symbol At Info in the file. */ export interface SymbolAtInfo { - kind: 'symbol' + // this means trying to query symbol in this at action + // it's different then actual SymbolKind + atKind: 'symbol' name: string location: FileLocation } @@ -309,11 +311,13 @@ export interface SymbolAtInfo { * Represents as File At Info in the file. */ export interface FileAtInfo { - kind: 'file' + atKind: 'file' name: string filepath: Filepath } +export interface AtInputOpts { query?: string, limit?: number } + export type AtInfo = SymbolAtInfo | FileAtInfo export interface ServerApi { @@ -372,7 +376,7 @@ export interface ClientApiMethods { * @param kind passing what kind of At info client want to get * @returns AtInfo array */ - provideAtInfo?: (kind: 'symbol' | 'file') => Promise + provideAtInfo?: (kind: 'symbol' | 'file', opts?: AtInputOpts) => Promise } export interface ClientApi extends ClientApiMethods { From e1176805b6371aacc57ff16e7ecaa7e89421f0fa Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Fri, 20 Dec 2024 02:14:10 -0600 Subject: [PATCH 4/6] refactor(chat): remove FilesMonitor dependency from ChatPanel and ChatSide view providers --- .../vscode/src/chat/ChatPanelViewProvider.ts | 2 +- .../vscode/src/chat/ChatSideViewProvider.ts | 4 +- clients/vscode/src/chat/filesMonitor.ts | 87 ------------------- clients/vscode/src/chat/utils.ts | 13 ++- clients/vscode/src/extension.ts | 4 +- 5 files changed, 9 insertions(+), 101 deletions(-) delete mode 100644 clients/vscode/src/chat/filesMonitor.ts diff --git a/clients/vscode/src/chat/ChatPanelViewProvider.ts b/clients/vscode/src/chat/ChatPanelViewProvider.ts index 67fc56017e7b..e0a5de7509bc 100644 --- a/clients/vscode/src/chat/ChatPanelViewProvider.ts +++ b/clients/vscode/src/chat/ChatPanelViewProvider.ts @@ -16,7 +16,7 @@ export class ChatPanelViewProvider { gitProvider: GitProvider, ) { const logger = getLogger(); - this.webviewHelper = new WebviewHelper(context, client, logger, gitProvider, undefined); + this.webviewHelper = new WebviewHelper(context, client, logger, gitProvider); } // The method is called when the chat panel first opened diff --git a/clients/vscode/src/chat/ChatSideViewProvider.ts b/clients/vscode/src/chat/ChatSideViewProvider.ts index 42fa4b60fcf4..ca548ae1694d 100644 --- a/clients/vscode/src/chat/ChatSideViewProvider.ts +++ b/clients/vscode/src/chat/ChatSideViewProvider.ts @@ -4,7 +4,6 @@ import { WebviewHelper } from "./WebviewHelper"; import { Client } from "../lsp/Client"; import type { LogOutputChannel } from "../logger"; import { GitProvider } from "../git/GitProvider"; -import { FilesMonitor } from "./filesMonitor"; export class ChatSideViewProvider implements WebviewViewProvider { webview?: WebviewView; @@ -16,9 +15,8 @@ export class ChatSideViewProvider implements WebviewViewProvider { client: Client, logger: LogOutputChannel, gitProvider: GitProvider, - filesMonitor: FilesMonitor, ) { - this.webviewHelper = new WebviewHelper(context, client, logger, gitProvider, filesMonitor); + this.webviewHelper = new WebviewHelper(context, client, logger, gitProvider); } // The method is called when the chat panel first opened diff --git a/clients/vscode/src/chat/filesMonitor.ts b/clients/vscode/src/chat/filesMonitor.ts deleted file mode 100644 index 6641d28d9132..000000000000 --- a/clients/vscode/src/chat/filesMonitor.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { workspace, ExtensionContext, FileSystemWatcher, RelativePattern, Uri } from "vscode"; -import EventEmitter from "events"; -import path from "path"; - -/** - * WorkspaceFile is a file in the workspace - */ -export interface WorkspaceFile { - filename: string; - fsPath: string; -} -/** - * Keep Tracking all files in workspace with cache set - */ -export class FilesMonitor extends EventEmitter { - private fileCache: Set; - private watcher: FileSystemWatcher | undefined; - - constructor(private readonly context: ExtensionContext) { - super(); - this.fileCache = new Set(); - this.initialize(); - } - - private initialize(): void { - // Initial file scan - this.scanWorkspace(); - - // Setup file watchers - const workspaceFolders = workspace.workspaceFolders; - if (workspaceFolders && workspaceFolders[0]) { - this.watcher = workspace.createFileSystemWatcher(new RelativePattern(workspaceFolders[0], "**/*")); - - this.watcher.onDidCreate((uri) => this.handleFileCreated(uri)); - this.watcher.onDidDelete((uri) => this.handleFileDeleted(uri)); - this.watcher.onDidChange((uri) => this.handleFileChanged(uri)); - - this.context.subscriptions.push(this.watcher); - } - } - - private async scanWorkspace() { - const workspaceFolders = workspace.workspaceFolders; - if (!workspaceFolders) return; - - for (const folder of workspaceFolders) { - const files = await workspace.findFiles(new RelativePattern(folder, "**/*"), "**/node_modules/**"); - files.forEach((uri) => { - this.fileCache.add(uri.fsPath); - }); - } - } - - private handleFileCreated(uri: Uri) { - this.fileCache.add(uri.fsPath); - } - - private handleFileDeleted(uri: Uri) { - this.fileCache.delete(uri.fsPath); - } - - private handleFileChanged(uri: Uri) { - if (!this.fileCache.has(uri.fsPath)) { - this.fileCache.add(uri.fsPath); - } - } - - /** - * Get all files in the workspace - * @returns WorkspaceFile is a file struct represent filename and fsPath in current workspace - */ - public getWorkspaceFiles(): WorkspaceFile[] { - return Array.from(this.fileCache).map((fsPath) => { - const relativePath = workspace.asRelativePath(fsPath); - const parsed = path.parse(relativePath); - return { - filename: parsed.base, - fsPath: fsPath, - } as WorkspaceFile; - }); - } - - public dispose(): void { - this.fileCache.clear(); - this.watcher?.dispose(); - } -} diff --git a/clients/vscode/src/chat/utils.ts b/clients/vscode/src/chat/utils.ts index 01b8dffdf013..32abc4572868 100644 --- a/clients/vscode/src/chat/utils.ts +++ b/clients/vscode/src/chat/utils.ts @@ -18,7 +18,6 @@ import type { } from "tabby-chat-panel"; import type { GitProvider } from "../git/GitProvider"; import { getLogger } from "../logger"; -import { WorkspaceFile } from "./filesMonitor"; const logger = getLogger("chat/utils"); @@ -140,7 +139,7 @@ export function vscodeSymbolToAtInfo( ): AtInfo { if (isDocumentSymbol(symbol)) { return { - kind: "symbol", + atKind: "symbol", name: symbol.name, location: { filepath: localUriToChatPanelFilepath(documentUri, gitProvider), @@ -149,7 +148,7 @@ export function vscodeSymbolToAtInfo( }; } return { - kind: "symbol", + atKind: "symbol", name: symbol.name, location: { filepath: localUriToChatPanelFilepath(documentUri, gitProvider), @@ -158,10 +157,10 @@ export function vscodeSymbolToAtInfo( }; } -export function fileInfoToAtInfo(file: WorkspaceFile, gitProvider: GitProvider): AtInfo { +export function uriToAtInfo(uri: Uri, gitProvider: GitProvider): AtInfo { return { - kind: "file", - name: file.filename, - filepath: localUriToChatPanelFilepath(Uri.file(file.fsPath), gitProvider), + atKind: "file", + name: path.basename(uri.fsPath), + filepath: localUriToChatPanelFilepath(uri, gitProvider), }; } diff --git a/clients/vscode/src/extension.ts b/clients/vscode/src/extension.ts index ce8077c0be20..aa1ac5ed758a 100644 --- a/clients/vscode/src/extension.ts +++ b/clients/vscode/src/extension.ts @@ -13,7 +13,6 @@ import { ChatSideViewProvider } from "./chat/ChatSideViewProvider"; import { Commands } from "./commands"; import { CodeActions } from "./CodeActions"; import { isBrowser } from "./env"; -import { FilesMonitor } from "./chat/filesMonitor"; const logger = getLogger(); let client: Client | undefined = undefined; @@ -58,9 +57,8 @@ export async function activate(context: ExtensionContext) { client.registerInlineCompletionProvider(inlineCompletionProvider); client.registerGitProvider(gitProvider); - const filesMonitor = new FilesMonitor(context); // Register chat panel - const chatViewProvider = new ChatSideViewProvider(context, client, logger, gitProvider, filesMonitor); + const chatViewProvider = new ChatSideViewProvider(context, client, logger, gitProvider); context.subscriptions.push( window.registerWebviewViewProvider("tabby.chatView", chatViewProvider, { webviewOptions: { retainContextWhenHidden: true }, From 34f86161567e0c43f6f13a00d34cc5b380f208e2 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Fri, 20 Dec 2024 02:16:13 -0600 Subject: [PATCH 5/6] feat(chat): enhance provideAtInfo to support query and limit options for symbol and file retrieval --- clients/vscode/src/chat/WebviewHelper.ts | 71 +++++++++++++++++------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/clients/vscode/src/chat/WebviewHelper.ts b/clients/vscode/src/chat/WebviewHelper.ts index 5bdf19160aa9..c8d01d0b4203 100644 --- a/clients/vscode/src/chat/WebviewHelper.ts +++ b/clients/vscode/src/chat/WebviewHelper.ts @@ -30,6 +30,7 @@ import type { GitRepository, AtInfo, AtKind, + AtInputOpts, } from "tabby-chat-panel"; import { TABBY_CHAT_PANEL_API_VERSION } from "tabby-chat-panel"; import hashObject from "object-hash"; @@ -50,9 +51,8 @@ import { getAllowedSymbolKinds, vscodeSymbolToAtInfo, isDocumentSymbol, - fileInfoToAtInfo, + uriToAtInfo, } from "./utils"; -import { FilesMonitor } from "./filesMonitor"; export class WebviewHelper { webview?: Webview; @@ -66,7 +66,6 @@ export class WebviewHelper { private readonly lspClient: LspClient, private readonly logger: LogOutputChannel, private readonly gitProvider: GitProvider, - private readonly filesMonitor: FilesMonitor | undefined, ) {} static getColorThemeString(kind: ColorThemeKind) { @@ -740,39 +739,69 @@ export class WebviewHelper { } return infoList; }, - provideAtInfo: async (kind: AtKind): Promise => { + provideAtInfo: async (kind: AtKind, opts?: AtInputOpts): Promise => { + // Common parameters + const maxResults = opts?.limit || 50; + const query = opts?.query?.toLowerCase(); + switch (kind) { case "symbol": { const editor = window.activeTextEditor; if (!editor) return null; - const document = editor.document; - const symbols = await commands.executeCommand( + + // Try document symbols first + const documentSymbols = await commands.executeCommand( "vscode.executeDocumentSymbolProvider", document.uri, ); - if (!symbols || symbols.length === 0) return null; + let results: AtInfo[] = []; - const results: AtInfo[] = []; - const processSymbol = (symbol: DocumentSymbol | SymbolInformation) => { - if (getAllowedSymbolKinds().includes(symbol.kind)) { - results.push(vscodeSymbolToAtInfo(symbol, document.uri, this.gitProvider)); - } - if (isDocumentSymbol(symbol)) { - symbol.children.forEach(processSymbol); + 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(vscodeSymbolToAtInfo(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) => vscodeSymbolToAtInfo(symbol, symbol.location.uri, this.gitProvider)); } - }; - symbols.forEach(processSymbol); + } - return results; + return results.length > 0 ? results : null; } case "file": { - if (this.filesMonitor) { - const files = this.filesMonitor.getWorkspaceFiles(); - return files.map((file) => fileInfoToAtInfo(file, this.gitProvider)); + // hack way to only get prefix match + const globPattern = query ? `**/${query}*` : "**/*"; + try { + const files = await workspace.findFiles(globPattern, null, maxResults); + return files.map((uri) => uriToAtInfo(uri, this.gitProvider)); + } catch (error) { + this.logger.error("Failed to find files:", error); + return null; } - return null; } } }, From eff945052fc566eeb9db84cc9acc1ef38340e403 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Fri, 20 Dec 2024 03:00:35 -0600 Subject: [PATCH 6/6] feat(chat): refactor provideAtInfo to provideSymbolAtInfo and provideFileAtInfo with enhanced content retrieval --- clients/tabby-chat-panel/src/index.ts | 12 +- clients/vscode/src/chat/WebviewHelper.ts | 137 ++++++++++++++--------- clients/vscode/src/chat/chatPanel.ts | 5 +- clients/vscode/src/chat/utils.ts | 7 +- 4 files changed, 98 insertions(+), 63 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 2c3cd9ac458d..b5a133ec37ed 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -372,11 +372,17 @@ export interface ClientApiMethods { readWorkspaceGitRepositories?: () => Promise /** - * Return a AtInfo List with kind of file + * Return a SymbolAtInfo List with kind of file * @param kind passing what kind of At info client want to get - * @returns AtInfo array + * @returns SymbolAtInfo array */ - provideAtInfo?: (kind: 'symbol' | 'file', opts?: AtInputOpts) => Promise + provideSymbolAtInfo?: (opts?: AtInputOpts) => Promise + + getSymbolAtInfoContent?: (info: SymbolAtInfo) => Promise + + provideFileAtInfo?: (opts?: AtInputOpts) => Promise + + getFileAtInfoContent?: (info: FileAtInfo) => Promise } export interface ClientApi extends ClientApiMethods { diff --git a/clients/vscode/src/chat/WebviewHelper.ts b/clients/vscode/src/chat/WebviewHelper.ts index c8d01d0b4203..21c780ac836b 100644 --- a/clients/vscode/src/chat/WebviewHelper.ts +++ b/clients/vscode/src/chat/WebviewHelper.ts @@ -28,9 +28,9 @@ import type { SymbolInfo, FileLocation, GitRepository, - AtInfo, - AtKind, AtInputOpts, + SymbolAtInfo, + FileAtInfo, } from "tabby-chat-panel"; import { TABBY_CHAT_PANEL_API_VERSION } from "tabby-chat-panel"; import hashObject from "object-hash"; @@ -49,9 +49,9 @@ import { vscodeRangeToChatPanelPositionRange, chatPanelLocationToVSCodeRange, getAllowedSymbolKinds, - vscodeSymbolToAtInfo, + vscodeSymbolToAtInfo as vscodeSymbolToSymbolAtInfo, isDocumentSymbol, - uriToAtInfo, + uriToFileAtInfo, } from "./utils"; export class WebviewHelper { @@ -739,71 +739,96 @@ export class WebviewHelper { } return infoList; }, - provideAtInfo: async (kind: AtKind, opts?: AtInputOpts): Promise => { - // Common parameters + provideSymbolAtInfo: async (opts?: AtInputOpts): Promise => { const maxResults = opts?.limit || 50; const query = opts?.query?.toLowerCase(); - switch (kind) { - case "symbol": { - const editor = window.activeTextEditor; - if (!editor) return null; - const document = editor.document; + 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, - ); + // Try document symbols first + const documentSymbols = await commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + document.uri, + ); - let results: AtInfo[] = []; + let results: SymbolAtInfo[] = []; - if (documentSymbols && documentSymbols.length > 0) { - const processSymbol = (symbol: DocumentSymbol | SymbolInformation) => { - if (results.length >= maxResults) return; + 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; + const symbolName = symbol.name.toLowerCase(); + if (query && !symbolName.includes(query)) return; - if (getAllowedSymbolKinds().includes(symbol.kind)) { - results.push(vscodeSymbolToAtInfo(symbol, document.uri, this.gitProvider)); - } - if (isDocumentSymbol(symbol)) { - symbol.children.forEach(processSymbol); - } - }; - documentSymbols.forEach(processSymbol); + if (getAllowedSymbolKinds().includes(symbol.kind)) { + results.push(vscodeSymbolToSymbolAtInfo(symbol, document.uri, this.gitProvider)); } - - // 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) => vscodeSymbolToAtInfo(symbol, symbol.location.uri, this.gitProvider)); - } + if (isDocumentSymbol(symbol)) { + symbol.children.forEach(processSymbol); } + }; + documentSymbols.forEach(processSymbol); + } - return results.length > 0 ? results : null; - } - case "file": { - // hack way to only get prefix match - const globPattern = query ? `**/${query}*` : "**/*"; - try { - const files = await workspace.findFiles(globPattern, null, maxResults); - return files.map((uri) => uriToAtInfo(uri, this.gitProvider)); - } catch (error) { - this.logger.error("Failed to find files:", error); - return null; - } + // 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) => uriToFileAtInfo(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 49de62e89565..afe4237685f6 100644 --- a/clients/vscode/src/chat/chatPanel.ts +++ b/clients/vscode/src/chat/chatPanel.ts @@ -36,7 +36,10 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi lookupSymbol: api.lookupSymbol, openInEditor: api.openInEditor, readWorkspaceGitRepositories: api.readWorkspaceGitRepositories, - provideAtInfo: api.provideAtInfo, + 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 32abc4572868..8cec030376e1 100644 --- a/clients/vscode/src/chat/utils.ts +++ b/clients/vscode/src/chat/utils.ts @@ -14,7 +14,8 @@ import type { LineRange, PositionRange, Location, - AtInfo, + SymbolAtInfo, + FileAtInfo, } from "tabby-chat-panel"; import type { GitProvider } from "../git/GitProvider"; import { getLogger } from "../logger"; @@ -136,7 +137,7 @@ export function vscodeSymbolToAtInfo( symbol: DocumentSymbol | SymbolInformation, documentUri: Uri, gitProvider: GitProvider, -): AtInfo { +): SymbolAtInfo { if (isDocumentSymbol(symbol)) { return { atKind: "symbol", @@ -157,7 +158,7 @@ export function vscodeSymbolToAtInfo( }; } -export function uriToAtInfo(uri: Uri, gitProvider: GitProvider): AtInfo { +export function uriToFileAtInfo(uri: Uri, gitProvider: GitProvider): FileAtInfo { return { atKind: "file", name: path.basename(uri.fsPath),