diff --git a/clients/tabby-chat-panel/package.json b/clients/tabby-chat-panel/package.json index ee4608375f5d..b107bf01035d 100644 --- a/clients/tabby-chat-panel/package.json +++ b/clients/tabby-chat-panel/package.json @@ -1,7 +1,7 @@ { "name": "tabby-chat-panel", "type": "module", - "version": "0.5.0", + "version": "0.6.0", "keywords": [], "sideEffects": false, "exports": { diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 857d83a42362..030294b76f18 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -107,7 +107,7 @@ export interface ErrorMessage { /** * Represents a filepath to identify a file. */ -export type Filepath = FilepathInGitRepository | FilepathUri +export type Filepath = FilepathInGitRepository | FilepathUri | FilepathAbsoluteUri /** * This is used for files in a Git repository, and should be used in priority. @@ -135,11 +135,30 @@ export interface FilepathInGitRepository { } /** - * This is used for files not in a Git repository. + * This is used for files in the workspace, but not in a Git repository. */ export interface FilepathUri { kind: 'uri' + /** + * A string that can be parsed as a URI, used to identify the directory in the client. + * The scheme of the URI could be 'file' or some other protocol to access the directory. + */ + baseDir: string + + /** + * A string that is a relative path to `baseDir`. + */ + filepath: string +} + +/** + * This is used for files not in a Git repository and not in the workspace. + * Also used for untitled files not saved. + */ +export interface FilepathAbsoluteUri { + kind: 'absolute-uri' + /** * A string that can be parsed as a URI, used to identify the file in the client. * The scheme of the URI could be: diff --git a/clients/vscode/src/chat/utils.ts b/clients/vscode/src/chat/utils.ts index 721468c01069..5c0c0fad6184 100644 --- a/clients/vscode/src/chat/utils.ts +++ b/clients/vscode/src/chat/utils.ts @@ -1,13 +1,6 @@ import path from "path"; import { TextEditor, Position as VSCodePosition, Range as VSCodeRange, Uri, workspace } from "vscode"; -import type { - Filepath, - Position as ChatPanelPosition, - LineRange, - PositionRange, - Location, - FilepathInGitRepository, -} from "tabby-chat-panel"; +import type { Filepath, Position as ChatPanelPosition, LineRange, PositionRange, Location } from "tabby-chat-panel"; import type { GitProvider } from "../git/GitProvider"; import { getLogger } from "../logger"; @@ -31,15 +24,12 @@ export function isValidForSyncActiveEditorSelection(editor: TextEditor): boolean } export function localUriToChatPanelFilepath(uri: Uri, gitProvider: GitProvider): Filepath { - let uriFilePath = uri.toString(true); - if (uri.scheme === DocumentSchemes.vscodeNotebookCell) { - const notebook = parseNotebookCellUri(uri); - if (notebook) { - // add fragment `#cell={number}` to filepath - uriFilePath = uri.with({ scheme: notebook.notebook.scheme, fragment: `cell=${notebook.handle}` }).toString(true); - } + let localUri = uri; + if (localUri.scheme === DocumentSchemes.vscodeNotebookCell) { + localUri = convertFromNotebookCellUri(localUri); } + const uriFilePath = uri.toString(true); const workspaceFolder = workspace.getWorkspaceFolder(uri); let repo = gitProvider.getRepository(uri); @@ -58,86 +48,57 @@ export function localUriToChatPanelFilepath(uri: Uri, gitProvider: GitProvider): } } + if (workspaceFolder) { + const baseDir = workspaceFolder.uri.toString(true); + const relativeFilePath = path.relative(baseDir, uriFilePath); + if (!relativeFilePath.startsWith("..")) { + return { + kind: "uri", + filepath: relativeFilePath, + baseDir: baseDir, + }; + } + } + return { - kind: "uri", + kind: "absolute-uri", uri: uriFilePath, }; } -function isJupyterNotebookFilepath(filepath: Filepath): boolean { - const _filepath = filepath.kind === "uri" ? filepath.uri : filepath.filepath; - const extname = path.extname(_filepath); - return extname.startsWith(".ipynb"); -} - export function chatPanelFilepathToLocalUri(filepath: Filepath, gitProvider: GitProvider): Uri | null { - const isNotebook = isJupyterNotebookFilepath(filepath); - - if (filepath.kind === "uri") { + let result: Uri | null = null; + if (filepath.kind === "absolute-uri") { try { - if (isNotebook) { - const handle = chatPanelFilePathToNotebookCellHandle(filepath.uri); - if (typeof handle === "number") { - return generateLocalNotebookCellUri(Uri.parse(filepath.uri), handle); - } - } - - return Uri.parse(filepath.uri, true); + result = Uri.parse(filepath.uri, true); } catch (e) { - // FIXME(@icycodes): this is a hack for uri is relative filepaths in workspaces - const workspaceRoot = workspace.workspaceFolders?.[0]; - if (workspaceRoot) { - return Uri.joinPath(workspaceRoot.uri, filepath.uri); + // do nothing + } + } else if (filepath.kind === "uri") { + try { + const workspaceFolder = workspace.getWorkspaceFolder(Uri.parse(filepath.baseDir, true)); + if (workspaceFolder) { + result = Uri.joinPath(workspaceFolder.uri, filepath.filepath); } + } catch (e) { + // do nothing } } else if (filepath.kind === "git") { const localGitRoot = gitProvider.findLocalRootUriByRemoteUrl(filepath.gitUrl); if (localGitRoot) { - // handling for Jupyter Notebook (.ipynb) files - if (isNotebook) { - return chatPanelFilepathToVscodeNotebookCellUri(localGitRoot, filepath); - } - - return Uri.joinPath(localGitRoot, filepath.filepath); + result = Uri.joinPath(localGitRoot, filepath.filepath); } } - logger.warn(`Invalid filepath params.`, filepath); - return null; -} - -function chatPanelFilepathToVscodeNotebookCellUri(root: Uri, filepath: FilepathInGitRepository): Uri | null { - if (filepath.kind !== "git") { - logger.warn(`Invalid filepath params.`, filepath); - return null; - } - - const filePathUri = Uri.parse(filepath.filepath); - const notebookUri = Uri.joinPath(root, filePathUri.path); - const handle = chatPanelFilePathToNotebookCellHandle(filepath.filepath); - if (typeof handle === "undefined") { + if (result == null) { logger.warn(`Invalid filepath params.`, filepath); return null; } - return generateLocalNotebookCellUri(notebookUri, handle); -} - -function chatPanelFilePathToNotebookCellHandle(filepath: string): number | undefined { - let handle: number | undefined; - - const fileUri = Uri.parse(filepath); - const fragment = fileUri.fragment; - const searchParams = new URLSearchParams(fragment); - if (searchParams.has("cell")) { - const cellString = searchParams.get("cell")?.toString() || ""; - handle = parseInt(cellString, 10); - } - if (typeof handle === "undefined" || isNaN(handle)) { - return undefined; + if (isJupyterNotebookFilepath(result)) { + result = convertToNotebookCellUri(result); } - - return handle; + return result; } export function vscodePositionToChatPanelPosition(position: VSCodePosition): ChatPanelPosition { @@ -191,10 +152,39 @@ export function chatPanelLocationToVSCodeRange(location: Location | undefined): return null; } +// Notebook URI utils +function isJupyterNotebookFilepath(uri: Uri): boolean { + const extname = path.extname(uri.fsPath); + return extname.startsWith(".ipynb"); +} + +function convertToNotebookCellUri(uri: Uri): Uri { + let handle: number | undefined; + + const searchParams = new URLSearchParams(uri.fragment); + const cellString = searchParams.get("cell"); + if (cellString) { + handle = parseInt(cellString, 10); + } + handle = handle || 0; + + searchParams.set("cell", handle.toString()); + return generateNotebookCellUri(uri, handle); +} + +function convertFromNotebookCellUri(uri: Uri): Uri { + const parsed = parseNotebookCellUri(uri); + if (!parsed) { + return uri; + } + return uri.with({ scheme: parsed.notebook.scheme, fragment: `cell=${parsed.handle}` }); +} + const nb_lengths = ["W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f"]; const nb_padRegexp = new RegExp(`^[${nb_lengths.join("")}]+`); const nb_radix = 7; -export function parseNotebookCellUri(cell: Uri): { notebook: Uri; handle: number } | undefined { + +function parseNotebookCellUri(cell: Uri): { notebook: Uri; handle: number } | undefined { if (cell.scheme !== DocumentSchemes.vscodeNotebookCell) { return undefined; } @@ -215,7 +205,7 @@ export function parseNotebookCellUri(cell: Uri): { notebook: Uri; handle: number }; } -export function generateLocalNotebookCellUri(notebook: Uri, handle: number): Uri { +function generateNotebookCellUri(notebook: Uri, handle: number): Uri { const s = handle.toString(nb_radix); const p = s.length < nb_lengths.length ? nb_lengths[s.length - 1] : "z"; const fragment = `${p}${s}s${Buffer.from(notebook.scheme).toString("base64")}`; diff --git a/ee/tabby-ui/components/message-markdown/index.tsx b/ee/tabby-ui/components/message-markdown/index.tsx index 1ba2c7aa7f85..27df1ef89031 100644 --- a/ee/tabby-ui/components/message-markdown/index.tsx +++ b/ee/tabby-ui/components/message-markdown/index.tsx @@ -9,7 +9,7 @@ import { MessageAttachmentClientCode } from '@/lib/gql/generates/graphql' import { AttachmentCodeItem, AttachmentDocItem, FileContext } from '@/lib/types' -import { cn } from '@/lib/utils' +import { cn, getFilepathFromContext } from '@/lib/utils' import { HoverCard, HoverCardContent, @@ -21,7 +21,6 @@ import './style.css' import { FileLocation, - Filepath, LookupSymbolHint, SymbolInfo } from 'tabby-chat-panel/index' @@ -175,26 +174,8 @@ export function MessageMarkdown({ setSymbolLocationMap(map => new Map(map.set(keyword, undefined))) const hints: LookupSymbolHint[] = [] if (activeSelection && activeSelection?.range) { - // FIXME(@icycodes): this is intended to convert the filepath to Filepath type - // We should remove this after FileContext.filepath use type Filepath instead of string - let filepath: Filepath - if ( - activeSelection.git_url.length > 1 && - !activeSelection.filepath.includes(':') - ) { - filepath = { - kind: 'git', - filepath: activeSelection.filepath, - gitUrl: activeSelection.git_url - } - } else { - filepath = { - kind: 'uri', - uri: activeSelection.filepath - } - } hints.push({ - filepath, + filepath: getFilepathFromContext(activeSelection), location: { start: activeSelection.range.start, end: activeSelection.range.end diff --git a/ee/tabby-ui/lib/utils/index.ts b/ee/tabby-ui/lib/utils/index.ts index 9b7f57702a50..77577b6dafd6 100644 --- a/ee/tabby-ui/lib/utils/index.ts +++ b/ee/tabby-ui/lib/utils/index.ts @@ -215,14 +215,22 @@ export function convertEditorContext( } } + // FIXME: improve this conversion const convertFilepath = (filepath: Filepath) => { - if (filepath.kind === 'uri') { + if (filepath.kind === 'absolute-uri') { return { filepath: filepath.uri, git_url: '' } } + if (filepath.kind === 'uri') { + return { + filepath: filepath.filepath, + git_url: '' + } + } + return { filepath: filepath.filepath, git_url: filepath.gitUrl @@ -237,6 +245,7 @@ export function convertEditorContext( } } +// FIXME: improve this conversion export function getFilepathFromContext(context: FileContext): Filepath { if (context.git_url.length > 1 && !context.filepath.includes(':')) { return { @@ -246,7 +255,7 @@ export function getFilepathFromContext(context: FileContext): Filepath { } } else { return { - kind: 'uri', + kind: 'absolute-uri', uri: context.filepath } }