Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(chat-panel): update typedef of filepath. #3710

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion clients/tabby-chat-panel/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tabby-chat-panel",
"type": "module",
"version": "0.5.0",
"version": "0.6.0",
"keywords": [],
"sideEffects": false,
"exports": {
Expand Down
23 changes: 21 additions & 2 deletions clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
142 changes: 66 additions & 76 deletions clients/vscode/src/chat/utils.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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")}`;
Expand Down
23 changes: 2 additions & 21 deletions ee/tabby-ui/components/message-markdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,7 +21,6 @@ import './style.css'

import {
FileLocation,
Filepath,
LookupSymbolHint,
SymbolInfo
} from 'tabby-chat-panel/index'
Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions ee/tabby-ui/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -246,7 +255,7 @@ export function getFilepathFromContext(context: FileContext): Filepath {
}
} else {
return {
kind: 'uri',
kind: 'absolute-uri',
uri: context.filepath
}
}
Expand Down
Loading