Skip to content

Commit

Permalink
feat(vscode): adding clickble symbol render in chat panel (#3420)
Browse files Browse the repository at this point in the history
* feat: add onRenderLsp event handler for rendering language server protocol

* feat: Add onRenderLsp event handler for rendering language server protocol

* chore(vscode): update targetFile path to use workspace relative path

* refactor: Update code to use KeywordInfo type for onRenderLsp event handler

* feat: add onNavigateSymbol method to ClientApi interface

* feat: add onNavigateSymbol method to ClientApi interface

* feat: add onNavigateSymbol method to ClientApi interface

* feat: add onHoverSymbol method to ClientApi interface

* feat: add onHoverSymbol and findSymbolInfo methods to WebviewHelper

* feat: add onHoverSymbol and findSymbolInfo methods to WebviewHelper

* fix: update onNavigateSymbol parameter name in ClientApi interface

* fix: update onNavigateSymbol parameter name in ClientApi interface

* feat: Add support for onNavigateSymbol and onHoverSymbol in ChatPage

The code changes in `ChatPage` component add support for the `onNavigateSymbol` and `onHoverSymbol` capabilities. These capabilities are checked and set using the `hasCapability` method of the server. The `onNavigateSymbol` and `onHoverSymbol` methods are conditionally used based on the support for these capabilities. This change enhances the functionality of the ChatPage component in the Tabby UI.

* chore: remove unused type

* feat: Update ClientApi interface to make onNavigateSymbol optional

The ClientApi interface has been updated to make the onNavigateSymbol method optional. This change allows for better flexibility in implementing the interface, as the onNavigateSymbol method is now conditionally used based on the support for the capability. This update enhances the usability of the ClientApi interface in the Tabby UI.

* feat: Add support for onHoverSymbol in ChatPage

The code changes in `ChatPage` component add support for the `onHoverSymbol` capability. This capability is checked and set using the `hasCapability` method of the server. The `onHoverSymbol` method is conditionally used based on the support for this capability. This change enhances the functionality of the ChatPage component in the Tabby UI.

* [autofix.ci] apply automated fixes

* feat: Add activeSelection prop to MessageMarkdown and update imports

* feat: Rename parameter in onNavigateSymbol to hintFilepaths for clarity

* feat: Implement CodeElement component for rendering code in Markdown

* refactor: Remove onNavigateToContext prop from MessageMarkdown and related components for simplification

* feat: Rename onNavigateSymbol to onLookupSymbol and update its signature for improved clarity

* feat: Rename onNavigateSymbol to onLookupSymbol and refactor symbol lookup logic for improved clarity and maintainability

* feat: Rename onNavigateSymbol to onLookupSymbol and update related logic for consistency across components

* [autofix.ci] apply automated fixes

* update: render symbol

* Update icons.tsx

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: liangfung <[email protected]>
Co-authored-by: Meng Zhang <[email protected]>
  • Loading branch information
4 people authored Dec 8, 2024
1 parent 5b6c845 commit c61a9ee
Show file tree
Hide file tree
Showing 13 changed files with 320 additions and 71 deletions.
12 changes: 11 additions & 1 deletion clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,14 @@ 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<void>
Expand All @@ -77,6 +84,8 @@ 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<SymbolInfo | undefined>
}

export interface ClientApi extends ClientApiMethods {
Expand Down Expand Up @@ -119,6 +128,7 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods):
onLoaded: api.onLoaded,
onCopy: api.onCopy,
onKeyboardEvent: api.onKeyboardEvent,
onLookupSymbol: api.onLookupSymbol,
},
})
}
Expand Down
64 changes: 63 additions & 1 deletion clients/vscode/src/chat/WebviewHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {
Webview,
ColorThemeKind,
ProgressLocation,
commands,
LocationLink,
workspace,
} from "vscode";
import type { ServerApi, ChatMessage, Context, NavigateOpts, OnLoadedParams } from "tabby-chat-panel";
import type { ServerApi, ChatMessage, Context, NavigateOpts, OnLoadedParams, SymbolInfo } from "tabby-chat-panel";
import { TABBY_CHAT_PANEL_API_VERSION } from "tabby-chat-panel";
import hashObject from "object-hash";
import * as semver from "semver";
Expand All @@ -21,6 +24,7 @@ import { createClient } from "./chatPanel";
import { Client as LspClient } from "../lsp/Client";
import { isBrowser } from "../env";
import { getFileContextFromSelection, showFileContext } from "./fileContext";
import path from "path";

