Skip to content

Commit

Permalink
feat(ui): SearchResultList treeItem component
Browse files Browse the repository at this point in the history
  • Loading branch information
SoonIter committed Feb 12, 2024
1 parent b9cef41 commit e77a290
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 17 deletions.
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export type Definition = {
id: string
}
reload: {}
openFile: {
filePath: string
locationsToSelect?: {
start: {
column: number
line: number
}
end: {
column: number
line: number
}
}
}
}
}

Expand Down
38 changes: 37 additions & 1 deletion src/view.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ParentPort, SgSearch } from './types'
import type { Definition, ParentPort, SgSearch } from './types'
import { execa } from 'execa'
import { Unport, ChannelMessage } from 'unport'
import * as vscode from 'vscode'
Expand Down Expand Up @@ -95,6 +95,42 @@ class SearchSidebarProvider implements vscode.WebviewViewProvider {
this.search.updateResult(res)
parentPort.postMessage('search', { ...payload, searchResult: res })
})

parentPort.onMessage('openFile', async payload => this.openFile(payload))
}

private openFile = ({
filePath,
locationsToSelect
}: Definition['child2parent']['openFile']) => {
const uris = workspace.workspaceFolders
const { joinPath } = vscode.Uri

if (!uris?.length) {
return
}

const fileUri: vscode.Uri = joinPath(uris?.[0].uri, filePath)
let range: undefined | vscode.Range
if (locationsToSelect) {
const { start, end } = locationsToSelect
range = new vscode.Range(
new vscode.Position(start.line, start.column),
new vscode.Position(end.line, end.column)
)
}

vscode.workspace.openTextDocument(fileUri).then(
async (textDoc: vscode.TextDocument) => {
return vscode.window.showTextDocument(textDoc, {
selection: range
})
},
(error: any) => {
console.error('error opening file', filePath)
console.error(error)
}
)
}

private getHtmlForWebview(webview: vscode.Webview) {
Expand Down
108 changes: 108 additions & 0 deletions src/webview/SearchSidebar/SearchResultList/comps/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Box } from '@chakra-ui/react'
import React, { useMemo } from 'react'
import type { SgSearch } from '../../../../types'
import { openFile } from '../../postMessage'

type HighLightToken = {
start: {
line: number
column: number
}
end: {
line: number
column: number
}
style: React.CSSProperties
}

function splitByHighLightTokens(tokens: HighLightToken[]) {
const codeSegments = tokens
.map(({ start, end, style }) => {
// TODO: multilines highlight
const { column: startColumn } = start
const { column: endColumn } = end

const startIdx = startColumn
const endIdx = endColumn

return {
range: [startIdx, endIdx],
style
}
})
.sort((a, b) => a.range[0] - b.range[0])

return codeSegments
}

interface CodeBlockProps {
match: SgSearch
}
export const CodeBlock = ({ match }: CodeBlockProps) => {
const { file, lines, range } = match
const matchHighlight = [
{
start: {
line: range.start.line,
column: range.start.column
},
end: {
line: range.end.line,
column: range.end.column
},
style: {
backgroundColor: 'var(--vscode-editor-findMatchHighlightBackground)',
border: '1px solid var(--vscode-editor-findMatchHighlightBackground)'
}
}
]

const codeSegments = useMemo(() => {
return splitByHighLightTokens(matchHighlight)
}, [lines, matchHighlight])

if (codeSegments.length === 0) {
return (
<Box
flex="1"
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="pre"
fontSize="13px"
lineHeight="22px"
height="22px"
>
{lines}
</Box>
)
}

const highlightStartIdx = codeSegments[0].range[0]
const highlightEndIdx = codeSegments[codeSegments.length - 1].range[1]
return (
<Box
flex="1"
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="pre"
fontSize="13px"
lineHeight="22px"
height="22px"
cursor="pointer"
onClick={() => {
openFile({ filePath: file, locationsToSelect: match.range })
}}
>
{highlightStartIdx <= 0 ? '' : lines.slice(0, highlightStartIdx)}
{codeSegments.map(({ range, style }) => {
const [start, end] = range
return (
<span style={style} key={`${start}-${end}`}>
{lines.slice(start, end)}
</span>
)
})}
{highlightEndIdx >= lines.length ? '' : lines.slice(highlightEndIdx)}
</Box>
)
}
34 changes: 34 additions & 0 deletions src/webview/SearchSidebar/SearchResultList/comps/FileLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Link, Text } from '@chakra-ui/react'
import { openFile } from '../../postMessage'

interface FileLinkProps {
filePath: string
}

