Skip to content
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
44 changes: 39 additions & 5 deletions clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createThreadFromIframe, createThreadFromInsideIframe } from 'tabby-threads'
import {
createThreadFromIframe,
createThreadFromInsideIframe,
} from 'tabby-threads'
import { version } from '../package.json'

export const TABBY_CHAT_PANEL_API_VERSION: string = version
Expand Down Expand Up @@ -181,6 +184,21 @@ export interface LookupSymbolHint {
location?: Location
}

/**
* Represents a hint to help find definitions.
*/
export interface LookupDefinitionsHint {
/**
* The filepath of the file to search the symbol.
*/
filepath?: Filepath

/**
* Using LineRange to confirm the specific code block of the file
*/
location?: LineRange
}

/**
* Includes information about a symbol returned by the {@link ClientApiMethods.lookupSymbol} method.
*/
Expand Down Expand Up @@ -209,7 +227,11 @@ export interface GitRepository {
* - 'generate-docs': Generate documentation for the selected code.
* - 'generate-tests': Generate tests for the selected code.
*/
export type ChatCommand = 'explain' | 'fix' | 'generate-docs' | 'generate-tests'
export type ChatCommand =
| 'explain'
| 'fix'
| 'generate-docs'
| 'generate-tests'

export interface ServerApi {
init: (request: InitRequest) => void
Expand Down Expand Up @@ -248,15 +270,21 @@ export interface ClientApiMethods {
// On user copy content to clipboard.
onCopy: (content: string) => void

onKeyboardEvent: (type: 'keydown' | 'keyup' | 'keypress', event: KeyboardEventInit) => void
onKeyboardEvent: (
type: 'keydown' | 'keyup' | 'keypress',
event: KeyboardEventInit
) => void

/**
* Find the target symbol and return the symbol information.
* @param symbol The symbol to find.
* @param hints The optional {@link LookupSymbolHint} list to help find the symbol. The hints should be sorted by priority.
* @returns The symbol information if found, otherwise undefined.
*/
lookupSymbol?: (symbol: string, hints?: LookupSymbolHint[] | undefined) => Promise<SymbolInfo | undefined>
lookupSymbol?: (
symbol: string,
hints?: LookupSymbolHint[] | undefined
) => Promise<SymbolInfo | undefined>

/**
* Open the target file location in the editor.
Expand All @@ -273,6 +301,8 @@ export interface ClientApiMethods {

// Provide all repos found in workspace folders.
readWorkspaceGitRepositories?: () => Promise<GitRepository[]>

lookupDefinitions?: (hint: LookupDefinitionsHint) => Promise<SymbolInfo[]>
}

export interface ClientApi extends ClientApiMethods {
Expand All @@ -284,7 +314,10 @@ export interface ClientApi extends ClientApiMethods {
hasCapability: (method: keyof ClientApiMethods) => Promise<boolean>
}

export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): ServerApi {
export function createClient(
target: HTMLIFrameElement,
api: ClientApiMethods,
): ServerApi {
return createThreadFromIframe(target, {
expose: {
refresh: api.refresh,
Expand All @@ -297,6 +330,7 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods):
openInEditor: api.openInEditor,
openExternal: api.openExternal,
readWorkspaceGitRepositories: api.readWorkspaceGitRepositories,
lookupDefinitions: api.lookupDefinitions,
},
})
}
Expand Down
1 change: 1 addition & 0 deletions clients/vscode/src/chat/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi
openInEditor: api.openInEditor,
openExternal: api.openExternal,
readWorkspaceGitRepositories: api.readWorkspaceGitRepositories,
lookupDefinitions: api.lookupDefinitions,
},
});
}
82 changes: 82 additions & 0 deletions clients/vscode/src/chat/definitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { LookupDefinitionsHint, SymbolInfo } from "tabby-chat-panel/index";
import {
chatPanelLocationToVSCodeRange,
getActualChatPanelFilepath,
vscodeRangeToChatPanelPositionRange,
} from "./utils";
import { Range as VSCodeRange } from "vscode";

/**
* Filters out SymbolInfos whose target is inside the given context range,
* and merges overlapping target ranges in the same file.
*/
export function filterSymbolInfosByContextAndOverlap(
symbolInfos: SymbolInfo[],
context: LookupDefinitionsHint | undefined,
): SymbolInfo[] {
if (!symbolInfos.length) {
return [];
}

// Filter out target inside context
let filtered = symbolInfos;
if (context?.location) {
const contextRange = chatPanelLocationToVSCodeRange(context.location);
const contextPath = context.filepath ? getActualChatPanelFilepath(context.filepath) : undefined;
if (contextRange && contextPath) {
filtered = filtered.filter((symbolInfo) => {
const targetPath = getActualChatPanelFilepath(symbolInfo.target.filepath);
if (targetPath !== contextPath) {
return true;
}
// Check if target is outside contextRange
const targetRange = chatPanelLocationToVSCodeRange(symbolInfo.target.location);
if (!targetRange) {
return true;
}
return targetRange.end.isBefore(contextRange.start) || targetRange.start.isAfter(contextRange.end);
});
}
}

// Merge overlapping target ranges in same file
const merged: SymbolInfo[] = [];
for (const current of filtered) {
const currentUri = getActualChatPanelFilepath(current.target.filepath);
const currentRange = chatPanelLocationToVSCodeRange(current.target.location);
if (!currentRange) {
merged.push(current);
continue;
}

// Try find a previously added symbol that is in the same file and has overlap
let hasMerged = false;
for (const existing of merged) {
const existingUri = getActualChatPanelFilepath(existing.target.filepath);
if (existingUri !== currentUri) {
continue;
}
const existingRange = chatPanelLocationToVSCodeRange(existing.target.location);
if (!existingRange) {
continue;
}
// Check overlap
const isOverlap = !(
currentRange.end.isBefore(existingRange.start) || currentRange.start.isAfter(existingRange.end)
);
if (isOverlap) {
// Merge
const newStart = currentRange.start.isBefore(existingRange.start) ? currentRange.start : existingRange.start;
const newEnd = currentRange.end.isAfter(existingRange.end) ? currentRange.end : existingRange.end;
const mergedRange = new VSCodeRange(newStart, newEnd);
existing.target.location = vscodeRangeToChatPanelPositionRange(mergedRange);
hasMerged = true;
break;
}
}
if (!hasMerged) {
merged.push(current);
}
}
return merged;
}
83 changes: 80 additions & 3 deletions clients/vscode/src/chat/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import path from "path";
import { TextEditor, Position as VSCodePosition, Range as VSCodeRange, Uri, workspace } from "vscode";
import {
TextEditor,
Position as VSCodePosition,
Range as VSCodeRange,
Uri,
workspace,
TextDocument,
commands,
LocationLink,
Location as VSCodeLocation,
} from "vscode";
import type {
Filepath,
Position as ChatPanelPosition,
LineRange,
PositionRange,
Location,
Location as ChatPanelLocation,
FilepathInGitRepository,
SymbolInfo,
} from "tabby-chat-panel";
import type { GitProvider } from "../git/GitProvider";
import { getLogger } from "../logger";
Expand Down Expand Up @@ -170,7 +181,7 @@ export function chatPanelLineRangeToVSCodeRange(lineRange: LineRange): VSCodeRan
return new VSCodeRange(Math.max(0, lineRange.start - 1), 0, lineRange.end, 0);
}

export function chatPanelLocationToVSCodeRange(location: Location | undefined): VSCodeRange | null {
export function chatPanelLocationToVSCodeRange(location: ChatPanelLocation | undefined): VSCodeRange | null {
if (!location) {
return null;
}
Expand Down Expand Up @@ -221,3 +232,69 @@ export function generateLocalNotebookCellUri(notebook: Uri, handle: number): Uri
const fragment = `${p}${s}s${Buffer.from(notebook.scheme).toString("base64")}`;
return notebook.with({ scheme: DocumentSchemes.vscodeNotebookCell, fragment });
}

/**
* Calls the built-in VSCode definition provider and returns an array of definitions
* (Location or LocationLink).
*/
export async function getDefinitionLocations(
uri: Uri,
position: VSCodePosition,
): Promise<(VSCodeLocation | LocationLink)[]> {
const results = await commands.executeCommand<VSCodeLocation[] | LocationLink[]>(
"vscode.executeDefinitionProvider",
uri,
position,
);
return results ?? [];
}

/**
* Converts a single VS Code Definition result (Location or LocationLink)
* into a SymbolInfo object for the chat panel.
*/
export function convertDefinitionToSymbolInfo(
document: TextDocument,
position: VSCodePosition,
definition: VSCodeLocation | LocationLink,
gitProvider: GitProvider,
): SymbolInfo | undefined {
let targetUri: Uri | undefined;
let targetRange: VSCodeRange | undefined;

if ("targetUri" in definition) {
// LocationLink
targetUri = definition.targetUri;
targetRange = definition.targetSelectionRange ?? definition.targetRange;
} else {
// Location
targetUri = definition.uri;
targetRange = definition.range;
}

if (!targetUri || !targetRange) {
return undefined;
}

return {
source: {
filepath: localUriToChatPanelFilepath(document.uri, gitProvider),
location: vscodePositionToChatPanelPosition(position),
},
target: {
filepath: localUriToChatPanelFilepath(targetUri, gitProvider),
location: vscodeRangeToChatPanelPositionRange(targetRange),
},
};
}

/**
* Gets the string path (either from 'kind=git' or 'kind=uri').
*/
export function getActualChatPanelFilepath(filepath: Filepath): string {
if (filepath.kind === "git") {
return filepath.filepath;
} else {
return filepath.uri;
}
}
Loading
Loading