export class WebviewHelper {
webview?: Webview;
Expand Down Expand Up @@ -384,6 +388,9 @@ export class WebviewHelper {
}

public createChatClient(webview: Webview) {
/*
utility functions for createClient
*/
const getIndentInfo = (document: TextDocument, selection: Selection) => {
// Determine the indentation for the content
// The calculation is based solely on the indentation of the first line
Expand Down Expand Up @@ -547,6 +554,61 @@ 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<SymbolInfo | undefined> => {
const findSymbolInfo = async (filepaths: string[], keyword: string): Promise<SymbolInfo | undefined> => {
if (!keyword || !filepaths.length) {
this.logger.info("No keyword or filepaths provided");
return undefined;
}
try {
const workspaceRoot = workspace.workspaceFolders?.[0];
if (!workspaceRoot) {
this.logger.error("No workspace folder found");
return undefined;
}
const rootPath = workspaceRoot.uri;
for (const filepath of filepaths) {
const normalizedPath = filepath.startsWith("/") ? filepath.slice(1) : filepath;
const fullPath = path.join(rootPath.path, normalizedPath);
const fileUri = Uri.file(fullPath);
const document = await workspace.openTextDocument(fileUri);
const content = document.getText();
let pos = 0;
while ((pos = content.indexOf(keyword, pos)) !== -1) {
const position = document.positionAt(pos);
const locations = await commands.executeCommand<LocationLink[]>(
"vscode.executeDefinitionProvider",
fileUri,
position,
);
if (locations && locations.length > 0) {
const location = locations[0];
if (location) {
const targetPath = location.targetUri.fsPath;
const relativePath = path.relative(rootPath.path, targetPath);
const normalizedTargetPath = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;

return {
sourceFile: filepath,
sourceLine: position.line + 1,
sourceCol: position.character,
targetFile: normalizedTargetPath,
targetLine: location.targetRange.start.line + 1,
targetCol: location.targetRange.start.character,
};
}
}
pos += keyword.length;
}
}
} catch (error) {
this.logger.error("Error in findSymbolInfo:", error);
}
return undefined;
};

return await findSymbolInfo(hintFilepaths, keyword);
},
});
}
}
1 change: 1 addition & 0 deletions clients/vscode/src/chat/chatPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi
onLoaded: api.onLoaded,
onCopy: api.onCopy,
onKeyboardEvent: api.onKeyboardEvent,
onLookupSymbol: api.onLookupSymbol,
},
});
}
6 changes: 6 additions & 0 deletions ee/tabby-ui/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default function ChatPage() {
// server feature support check
const [supportsOnApplyInEditorV2, setSupportsOnApplyInEditorV2] =
useState(false)
const [supportsOnLookupSymbol, setSupportsOnLookupSymbol] = useState(false)

const sendMessage = (message: ChatMessage) => {
if (chatRef.current) {
Expand Down Expand Up @@ -236,6 +237,7 @@ export default function ChatPage() {
server
?.hasCapability('onApplyInEditorV2')
.then(setSupportsOnApplyInEditorV2)
server?.hasCapability('onLookupSymbol').then(setSupportsOnLookupSymbol)
}

checkCapabilities()
Expand Down Expand Up @@ -388,6 +390,10 @@ export default function ChatPage() {
: server?.onApplyInEditor)
}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
onLookupSymbol={
isInEditor &&
(supportsOnLookupSymbol ? server?.onLookupSymbol : undefined)
}
/>
</ErrorBoundary>
)
Expand Down
5 changes: 4 additions & 1 deletion ee/tabby-ui/app/files/components/chat-side-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ export const ChatSideBar: React.FC<ChatSideBarProps> = ({
onApplyInEditor(_content) {},
onLoaded() {},
onCopy(_content) {},
onKeyboardEvent() {}
onKeyboardEvent() {},
async onLookupSymbol(_filepath, _keywords) {
return undefined
}
})

const getPrompt = ({ action }: QuickActionEventPayload) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export function AssistantMessageSection({
onUpdateMessage
} = useContext(SearchContext)

const { supportsOnApplyInEditorV2 } = useContext(ChatContext)
const { supportsOnApplyInEditorV2, onNavigateToContext } =
useContext(ChatContext)

const [isEditing, setIsEditing] = useState(false)
const [showMoreSource, setShowMoreSource] = useState(false)
Expand Down Expand Up @@ -374,6 +375,7 @@ export function AssistantMessageSection({
fetchingContextInfo={fetchingContextInfo}
canWrapLongLines={!isLoading}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
onNavigateToContext={onNavigateToContext}
/>
{/* if isEditing, do not display error message block */}
{message.error && <ErrorMessageBlock error={message.error} />}
Expand Down
17 changes: 16 additions & 1 deletion ee/tabby-ui/components/chat/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React, { RefObject } from 'react'
import { compact, findIndex, isEqual, some, uniqWith } from 'lodash-es'
import type { Context, FileContext, NavigateOpts } from 'tabby-chat-panel'
import type {
Context,
FileContext,
NavigateOpts,
SymbolInfo
} from 'tabby-chat-panel'

import { ERROR_CODE_NOT_FOUND } from '@/lib/constants'
import {
Expand Down Expand Up @@ -46,6 +51,10 @@ type ChatContextValue = {
onApplyInEditor?:
| ((content: string) => void)
| ((content: string, opts?: { languageId: string; smart: boolean }) => void)
onLookupSymbol?: (
filepaths: string[],
keyword: string
) => Promise<SymbolInfo | undefined>
relevantContext: Context[]
activeSelection: Context | null
removeRelevantContext: (index: number) => void
Expand Down Expand Up @@ -84,6 +93,10 @@ interface ChatProps extends React.ComponentProps<'div'> {
onApplyInEditor?:
| ((content: string) => void)
| ((content: string, opts?: { languageId: string; smart: boolean }) => void)
onLookupSymbol?: (
filepaths: string[],
keyword: string
) => Promise<SymbolInfo | undefined>
chatInputRef: RefObject<HTMLTextAreaElement>
supportsOnApplyInEditorV2: boolean
}
Expand All @@ -105,6 +118,7 @@ function ChatRenderer(
onCopyContent,
onSubmitMessage,
onApplyInEditor,
onLookupSymbol,
chatInputRef,
supportsOnApplyInEditorV2
}: ChatProps,
Expand Down Expand Up @@ -531,6 +545,7 @@ function ChatRenderer(
container,
onCopyContent,
onApplyInEditor,
onLookupSymbol,
relevantContext,
removeRelevantContext,
chatInputRef,
Expand Down
4 changes: 4 additions & 0 deletions ee/tabby-ui/components/chat/question-answer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ function AssistantMessageCard(props: AssistantMessageCardProps) {
onNavigateToContext,
onApplyInEditor,
onCopyContent,
onLookupSymbol,
supportsOnApplyInEditorV2
} = React.useContext(ChatContext)
const [relevantCodeHighlightIndex, setRelevantCodeHighlightIndex] =
Expand Down Expand Up @@ -404,7 +405,10 @@ function AssistantMessageCard(props: AssistantMessageCardProps) {
onCodeCitationMouseEnter={onCodeCitationMouseEnter}
onCodeCitationMouseLeave={onCodeCitationMouseLeave}
canWrapLongLines={!isLoading}
onLookupSymbol={onLookupSymbol}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
activeSelection={userMessage.activeContext}
onNavigateToContext={onNavigateToContext}
/>
{!!message.error && <ErrorMessageBlock error={message.error} />}
</>
Expand Down
113 changes: 113 additions & 0 deletions ee/tabby-ui/components/message-markdown/code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { ReactNode, useContext, useEffect } from 'react'
import { Element } from 'react-markdown/lib/ast-to-react'

import { cn } from '@/lib/utils'

import { CodeBlock } from '../ui/codeblock'
import { IconSquareChevronRight } from '../ui/icons'
import { MessageMarkdownContext } from './markdown-context'

export interface CodeElementProps {
node: Element
inline?: boolean
className?: string
children: ReactNode & ReactNode[]
}

/**
* Code element in Markdown AST.
*/
export function CodeElement({
inline,
className,
children,
...props
}: CodeElementProps) {
const {
lookupSymbol,
canWrapLongLines,
onApplyInEditor,
onCopyContent,
supportsOnApplyInEditorV2,
onNavigateToContext,
symbolPositionMap
} = useContext(MessageMarkdownContext)

const keyword = children[0]?.toString()
const symbolLocation = keyword ? symbolPositionMap.get(keyword) : undefined

useEffect(() => {
if (!inline || !lookupSymbol || !keyword) return
lookupSymbol(keyword)
}, [inline, keyword, lookupSymbol])

if (children.length) {
if (children[0] === '▍') {
return <span className="mt-1 animate-pulse cursor-default"></span>
}
children[0] = (children[0] as string).replace('`▍`', '▍')
}

if (inline) {
const isSymbolNavigable = Boolean(symbolLocation)

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
}
)
}

return (
<code
className={cn(
'group/symbol inline-flex flex-nowrap items-center gap-1',
className,
{
symbol: !!lookupSymbol,
'bg-muted leading-5': !isSymbolNavigable,
'cursor-pointer hover:bg-muted/50 border': isSymbolNavigable
}
)}
onClick={handleClick}
{...props}
>
{isSymbolNavigable && (
<IconSquareChevronRight className="h-3.5 w-3.5 text-primary" />
)}
<span
className={cn('self-baseline', {
'group-hover/symbol:text-primary': isSymbolNavigable
})}
>
{children}
</span>
</code>
)
}

const match = /language-(\w+)/.exec(className || '')
return (
<CodeBlock
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
onApplyInEditor={onApplyInEditor}
onCopyContent={onCopyContent}
canWrapLongLines={canWrapLongLines}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
/>
)
}
Loading

0 comments on commit c61a9ee

Please sign in to comment.