export const FileLink = ({ filePath }: FileLinkProps) => {
return (
<Link
onClick={e => {
e.stopPropagation()
openFile({
filePath
})
}}
fontWeight="500"
display="inline-flex"
cursor="pointer"
>
<Text
size="13"
style={{
textAlign: 'left',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden'
}}
>
{filePath}
</Text>
</Link>
)
}
69 changes: 69 additions & 0 deletions src/webview/SearchSidebar/SearchResultList/comps/TreeItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { HStack, IconButton, VStack, Box } from '@chakra-ui/react'
import { HiOutlineChevronDown, HiOutlineChevronRight } from 'react-icons/hi'
import { useBoolean } from 'react-use'
import type { SgSearch } from '../../../../types'
import { CodeBlock } from './CodeBlock'
import { FileLink } from './FileLink'

interface TreeItemProps {
filePath: string
matches: SgSearch[]
}

const TreeItem = ({ filePath, matches }: TreeItemProps) => {
const [isExpanded, toggleIsExpanded] = useBoolean(true)

return (
<VStack w="100%" pl="10" p="2" gap="0">
<HStack
w="100%"
onClick={toggleIsExpanded}
cursor="pointer"
_hover={{
background: 'var(--vscode-list-inactiveSelectionBackground)'
}}
>
<IconButton
flex={0}
color="var(--vscode-foreground)"
background="transparent"
aria-label="expand/collapse button"
pointerEvents="none"
icon={
isExpanded ? <HiOutlineChevronDown /> : <HiOutlineChevronRight />
}
mr="2"
/>
<HStack
flex={1}
gap="4"
px="2"
alignItems="center"
h="22px"
lineHeight="22px"
>
<FileLink filePath={filePath} />
<div>{matches.length.toString()}</div>
</HStack>
</HStack>

<VStack w="100%" alignItems="flex-start" gap="0">
{isExpanded &&
matches?.map(match => {
const { file, range } = match
return (
<HStack
w="100%"
justifyContent="flex-start"
key={file + range.start.line + range.start.column}
>
<Box w="20px" />
<CodeBlock match={match} />
</HStack>
)
})}
</VStack>
</VStack>
)
}
export default TreeItem
32 changes: 29 additions & 3 deletions src/webview/SearchSidebar/SearchResultList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
import { useMemo } from 'react'
import { SgSearch } from '../../../types'
import TreeItem from './comps/TreeItem'
import { Box } from '@chakra-ui/react'

type SearchResultListProps = {
matches: Array<SgSearch>
pattern: string
}

const SearchResultList = ({ matches }: SearchResultListProps) => {
// TODO
return <pre>{JSON.stringify(matches, null, 2)}</pre>
const displayLimit = 2000
const SearchResultList = ({ matches, pattern }: SearchResultListProps) => {
const groupedByFile = useMemo(() => {
return matches.slice(0, displayLimit).reduce(
(groups, match) => {
if (!groups[match.file]) {
groups[match.file] = []
}

groups[match.file].push(match)

return groups
},
{} as Record<string, SgSearch[]>
)
}, [matches])

return (
<Box mt="10">
{Object.entries(groupedByFile).map(([filePath, match]) => {
return <TreeItem filePath={filePath} matches={match} key={filePath} />
})}
</Box>
)
}

export default SearchResultList
11 changes: 7 additions & 4 deletions src/webview/SearchSidebar/SearchWidgetContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@ const SearchWidgetContainer = ({
return (
<HStack position="relative">
<Center
_hover={{
background: 'var(--vscode-list-inactiveSelectionBackground)'
}}
w={16}
h="100%"
cursor="pointer"
position="absolute"
top="0"
left="0"
onClick={toggleIsExpanded}
_hover={{
background: 'var(--vscode-list-inactiveSelectionBackground)'
}}
>
{isExpanded ? (
<HiOutlineChevronDown pointerEvents="none" />
) : (
<HiOutlineChevronRight pointerEvents="none" />
)}
</Center>
<VStack gap={6} flex={1}>
<VStack gap={6} flex={1} ml="18px">
<SearchInput
value={inputValue}
onChange={setInputValue}
Expand Down
40 changes: 40 additions & 0 deletions src/webview/SearchSidebar/hooks/useDark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
PropsWithChildren,
createContext,
useContext,
useEffect,
useState
} from 'react'

const BODY_DARK_ATTRIBUTE = 'data-vscode-theme-kind'

function getIsDark() {
return document.body.getAttribute(BODY_DARK_ATTRIBUTE) === 'vscode-dark'
}

const UseDarkContext = createContext(true)

function useObserveDark() {
const [isDark, setIsDark] = useState(getIsDark())
useEffect(() => {
const observer = new MutationObserver((mutationsList, _observer) => {
for (let mutation of mutationsList) {
if (mutation.attributeName === BODY_DARK_ATTRIBUTE) {
setIsDark(() => getIsDark())
}
}
})
const config = { attributes: true, childList: false, subtree: false }
observer.observe(document.body, config)
}, [])
return isDark
}

export const UseDarkContextProvider = ({ children }: PropsWithChildren) => {
const isDark = useObserveDark()
return <UseDarkContext.Provider value={isDark} children={children} />
}

export function useDark() {
return useContext(UseDarkContext)
}
Loading

0 comments on commit e77a290

Please sign in to comment.