Skip to content

Commit

Permalink
feat(definitions): add lookup definitions functionality and filtering…
Browse files Browse the repository at this point in the history
… logic
  • Loading branch information
Sma1lboy committed Dec 30, 2024
1 parent 2ce8dc4 commit 1b12c7b
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 39 deletions.
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;
}
}
140 changes: 104 additions & 36 deletions clients/vscode/src/chat/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ProgressLocation,
Location,
LocationLink,
Position,
} from "vscode";
import { TABBY_CHAT_PANEL_API_VERSION } from "tabby-chat-panel";
import type {
Expand All @@ -25,6 +26,7 @@ import type {
SymbolInfo,
FileLocation,
GitRepository,
LookupDefinitionsHint,
} from "tabby-chat-panel";
import * as semver from "semver";
import type { StatusInfo, Config } from "tabby-agent";
Expand All @@ -35,15 +37,15 @@ import { isBrowser } from "../env";
import { getLogger } from "../logger";
import { getFileContextFromSelection } from "./fileContext";
import {
localUriToChatPanelFilepath,
chatPanelFilepathToLocalUri,
vscodePositionToChatPanelPosition,
vscodeRangeToChatPanelPositionRange,
chatPanelLocationToVSCodeRange,
isValidForSyncActiveEditorSelection,
convertDefinitionToSymbolInfo,
getDefinitionLocations,
} from "./utils";
import mainHtml from "./html/main.html";
import errorHtml from "./html/error.html";
import { filterSymbolInfosByContextAndOverlap } from "./definitions";

export class ChatWebview {
private readonly logger = getLogger("ChatWebView");
Expand Down Expand Up @@ -184,6 +186,14 @@ export class ChatWebview {
}
}

private async getDefinitionLocations(uri: Uri, position: Position) {
return await commands.executeCommand<Location[] | LocationLink[]>(
"vscode.executeDefinitionProvider",
uri,
position,
);
}

private createChatPanelApiClient(): ServerApi | undefined {
const webview = this.webview;
if (!webview) {
Expand Down Expand Up @@ -310,40 +320,13 @@ export class ChatWebview {
while ((match = matchRegExp.exec(content)) !== null) {
const offset = offsetInDocument + match.index;
const position = document.positionAt(offset);
const locations = await commands.executeCommand<Location[] | LocationLink[]>(
"vscode.executeDefinitionProvider",
document.uri,
position,
);
if (locations && locations.length > 0) {
const location = locations[0];
if (location) {
if ("targetUri" in location) {
const targetLocation = location.targetSelectionRange ?? location.targetRange;
return {
source: {
filepath: localUriToChatPanelFilepath(document.uri, this.gitProvider),
location: vscodePositionToChatPanelPosition(position),
},
target: {
filepath: localUriToChatPanelFilepath(location.targetUri, this.gitProvider),
location: vscodeRangeToChatPanelPositionRange(targetLocation),
},
};
} else if ("uri" in location) {
return {
source: {
filepath: localUriToChatPanelFilepath(document.uri, this.gitProvider),
location: vscodePositionToChatPanelPosition(position),
},
target: {
filepath: localUriToChatPanelFilepath(location.uri, this.gitProvider),
location: vscodeRangeToChatPanelPositionRange(location.range),
},
};
}
}
// get definitions
const locations = await this.getDefinitionLocations(document.uri, position);
if (!locations || locations.length === 0 || !locations[0]) {
continue;
}

return convertDefinitionToSymbolInfo(document, position, locations[0], this.gitProvider);
}
return undefined;
};
Expand Down Expand Up @@ -448,8 +431,93 @@ export class ChatWebview {
}
return infoList;
},

lookupDefinitions: async (context: LookupDefinitionsHint): Promise<SymbolInfo[]> => {
if (!context?.filepath) {
this.logger.info("lookupDefinitions: Missing filepath in context.");
return [];
}

// convert ChatPanel filepath to a local URI
const uri = chatPanelFilepathToLocalUri(context.filepath, this.gitProvider);
if (!uri) {
this.logger.info("lookupDefinitions: Could not resolve local URI for:", context.filepath);
return [];
}

// open the document
let document;
try {
document = await workspace.openTextDocument(uri);
} catch (e) {
this.logger.info("lookupDefinitions: Can't open file:", uri);
return [];
}

// determine the snippet range and get its text
const snippetRange = this.getSnippetRange(document, context);
const snippetText = document.getText(snippetRange);

// split the text into words
const words = snippetText.split(/\b/);

// Use an offset accumulator to track each word's position
let offset = 0;

// Map each word to an async definition lookup
const tasks = words.map((rawWord) => {
const currentOffset = offset;
offset += rawWord.length;

const trimmedWord = rawWord.trim();
if (!trimmedWord || trimmedWord.match(/^\W+$/)) {
return Promise.resolve<SymbolInfo[]>([]);
}

const position = document.positionAt(document.offsetAt(snippetRange.start) + currentOffset);

return getDefinitionLocations(document.uri, position)
.then((definitions) => {
if (!definitions || definitions.length === 0) {
return [];
}
const result: SymbolInfo[] = [];
definitions.forEach((def) => {
const info = convertDefinitionToSymbolInfo(document, position, def, this.gitProvider);
if (info) {
result.push(info);
}
});
return result;
})
.catch((err) => {
this.logger.error(`lookupDefinitions: DefinitionProvider error: ${err}`);
return [];
});
});

// await all lookups in parallel and flatten the results
const symbolInfosArrays = await Promise.all(tasks);
const symbolInfos = symbolInfosArrays.flat();

// filter and merge final results
return filterSymbolInfosByContextAndOverlap(symbolInfos, context);
},
});
}
/**
* Helper: decide snippet range from context.location or entire doc.
*/
private getSnippetRange(document: TextDocument, context: LookupDefinitionsHint): Range {
if (!context.location) {
return new Range(0, 0, document.lineCount, 0);
}
const vsRange = chatPanelLocationToVSCodeRange(context.location);
if (!vsRange || vsRange.isEmpty) {
return new Range(0, 0, document.lineCount, 0);
}
return vsRange;
}

private checkStatusAndLoadContent() {
const statusInfo = this.lspClient.status.current;
Expand Down

0 comments on commit 1b12c7b

Please sign in to comment.