Skip to content

Commit

Permalink
feat(ui): add code search functionality (#2587)
Browse files Browse the repository at this point in the history
* feat(ui): add code search functionality

* [autofix.ci] apply automated fixes

* update

* update: adjust style

* [autofix.ci] apply automated fixes

* add eventlistener

* display duration

* [autofix.ci] apply automated fixes

* using numeral

* update style of breadcrumb

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
liangfung and autofix-ci[bot] authored Jul 9, 2024
1 parent 15e82f2 commit aabc22e
Show file tree
Hide file tree
Showing 16 changed files with 694 additions and 53 deletions.
9 changes: 0 additions & 9 deletions ee/tabby-ui/app/files/components/code-editor-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,6 @@ const CodeEditorView: React.FC<CodeEditorViewProps> = ({ value, language }) => {

const extensions = React.useMemo(() => {
let result: Extension[] = [
EditorView.baseTheme({
'.cm-scroller': {
fontSize: '14px'
},
'.cm-gutters': {
backgroundColor: 'transparent',
borderRight: 'none'
}
}),
selectLinesGutter({
onSelectLine: v => {
if (v === -1 || isNaN(v)) return
Expand Down
120 changes: 120 additions & 0 deletions ee/tabby-ui/app/files/components/code-search-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from 'react'
import { useRouter, useSearchParams } from 'next/navigation'

import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { IconClose, IconSearch } from '@/components/ui/icons'
import { Input } from '@/components/ui/input'

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

interface CodeSearchBarProps extends React.OlHTMLAttributes<HTMLDivElement> {}

export const CodeSearchBar: React.FC<CodeSearchBarProps> = ({ className }) => {
const router = useRouter()
const searchParams = useSearchParams()
const { activeEntryInfo, activeRepo } = React.useContext(
SourceCodeBrowserContext
)
const [query, setQuery] = React.useState(searchParams.get('q')?.toString())
const inputRef = React.useRef<HTMLInputElement>(null)
const clearInput = () => {
setQuery('')
inputRef.current?.focus()
}

// shortcut 's'
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as Element
const tagName = target?.tagName?.toLowerCase()
if (
tagName === 'input' ||
tagName === 'textarea' ||
tagName === 'select'
) {
return
}

if (event.key === 's') {
event.preventDefault()
inputRef.current?.focus()
}
}

window.addEventListener('keydown', handleKeyDown)

return () => {
window.removeEventListener('keydown', handleKeyDown)
}
}, [])

const onSubmit: React.FormEventHandler<HTMLFormElement> = e => {
e.preventDefault()

if (!query) return

const pathname = generateEntryPath(
activeRepo,
activeEntryInfo?.rev,
'',
'search'
)

router.push(`/files/${pathname}?q=${encodeURIComponent(query)}`)
}

return (
<form
onSubmit={onSubmit}
className={cn(
'flex w-full shrink-0 items-center bg-background px-4 py-3.5 transition duration-500 ease-in-out',
className
)}
>
<div className="relative w-full">
<Input
ref={inputRef}
placeholder="Search for code..."
className="w-full"
autoComplete="off"
value={query}
onChange={e => setQuery(e.target.value)}
/>
<div className="absolute right-2 top-0 flex h-full items-center">
{query ? (
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6 cursor-pointer"
onClick={clearInput}
>
<IconClose />
</Button>
) : (
<kbd
className="rounded-md border bg-secondary/50 px-1.5 text-xs leading-4 text-muted-foreground shadow-[inset_-0.5px_-1.5px_0_hsl(var(--muted))]"
onClick={() => {
inputRef.current?.focus()
}}
>
s
</kbd>
)}
<div className="ml-2 flex items-center border-l border-l-border pl-2">
<Button
variant="ghost"
className="h-6 w-6 "
size="icon"
type="submit"
>
<IconSearch />
</Button>
</div>
</div>
</div>
</form>
)
}
97 changes: 97 additions & 0 deletions ee/tabby-ui/app/files/components/code-search-result-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use client'

import React from 'react'
import { useSearchParams } from 'next/navigation'
import humanizerDuration from 'humanize-duration'
import numeral from 'numeral'

import { GrepFile } from '@/lib/gql/generates/graphql'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'

import { SourceCodeSearchResult } from './code-search-result'

export interface SourceCodeSearchResult extends GrepFile {
blob: string
}

interface CodeSearchResultViewProps {
results?: GrepFile[]
requestDuration?: number
loading?: boolean
}

export const CodeSearchResultView = (props: CodeSearchResultViewProps) => {
const searchParams = useSearchParams()
const query = searchParams.get('q')?.toString() ?? ''

const results: SourceCodeSearchResult[] = React.useMemo(() => {
const _results = props.results
return (
_results?.map(item => ({
...item,
blob: item.lines.reduce((sum, cur) => {
return sum + (cur.line.text ?? '')
}, '')
})) ?? []
)
}, [props.results])

const matchCount: string = React.useMemo(() => {
let count = 0
if (!props.results) return '0'

for (const result of props.results) {
const curCount = result.lines.reduce((sum, cur) => {
const _matchCount = cur.subMatches.length
return sum + _matchCount
}, 0)

count += curCount
}
const format = count < 1000 ? '0' : '0.0a'
return numeral(count).format(format)
}, [props.results])

const duration = humanizerDuration.humanizer({
units: ['d', 'h', 'm', 's'],
spacer: '',
maxDecimalPoints: 2,
language: 'shortEn',
languages: {
shortEn: {
m: () => 'm',
s: () => 's'
}
}
})(props.requestDuration ?? 0)

return (
<>
{props.loading ? (
<CodeSearchSkeleton className="mt-3" />
) : (
<>
<h1 className="sticky top-0 z-20 bg-background pb-2 pt-1 font-semibold">
{matchCount} results in {duration}
</h1>
{results?.map((result, i) => (
<div key={`${result.path}-${i}`}>
<SourceCodeSearchResult result={result} query={query} />
</div>
))}
</>
)}
</>
)
}

function CodeSearchSkeleton({ className }: React.ComponentProps<'div'>) {
return (
<div className={cn('flex flex-col gap-3', className)}>
<Skeleton className="h-4 w-[20%]" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-8 w-full" />
</div>
)
}
Loading

0 comments on commit aabc22e

Please sign in to comment.