From c90db2f9c1cc9239b649abc4d9add7c4a463d5f5 Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Fri, 13 Dec 2024 10:13:41 +0800 Subject: [PATCH] fix(chat-panel): improve the implement of lookup symbol. (#3551) * fix(chat-panel): improve the implement of lookup symbol. * fix(chat-panel): fix missing methods in createClient. * fix(chat-panel): fix filepath convert string to type Filepath. --- clients/tabby-chat-panel/package.json | 2 +- clients/tabby-chat-panel/src/index.ts | 173 +++++++++++++++--- clients/vscode/src/chat/WebviewHelper.ts | 170 +++++++++++++---- clients/vscode/src/chat/chatPanel.ts | 3 +- clients/vscode/src/chat/fileContext.ts | 1 + clients/vscode/src/chat/utils.ts | 102 +++++++++++ ee/tabby-ui/app/chat/page.tsx | 5 +- .../app/files/components/chat-side-bar.tsx | 4 +- .../components/assistant-message-section.tsx | 1 - ee/tabby-ui/components/chat/chat.tsx | 14 +- .../components/chat/question-answer.tsx | 3 +- .../components/message-markdown/code.tsx | 26 +-- .../components/message-markdown/index.tsx | 50 +++-- .../message-markdown/markdown-context.tsx | 9 +- 14 files changed, 453 insertions(+), 110 deletions(-) create mode 100644 clients/vscode/src/chat/utils.ts diff --git a/clients/tabby-chat-panel/package.json b/clients/tabby-chat-panel/package.json index 27775eac49c7..ca4726646d4f 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.3.0", + "version": "0.4.0", "keywords": [], "sideEffects": false, "exports": { diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 0cfcfc43a0dc..da6a46aa0512 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -3,11 +3,54 @@ import { version } from '../package.json' export const TABBY_CHAT_PANEL_API_VERSION: string = version +/** + * Represents a position in a file. + */ +export interface Position { + /** + * 1-based line number + */ + line: number + /** + * 1-based character number + */ + character: number +} + +/** + * Represents a range in a file. + */ +export interface PositionRange { + /** + * The start position of the range. + */ + start: Position + /** + * The end position of the range. + */ + end: Position +} + +/** + * Represents a range of lines in a file. + */ export interface LineRange { + /** + * 1-based line number + */ start: number + /** + * 1-based line number + */ end: number } +/** + * Represents a location in a file. + * It could be a 1-based line number, a line range, a position or a position range. + */ +export type Location = number | LineRange | Position | PositionRange + export interface FileContext { kind: 'file' range: LineRange @@ -44,6 +87,96 @@ export interface NavigateOpts { openInEditor?: boolean } +/** + * Represents a filepath to identify a file. + */ +export type Filepath = FilepathInGitRepository | FilepathUri + +/** + * This is used for files in a Git repository, and should be used in priority. + */ +export interface FilepathInGitRepository { + kind: 'git' + + /** + * A string that is a relative path in the repository. + */ + filepath: string + + /** + * A URL used to identify the Git repository in both the client and server. + * The URL should be valid for use as a git remote URL, for example: + * 1. 'https://github.com/TabbyML/tabby' + * 2. 'git://github.com/TabbyML/tabby.git' + */ + gitUrl: string + + /** + * An optional Git revision which the file is at. + */ + revision?: string +} + +/** + * This is used for files not in a Git repository. + */ +export interface FilepathUri { + kind: '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: + * - 'untitled' means a new file not saved. + * - 'file', 'vscode-vfs' or some other protocol to access the file. + */ + uri: string +} + +/** + * Represents a file and a location in it. + */ +export interface FileLocation { + /** + * The filepath of the file. + */ + filepath: Filepath + + /** + * The location in the file. + * It could be a 1-based line number, a line range, a position or a position range. + */ + location: Location +} + +/** + * Represents a hint to help find a symbol. + */ +export interface LookupSymbolHint { + /** + * The filepath of the file to search the symbol. + */ + filepath?: Filepath + + /** + * The location in the file to search the symbol. + */ + location?: Location +} + +/** + * Includes information about a symbol returned by the {@link ClientApiMethods.lookupSymbol} method. + */ +export interface SymbolInfo { + /** + * Where the symbol is found. + */ + source: FileLocation + /** + * The target location to navigate to when the symbol is clicked. + */ + target: FileLocation +} + export interface ServerApi { init: (request: InitRequest) => void sendMessage: (message: ChatMessage) => void @@ -53,14 +186,7 @@ export interface ServerApi { updateTheme: (style: string, themeClass: string) => void updateActiveSelection: (context: Context | null) => void } -export interface SymbolInfo { - sourceFile: string - sourceLine: number - sourceCol: number - targetFile: string - targetLine: number - targetCol: number -} + export interface ClientApiMethods { navigate: (context: Context, opts?: NavigateOpts) => void refresh: () => Promise @@ -84,8 +210,20 @@ export interface ClientApiMethods { onKeyboardEvent: (type: 'keydown' | 'keyup' | 'keypress', event: KeyboardEventInit) => void - // find symbol definition location by hint filepaths and keyword - onLookupSymbol?: (hintFilepaths: string[], keyword: string) => Promise + /** + * 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 + + /** + * Open the target file location in the editor. + * @param target The target file location to open. + * @returns Whether the file location is opened successfully. + */ + openInEditor: (target: FileLocation) => Promise } export interface ClientApi extends ClientApiMethods { @@ -94,17 +232,6 @@ export interface ClientApi extends ClientApiMethods { hasCapability: (method: keyof ClientApiMethods) => Promise } -export const clientApiKeys: (keyof ClientApiMethods)[] = [ - 'navigate', - 'refresh', - 'onSubmitMessage', - 'onApplyInEditor', - 'onApplyInEditorV2', - 'onLoaded', - 'onCopy', - 'onKeyboardEvent', -] - export interface ChatMessage { message: string @@ -125,10 +252,12 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): refresh: api.refresh, onSubmitMessage: api.onSubmitMessage, onApplyInEditor: api.onApplyInEditor, + onApplyInEditorV2: api.onApplyInEditorV2, onLoaded: api.onLoaded, onCopy: api.onCopy, onKeyboardEvent: api.onKeyboardEvent, - onLookupSymbol: api.onLookupSymbol, + lookupSymbol: api.lookupSymbol, + openInEditor: api.openInEditor, }, }) } diff --git a/clients/vscode/src/chat/WebviewHelper.ts b/clients/vscode/src/chat/WebviewHelper.ts index 3bfa516f83d3..c8edb5d0775e 100644 --- a/clients/vscode/src/chat/WebviewHelper.ts +++ b/clients/vscode/src/chat/WebviewHelper.ts @@ -4,15 +4,26 @@ import { env, TextEditor, window, + Range, Selection, TextDocument, Webview, ColorThemeKind, ProgressLocation, commands, + Location, LocationLink, } from "vscode"; -import type { ServerApi, ChatMessage, Context, NavigateOpts, OnLoadedParams, SymbolInfo } from "tabby-chat-panel"; +import type { + ServerApi, + ChatMessage, + Context, + NavigateOpts, + OnLoadedParams, + LookupSymbolHint, + SymbolInfo, + FileLocation, +} from "tabby-chat-panel"; import { TABBY_CHAT_PANEL_API_VERSION } from "tabby-chat-panel"; import hashObject from "object-hash"; import * as semver from "semver"; @@ -23,6 +34,13 @@ import { createClient } from "./chatPanel"; import { Client as LspClient } from "../lsp/Client"; import { isBrowser } from "../env"; import { getFileContextFromSelection, showFileContext, openTextDocument } from "./fileContext"; +import { + localUriToChatPanelFilepath, + chatPanelFilepathToLocalUri, + vscodePositionToChatPanelPosition, + vscodeRangeToChatPanelPositionRange, + chatPanelLocationToVSCodeRange, +} from "./utils"; export class WebviewHelper { webview?: Webview; @@ -552,51 +570,129 @@ export class WebviewHelper { this.logger.debug(`Dispatching keyboard event: ${type} ${JSON.stringify(event)}`); this.webview?.postMessage({ action: "dispatchKeyboardEvent", type, event }); }, - onLookupSymbol: async (hintFilepaths: string[], keyword: string): Promise => { - const findSymbolInfo = async (filepaths: string[], keyword: string): Promise => { - if (!keyword || !filepaths.length) { - this.logger.info("No keyword or filepaths provided"); - return undefined; + lookupSymbol: async (symbol: string, hints?: LookupSymbolHint[] | undefined): Promise => { + if (!symbol.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) { + // Do not process invalid symbols + return undefined; + } + /// FIXME: When no hints provided, try to use `vscode.executeWorkspaceSymbolProvider` to find the symbol. + + // Find the symbol in the hints + for (const hint of hints ?? []) { + if (!hint.filepath) { + this.logger.debug("No filepath in the hint:", hint); + continue; } - try { - for (const filepath of filepaths) { - const document = await openTextDocument({ filePath: filepath }, this.gitProvider); - if (!document) { - this.logger.info(`File not found: ${filepath}`); - continue; - } - const content = document.getText(); - let pos = 0; - while ((pos = content.indexOf(keyword, pos)) !== -1) { - const position = document.positionAt(pos); - const locations = await commands.executeCommand( - "vscode.executeDefinitionProvider", - document.uri, - position, - ); - if (locations && locations.length > 0) { - const location = locations[0]; - if (location) { + const uri = chatPanelFilepathToLocalUri(hint.filepath, this.gitProvider); + if (!uri) { + continue; + } + const document = await openTextDocument({ filePath: uri.toString(true) }, this.gitProvider); + if (!document) { + continue; + } + + const findSymbolInContent = async ( + content: string, + offsetInDocument: number, + ): Promise => { + // Add word boundary to perform exact match + const matchRegExp = new RegExp(`\\b${symbol}\\b`, "g"); + let match; + while ((match = matchRegExp.exec(content)) !== null) { + const offset = offsetInDocument + match.index; + const position = document.positionAt(offset); + const locations = await commands.executeCommand( + "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 { - sourceFile: filepath, - sourceLine: position.line + 1, - sourceCol: position.character, - targetFile: location.targetUri.toString(true), - targetLine: location.targetRange.start.line + 1, - targetCol: location.targetRange.start.character, + source: { + filepath: localUriToChatPanelFilepath(document.uri, this.gitProvider), + location: vscodePositionToChatPanelPosition(position), + }, + target: { + filepath: localUriToChatPanelFilepath(location.uri, this.gitProvider), + location: vscodeRangeToChatPanelPositionRange(location.range), + }, }; } } - pos += keyword.length; } } - } catch (error) { - this.logger.error("Error in findSymbolInfo:", error); + return undefined; + }; + + let symbolInfo: SymbolInfo | undefined; + if (hint.location) { + // Find in the hint location + const location = chatPanelLocationToVSCodeRange(hint.location); + if (location) { + let range: Range; + if (!location.isEmpty) { + range = location; + } else { + // a empty range, create a new range with this line to the end of the file + range = new Range(location.start.line, 0, document.lineCount, 0); + } + const content = document.getText(range); + const offset = document.offsetAt(range.start); + symbolInfo = await findSymbolInContent(content, offset); + } } - return undefined; - }; + if (!symbolInfo) { + // Fallback to find in full content + const content = document.getText(); + symbolInfo = await findSymbolInContent(content, 0); + } + if (symbolInfo) { + // Symbol found + this.logger.debug( + `Symbol found: ${symbol} with hints: ${JSON.stringify(hints)}: ${JSON.stringify(symbolInfo)}`, + ); + return symbolInfo; + } + } + this.logger.debug(`Symbol not found: ${symbol} with hints: ${JSON.stringify(hints)}`); + return undefined; + }, + openInEditor: async (fileLocation: FileLocation): Promise => { + const uri = chatPanelFilepathToLocalUri(fileLocation.filepath, this.gitProvider); + if (!uri) { + return false; + } - return await findSymbolInfo(hintFilepaths, keyword); + const targetRange = chatPanelLocationToVSCodeRange(fileLocation.location) ?? new Range(0, 0, 0, 0); + try { + await commands.executeCommand( + "editor.action.goToLocations", + uri, + targetRange.start, + [new Location(uri, targetRange)], + "goto", + ); + return true; + } catch (error) { + this.logger.error("Failed to go to location:", fileLocation, error); + return false; + } }, }); } diff --git a/clients/vscode/src/chat/chatPanel.ts b/clients/vscode/src/chat/chatPanel.ts index 3ef44a34a236..0d3c50de1582 100644 --- a/clients/vscode/src/chat/chatPanel.ts +++ b/clients/vscode/src/chat/chatPanel.ts @@ -33,7 +33,8 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi onLoaded: api.onLoaded, onCopy: api.onCopy, onKeyboardEvent: api.onKeyboardEvent, - onLookupSymbol: api.onLookupSymbol, + lookupSymbol: api.lookupSymbol, + openInEditor: api.openInEditor, }, }); } diff --git a/clients/vscode/src/chat/fileContext.ts b/clients/vscode/src/chat/fileContext.ts index 4a4f175366cb..8cf6dbb50fce 100644 --- a/clients/vscode/src/chat/fileContext.ts +++ b/clients/vscode/src/chat/fileContext.ts @@ -166,6 +166,7 @@ export async function openTextDocument( } } + logger.warn(`File not found: ${filePath}`); return null; } diff --git a/clients/vscode/src/chat/utils.ts b/clients/vscode/src/chat/utils.ts new file mode 100644 index 000000000000..1721277345a9 --- /dev/null +++ b/clients/vscode/src/chat/utils.ts @@ -0,0 +1,102 @@ +import path from "path"; +import { Position as VSCodePosition, Range as VSCodeRange, Uri, workspace } from "vscode"; +import type { Filepath, Position as ChatPanelPosition, LineRange, PositionRange, Location } from "tabby-chat-panel"; +import type { GitProvider } from "../git/GitProvider"; +import { getLogger } from "../logger"; + +const logger = getLogger("chat/utils"); + +export function localUriToChatPanelFilepath(uri: Uri, gitProvider: GitProvider): Filepath { + const workspaceFolder = workspace.getWorkspaceFolder(uri); + + let repo = gitProvider.getRepository(uri); + if (!repo && workspaceFolder) { + repo = gitProvider.getRepository(workspaceFolder.uri); + } + const gitRemoteUrl = repo ? gitProvider.getDefaultRemoteUrl(repo) : undefined; + + if (repo && gitRemoteUrl) { + const relativeFilePath = path.relative(repo.rootUri.toString(true), uri.toString(true)); + if (!relativeFilePath.startsWith("..")) { + return { + kind: "git", + filepath: relativeFilePath, + gitUrl: gitRemoteUrl, + }; + } + } + + return { + kind: "uri", + uri: uri.toString(true), + }; +} + +export function chatPanelFilepathToLocalUri(filepath: Filepath, gitProvider: GitProvider): Uri | null { + if (filepath.kind === "uri") { + try { + return 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); + } + } + } else if (filepath.kind === "git") { + const localGitRoot = gitProvider.findLocalRootUriByRemoteUrl(filepath.gitUrl); + if (localGitRoot) { + return Uri.joinPath(localGitRoot, filepath.filepath); + } + } + logger.warn(`Invalid filepath params.`, filepath); + return null; +} + +export function vscodePositionToChatPanelPosition(position: VSCodePosition): ChatPanelPosition { + return { + line: position.line + 1, + character: position.character + 1, + }; +} + +export function chatPanelPositionToVSCodePosition(position: ChatPanelPosition): VSCodePosition { + return new VSCodePosition(Math.max(0, position.line - 1), Math.max(0, position.character - 1)); +} + +export function vscodeRangeToChatPanelPositionRange(range: VSCodeRange): PositionRange { + return { + start: vscodePositionToChatPanelPosition(range.start), + end: vscodePositionToChatPanelPosition(range.end), + }; +} + +export function chatPanelPositionRangeToVSCodeRange(positionRange: PositionRange): VSCodeRange { + return new VSCodeRange( + chatPanelPositionToVSCodePosition(positionRange.start), + chatPanelPositionToVSCodePosition(positionRange.end), + ); +} + +export function chatPanelLineRangeToVSCodeRange(lineRange: LineRange): VSCodeRange { + // Do not minus 1 from end line number, as we want to include the last line. + return new VSCodeRange(Math.max(0, lineRange.start - 1), 0, lineRange.end, 0); +} + +export function chatPanelLocationToVSCodeRange(location: Location): VSCodeRange | null { + if (typeof location === "number") { + const position = new VSCodePosition(Math.max(0, location - 1), 0); + return new VSCodeRange(position, position); + } else if ("line" in location) { + const position = chatPanelPositionToVSCodePosition(location); + return new VSCodeRange(position, position); + } else if ("start" in location) { + if (typeof location.start === "number") { + return chatPanelLineRangeToVSCodeRange(location as LineRange); + } else { + return chatPanelPositionRangeToVSCodeRange(location as PositionRange); + } + } + logger.warn(`Invalid location params.`, location); + return null; +} diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index 2c5da6d76dbd..48287de58c56 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -237,7 +237,7 @@ export default function ChatPage() { server ?.hasCapability('onApplyInEditorV2') .then(setSupportsOnApplyInEditorV2) - server?.hasCapability('onLookupSymbol').then(setSupportsOnLookupSymbol) + server?.hasCapability('lookupSymbol').then(setSupportsOnLookupSymbol) } checkCapabilities() @@ -392,8 +392,9 @@ export default function ChatPage() { supportsOnApplyInEditorV2={supportsOnApplyInEditorV2} onLookupSymbol={ isInEditor && - (supportsOnLookupSymbol ? server?.onLookupSymbol : undefined) + (supportsOnLookupSymbol ? server?.lookupSymbol : undefined) } + openInEditor={isInEditor && server?.openInEditor} /> ) diff --git a/ee/tabby-ui/app/files/components/chat-side-bar.tsx b/ee/tabby-ui/app/files/components/chat-side-bar.tsx index 824140f4c94c..f8f673dfae95 100644 --- a/ee/tabby-ui/app/files/components/chat-side-bar.tsx +++ b/ee/tabby-ui/app/files/components/chat-side-bar.tsx @@ -80,8 +80,8 @@ export const ChatSideBar: React.FC = ({ onLoaded() {}, onCopy(_content) {}, onKeyboardEvent() {}, - async onLookupSymbol(_filepath, _keywords) { - return undefined + async openInEditor() { + return false } }) diff --git a/ee/tabby-ui/app/search/components/assistant-message-section.tsx b/ee/tabby-ui/app/search/components/assistant-message-section.tsx index ee47857140bc..d4af5ab3a092 100644 --- a/ee/tabby-ui/app/search/components/assistant-message-section.tsx +++ b/ee/tabby-ui/app/search/components/assistant-message-section.tsx @@ -382,7 +382,6 @@ export function AssistantMessageSection({ fetchingContextInfo={fetchingContextInfo} canWrapLongLines={!isLoading} supportsOnApplyInEditorV2={supportsOnApplyInEditorV2} - onNavigateToContext={onNavigateToContext} /> {/* if isEditing, do not display error message block */} {message.error && } diff --git a/ee/tabby-ui/components/chat/chat.tsx b/ee/tabby-ui/components/chat/chat.tsx index 03212c6c3295..31852e599a7d 100644 --- a/ee/tabby-ui/components/chat/chat.tsx +++ b/ee/tabby-ui/components/chat/chat.tsx @@ -3,6 +3,8 @@ import { compact, findIndex, isEqual, some, uniqWith } from 'lodash-es' import type { Context, FileContext, + FileLocation, + LookupSymbolHint, NavigateOpts, SymbolInfo } from 'tabby-chat-panel' @@ -52,9 +54,10 @@ type ChatContextValue = { | ((content: string) => void) | ((content: string, opts?: { languageId: string; smart: boolean }) => void) onLookupSymbol?: ( - filepaths: string[], - keyword: string + symbol: string, + hints?: LookupSymbolHint[] | undefined ) => Promise + openInEditor?: (target: FileLocation) => void relevantContext: Context[] activeSelection: Context | null removeRelevantContext: (index: number) => void @@ -94,9 +97,10 @@ interface ChatProps extends React.ComponentProps<'div'> { | ((content: string) => void) | ((content: string, opts?: { languageId: string; smart: boolean }) => void) onLookupSymbol?: ( - filepaths: string[], - keyword: string + symbol: string, + hints?: LookupSymbolHint[] | undefined ) => Promise + openInEditor?: (target: FileLocation) => void chatInputRef: RefObject supportsOnApplyInEditorV2: boolean } @@ -119,6 +123,7 @@ function ChatRenderer( onSubmitMessage, onApplyInEditor, onLookupSymbol, + openInEditor, chatInputRef, supportsOnApplyInEditorV2 }: ChatProps, @@ -546,6 +551,7 @@ function ChatRenderer( onCopyContent, onApplyInEditor, onLookupSymbol, + openInEditor, relevantContext, removeRelevantContext, chatInputRef, diff --git a/ee/tabby-ui/components/chat/question-answer.tsx b/ee/tabby-ui/components/chat/question-answer.tsx index 5c1de52133a9..f9dd02821ae9 100644 --- a/ee/tabby-ui/components/chat/question-answer.tsx +++ b/ee/tabby-ui/components/chat/question-answer.tsx @@ -262,6 +262,7 @@ function AssistantMessageCard(props: AssistantMessageCardProps) { onApplyInEditor, onCopyContent, onLookupSymbol, + openInEditor, supportsOnApplyInEditorV2 } = React.useContext(ChatContext) const [relevantCodeHighlightIndex, setRelevantCodeHighlightIndex] = @@ -406,9 +407,9 @@ function AssistantMessageCard(props: AssistantMessageCardProps) { onCodeCitationMouseLeave={onCodeCitationMouseLeave} canWrapLongLines={!isLoading} onLookupSymbol={onLookupSymbol} + openInEditor={openInEditor} supportsOnApplyInEditorV2={supportsOnApplyInEditorV2} activeSelection={userMessage.activeContext} - onNavigateToContext={onNavigateToContext} /> {!!message.error && } diff --git a/ee/tabby-ui/components/message-markdown/code.tsx b/ee/tabby-ui/components/message-markdown/code.tsx index f6759acdc2bb..f907b78b0c93 100644 --- a/ee/tabby-ui/components/message-markdown/code.tsx +++ b/ee/tabby-ui/components/message-markdown/code.tsx @@ -25,16 +25,16 @@ export function CodeElement({ }: CodeElementProps) { const { lookupSymbol, + openInEditor, canWrapLongLines, onApplyInEditor, onCopyContent, supportsOnApplyInEditorV2, - onNavigateToContext, symbolPositionMap } = useContext(MessageMarkdownContext) const keyword = children[0]?.toString() - const symbolLocation = keyword ? symbolPositionMap.get(keyword) : undefined + const symbolInfo = keyword ? symbolPositionMap.get(keyword) : undefined useEffect(() => { if (!inline || !lookupSymbol || !keyword) return @@ -49,26 +49,12 @@ export function CodeElement({ } if (inline) { - const isSymbolNavigable = Boolean(symbolLocation) + const isSymbolNavigable = Boolean(symbolInfo?.target) const handleClick = () => { - if (!isSymbolNavigable || !symbolLocation || !onNavigateToContext) return - - onNavigateToContext( - { - filepath: symbolLocation.targetFile, - range: { - start: symbolLocation.targetLine, - end: symbolLocation.targetLine - }, - git_url: '', - content: '', - kind: 'file' - }, - { - openInEditor: true - } - ) + if (isSymbolNavigable && openInEditor && symbolInfo?.target) { + openInEditor(symbolInfo.target) + } } return ( diff --git a/ee/tabby-ui/components/message-markdown/index.tsx b/ee/tabby-ui/components/message-markdown/index.tsx index c93fc3d8647d..935ed8bed849 100644 --- a/ee/tabby-ui/components/message-markdown/index.tsx +++ b/ee/tabby-ui/components/message-markdown/index.tsx @@ -20,9 +20,10 @@ import { MemoizedReactMarkdown } from '@/components/markdown' import './style.css' import { - Context, FileContext, - NavigateOpts, + FileLocation, + Filepath, + LookupSymbolHint, SymbolInfo } from 'tabby-chat-panel/index' @@ -62,10 +63,10 @@ export interface MessageMarkdownProps { opts?: { languageId: string; smart: boolean } ) => void onLookupSymbol?: ( - filepaths: string[], - keyword: string + symbol: string, + hints?: LookupSymbolHint[] | undefined ) => Promise - onNavigateToContext?: (context: Context, opts?: NavigateOpts) => void + openInEditor?: (target: FileLocation) => void onCodeCitationClick?: (code: AttachmentCodeItem) => void onCodeCitationMouseEnter?: (index: number) => void onCodeCitationMouseLeave?: (index: number) => void @@ -91,9 +92,9 @@ export function MessageMarkdown({ className, canWrapLongLines, onLookupSymbol, + openInEditor, supportsOnApplyInEditorV2, activeSelection, - onNavigateToContext, ...rest }: MessageMarkdownProps) { const [symbolPositionMap, setSymbolLocationMap] = useState< @@ -173,10 +174,35 @@ export function MessageMarkdown({ if (symbolPositionMap.has(keyword)) return setSymbolLocationMap(map => new Map(map.set(keyword, undefined))) - const symbolInfo = await onLookupSymbol( - activeSelection?.filepath ? [activeSelection?.filepath] : [], - keyword - ) + const hints: LookupSymbolHint[] = [] + if (activeSelection) { + // 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, + location: { + start: activeSelection.range.start, + end: activeSelection.range.end + } + }) + } + const symbolInfo = await onLookupSymbol(keyword, hints) setSymbolLocationMap(map => new Map(map.set(keyword, symbolInfo))) } @@ -193,9 +219,9 @@ export function MessageMarkdown({ canWrapLongLines: !!canWrapLongLines, supportsOnApplyInEditorV2, activeSelection, - onNavigateToContext, symbolPositionMap, - lookupSymbol: onLookupSymbol ? lookupSymbol : undefined + lookupSymbol: onLookupSymbol ? lookupSymbol : undefined, + openInEditor }} > void supportsOnApplyInEditorV2: boolean activeSelection?: FileContext symbolPositionMap: Map + openInEditor?: (target: FileLocation) => void lookupSymbol?: (keyword: string) => void }