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

feat(tabby-panel): adding onLookup Definitions api to collect relevant declaration context #3546

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
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