Skip to content

Commit

Permalink
feat(ui): support browsing different git tree in code browser (#2332)
Browse files Browse the repository at this point in the history
* feat(ui): support browsing different git tree in code browser

* update

* encode/decode rev

* expand keys

* repositorySearch

* key

* navigate

* viewmode

* fetch entries

* format

* error view

* update

* update

* using '/-/{viewMode}' as separator

* [autofix.ci] apply automated fixes

* encodeURIComponentIgnoringSlash

* update

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
liangfung and autofix-ci[bot] authored Jun 13, 2024
1 parent 6f46ac0 commit 8cb326d
Show file tree
Hide file tree
Showing 15 changed files with 981 additions and 679 deletions.
75 changes: 15 additions & 60 deletions ee/tabby-ui/app/files/components/chat-side-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,15 @@ import { useClient } from 'tabby-chat-panel/react'

import { useLatest } from '@/lib/hooks/use-latest'
import { useMe } from '@/lib/hooks/use-me'
import useRouterStuff from '@/lib/hooks/use-router-stuff'
import { useStore } from '@/lib/hooks/use-store'
import { useChatStore } from '@/lib/stores/chat-store'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { IconClose } from '@/components/ui/icons'
import { useTopbarProgress } from '@/components/topbar-progress-indicator'

import { QuickActionEventPayload } from '../lib/event-emitter'
import { SourceCodeBrowserContext, TFileMap } from './source-code-browser'
import {
fetchEntriesFromPath,
getDirectoriesFromBasename,
resolveFileNameFromPath,
resolveRepoSpecifierFromRepoInfo
} from './utils'
import { SourceCodeBrowserContext } from './source-code-browser'
import { resolveRepoSpecifierFromRepoInfo } from './utils'

interface ChatSideBarProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
Expand All @@ -29,19 +22,18 @@ export const ChatSideBar: React.FC<ChatSideBarProps> = ({
className,
...props
}) => {
const { setProgress } = useTopbarProgress()
const { updateSearchParams } = useRouterStuff()
const [{ data }] = useMe()
const {
pendingEvent,
setPendingEvent,
repoMap,
setExpandedKeys,
updateFileMap
activeRepoRef,
updateActivePath
} = React.useContext(SourceCodeBrowserContext)
const activeChatId = useStore(useChatStore, state => state.activeChatId)
const iframeRef = React.useRef<HTMLIFrameElement>(null)
const repoMapRef = useLatest(repoMap)
const latestRepoRef = useLatest(activeRepoRef)
const onNavigate = async (context: Context) => {
if (context?.filepath && context?.git_url) {
const repoMap = repoMapRef.current
Expand All @@ -52,55 +44,18 @@ export const ChatSideBar: React.FC<ChatSideBarProps> = ({
if (matchedRepositoryKey) {
const repository = repoMap[matchedRepositoryKey]
const repositorySpecifier = resolveRepoSpecifierFromRepoInfo(repository)
const fullPath = `${repositorySpecifier}/${context.filepath}`
if (!fullPath) return
try {
setProgress(true)
const entries = await fetchEntriesFromPath(
fullPath,
repositorySpecifier ? repoMap?.[repositorySpecifier] : undefined
)
const initialExpandedDirs = getDirectoriesFromBasename(
context.filepath
)
const rev = latestRepoRef?.current?.name ?? 'main'
const isFile = context.kind === 'file'

const patchMap: TFileMap = {}
// fetch dirs
for (const entry of entries) {
const path = `${repositorySpecifier}/${entry.basename}`
patchMap[path] = {
file: entry,
name: resolveFileNameFromPath(path),
fullPath: path,
treeExpanded: initialExpandedDirs.includes(entry.basename)
}
}
const expandedKeys = initialExpandedDirs.map(dir =>
[repositorySpecifier, dir].filter(Boolean).join('/')
)
if (patchMap) {
updateFileMap(patchMap)
}
if (expandedKeys?.length) {
setExpandedKeys(prevKeys => {
const newSet = new Set(prevKeys)
for (const k of expandedKeys) {
newSet.add(k)
}
return newSet
})
const fullPath = `${repositorySpecifier}/-/${
isFile ? 'blob' : 'tree'
}/${rev}/${context.filepath}`
if (!fullPath) return
updateActivePath(fullPath, {
params: {
line: String(context.range.start)
}
} catch (e) {
} finally {
updateSearchParams({
set: {
path: `${repositorySpecifier ?? ''}/${context.filepath}`,
line: String(context.range.start ?? '')
},
del: 'plain'
})
setProgress(false)
}
})
}
}
}
Expand Down
11 changes: 4 additions & 7 deletions ee/tabby-ui/app/files/components/code-editor-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import useRouterStuff from '@/lib/hooks/use-router-stuff'

import { emitter, LineMenuActionEventPayload } from '../lib/event-emitter'
import { resolveRepositoryInfoFromPath } from './utils'

interface CodeEditorViewProps {
value: string
Expand All @@ -43,13 +42,11 @@ const CodeEditorView: React.FC<CodeEditorViewProps> = ({ value, language }) => {
const line = searchParams.get('line')?.toString()
const [editorView, setEditorView] = React.useState<EditorView | null>(null)

const { isChatEnabled, activePath, fileMap } = React.useContext(
SourceCodeBrowserContext
)
const { repositorySpecifier, basename } =
resolveRepositoryInfoFromPath(activePath)
const { isChatEnabled, activePath, fileMap, activeEntryInfo } =
React.useContext(SourceCodeBrowserContext)
const { repositorySpecifier, rev, basename } = activeEntryInfo
const gitUrl = repositorySpecifier
? fileMap[repositorySpecifier]?.repository?.gitUrl ?? ''
? fileMap[`${repositorySpecifier}/${rev}`]?.repository?.gitUrl ?? ''
: ''

const extensions = React.useMemo(() => {
Expand Down
30 changes: 30 additions & 0 deletions ee/tabby-ui/app/files/components/error-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react'

import { cn } from '@/lib/utils'
import { IconFileSearch } from '@/components/ui/icons'

import { Errors } from './utils'

interface ErrorViewProps extends React.HTMLAttributes<HTMLDivElement> {
error: Error | undefined
}

export const ErrorView: React.FC<ErrorViewProps> = ({ className, error }) => {
const isEmptyRepository = error?.message === Errors?.EMPTY_REPOSITORY

let errorMessge = 'Not found'
if (isEmptyRepository) {
errorMessge = 'Empty repository'
}

return (
<div
className={cn('flex min-h-[30vh] items-center justify-center', className)}
>
<div className="flex flex-col items-center">
<IconFileSearch className="mb-2 h-10 w-10" />
<div className="text-2xl font-semibold">{errorMessge}</div>
</div>
</div>
)
}
83 changes: 54 additions & 29 deletions ee/tabby-ui/app/files/components/file-directory-breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,80 @@
import React from 'react'
import Link from 'next/link'

import { RepositoryKind } from '@/lib/gql/generates/graphql'
import { cn } from '@/lib/utils'
import { CopyButton } from '@/components/copy-button'

import { SourceCodeBrowserContext } from './source-code-browser'
import { resolveRepositoryInfoFromPath } from './utils'
import { generateEntryPath, resolveFileNameFromPath } from './utils'

interface FileDirectoryBreadcrumbProps
extends React.HTMLAttributes<HTMLDivElement> {}

const FileDirectoryBreadcrumb: React.FC<FileDirectoryBreadcrumbProps> = ({
className
}) => {
const { currentFileRoutes, setActivePath, activePath } = React.useContext(
SourceCodeBrowserContext
)
const basename = React.useMemo(
() => resolveRepositoryInfoFromPath(activePath)?.basename,
[activePath]
)
const { currentFileRoutes, activeRepo, activeRepoRef, activeEntryInfo } =
React.useContext(SourceCodeBrowserContext)
const basename = activeEntryInfo?.basename
const routes: Array<{
name: string
href: string
kind?: RepositoryKind
}> = React.useMemo(() => {
const basename = activeEntryInfo?.basename
let result = [
{
name: activeEntryInfo?.repositoryName ?? '',
href: generateEntryPath(activeRepo, activeRepoRef?.name, '', 'dir')
}
]

if (basename) {
const pathSegments = basename?.split('/') || []
for (let i = 0; i < pathSegments.length; i++) {
const p = pathSegments.slice(0, i + 1).join('/')
const name = resolveFileNameFromPath(p)
result.push({
name: decodeURIComponent(name),
href: generateEntryPath(activeRepo, activeRepoRef?.name, p, 'dir')
})
}
}

return result
}, [activeEntryInfo, activeRepo, activeRepoRef])

return (
<div className={cn('flex flex-nowrap items-center gap-1', className)}>
<div className="flex items-center gap-1 overflow-x-auto leading-8">
<div
<Link
className="cursor-pointer font-medium text-primary hover:underline"
onClick={e => setActivePath(undefined)}
href="/files"
>
Repositories
</div>
</Link>
<div>/</div>
{currentFileRoutes?.map((route, idx) => {
const isRepo = idx === 0 && currentFileRoutes?.length > 1
const isActiveFile = idx === currentFileRoutes.length - 1
{routes?.map((route, idx) => {
const isRepo = idx === 0 && routes?.length > 1
const isActiveFile = idx === routes.length - 1
const classname = cn(
'whitespace-nowrap',
isRepo || isActiveFile ? 'font-bold' : 'font-medium',
isActiveFile ? '' : 'cursor-pointer text-primary hover:underline',
isRepo ? 'hover:underline' : undefined
)

return (
<React.Fragment key={route.fullPath}>
<div
className={cn(
'whitespace-nowrap',
isRepo || isActiveFile ? 'font-bold' : 'font-medium',
isActiveFile
? ''
: 'cursor-pointer text-primary hover:underline',
isRepo ? 'hover:underline' : undefined
)}
onClick={e => setActivePath(route.fullPath)}
>
{route.name}
</div>
{route.file.kind !== 'file' && <div>/</div>}
<React.Fragment key={route.href}>
{isActiveFile ? (
<div className={classname}>{route.name}</div>
) : (
<Link className={classname} href={`/files/${route.href}`}>
{route.name}
</Link>
)}
{!isActiveFile && <div>/</div>}
</React.Fragment>
)
})}
Expand Down
Loading

0 comments on commit 8cb326d

Please sign in to comment.