Skip to content

Commit 21d8d3b

Browse files
SoonIterHerringtonDarkholme
authored andcommitted
feat(ui): SearchResultList treeItem component
1 parent b9cef41 commit 21d8d3b

File tree

10 files changed

+348
-17
lines changed

10 files changed

+348
-17
lines changed

src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ export type Definition = {
3434
id: string
3535
}
3636
reload: {}
37+
openFile: {
38+
filePath: string
39+
locationsToSelect?: {
40+
start: {
41+
column: number
42+
line: number
43+
}
44+
end: {
45+
column: number
46+
line: number
47+
}
48+
}
49+
}
3750
}
3851
}
3952

src/view.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ParentPort, SgSearch } from './types'
1+
import type { Definition, ParentPort, SgSearch } from './types'
22
import { execa } from 'execa'
33
import { Unport, ChannelMessage } from 'unport'
44
import * as vscode from 'vscode'
@@ -95,6 +95,42 @@ class SearchSidebarProvider implements vscode.WebviewViewProvider {
9595
this.search.updateResult(res)
9696
parentPort.postMessage('search', { ...payload, searchResult: res })
9797
})
98+
99+
parentPort.onMessage('openFile', async payload => this.openFile(payload))
100+
}
101+
102+
private openFile = ({
103+
filePath,
104+
locationsToSelect
105+
}: Definition['child2parent']['openFile']) => {
106+
const uris = workspace.workspaceFolders
107+
const { joinPath } = vscode.Uri
108+
109+
if (!uris?.length) {
110+
return
111+
}
112+
113+
const fileUri: vscode.Uri = joinPath(uris?.[0].uri, filePath)
114+
let range: undefined | vscode.Range
115+
if (locationsToSelect) {
116+
const { start, end } = locationsToSelect
117+
range = new vscode.Range(
118+
new vscode.Position(start.line, start.column),
119+
new vscode.Position(end.line, end.column)
120+
)
121+
}
122+
123+
vscode.workspace.openTextDocument(fileUri).then(
124+
async (textDoc: vscode.TextDocument) => {
125+
return vscode.window.showTextDocument(textDoc, {
126+
selection: range
127+
})
128+
},
129+
(error: any) => {
130+
console.error('error opening file', filePath)
131+
console.error(error)
132+
}
133+
)
98134
}
99135

100136
private getHtmlForWebview(webview: vscode.Webview) {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Box } from '@chakra-ui/react'
2+
import React, { useMemo } from 'react'
3+
import type { SgSearch } from '../../../../types'
4+
import { openFile } from '../../postMessage'
5+
6+
type HighLightToken = {
7+
start: {
8+
line: number
9+
column: number
10+
}
11+
end: {
12+
line: number
13+
column: number
14+
}
15+
style: React.CSSProperties
16+
}
17+
18+
function splitByHighLightTokens(tokens: HighLightToken[]) {
19+
const codeSegments = tokens
20+
.map(({ start, end, style }) => {
21+
// TODO: multilines highlight
22+
const { column: startColumn } = start
23+
const { column: endColumn } = end
24+
25+
const startIdx = startColumn
26+
const endIdx = endColumn
27+
28+
return {
29+
range: [startIdx, endIdx],
30+
style
31+
}
32+
})
33+
.sort((a, b) => a.range[0] - b.range[0])
34+
35+
return codeSegments
36+
}
37+
38+
interface CodeBlockProps {
39+
match: SgSearch
40+
}
41+
export const CodeBlock = ({ match }: CodeBlockProps) => {
42+
const { file, lines, range } = match
43+
const matchHighlight = [
44+
{
45+
start: {
46+
line: range.start.line,
47+
column: range.start.column
48+
},
49+
end: {
50+
line: range.end.line,
51+
column: range.end.column
52+
},
53+
style: {
54+
backgroundColor: 'var(--vscode-editor-findMatchHighlightBackground)',
55+
border: '1px solid var(--vscode-editor-findMatchHighlightBackground)'
56+
}
57+
}
58+
]
59+
60+
const codeSegments = useMemo(() => {
61+
return splitByHighLightTokens(matchHighlight)
62+
}, [lines, matchHighlight])
63+
64+
if (codeSegments.length === 0) {
65+
return (
66+
<Box
67+
flex="1"
68+
textOverflow="ellipsis"
69+
overflow="hidden"
70+
whiteSpace="pre"
71+
fontSize="13px"
72+
lineHeight="22px"
73+
height="22px"
74+
>
75+
{lines}
76+
</Box>
77+
)
78+
}
79+
80+
const highlightStartIdx = codeSegments[0].range[0]
81+
const highlightEndIdx = codeSegments[codeSegments.length - 1].range[1]
82+
return (
83+
<Box
84+
flex="1"
85+
textOverflow="ellipsis"
86+
overflow="hidden"
87+
whiteSpace="pre"
88+
fontSize="13px"
89+
lineHeight="22px"
90+
height="22px"
91+
cursor="pointer"
92+
onClick={() => {
93+
openFile({ filePath: file, locationsToSelect: match.range })
94+
}}
95+
>
96+
{highlightStartIdx <= 0 ? '' : lines.slice(0, highlightStartIdx)}
97+
{codeSegments.map(({ range, style }) => {
98+
const [start, end] = range
99+
return (
100+
<span style={style} key={`${start}-${end}`}>
101+
{lines.slice(start, end)}
102+
</span>
103+
)
104+
})}
105+
{highlightEndIdx >= lines.length ? '' : lines.slice(highlightEndIdx)}
106+
</Box>
107+
)
108+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Link, Text } from '@chakra-ui/react'
2+
import { openFile } from '../../postMessage'
3+
4+
interface FileLinkProps {
5+
filePath: string
6+
}
7+
8+
export const FileLink = ({ filePath }: FileLinkProps) => {
9+
return (
10+
<Link
11+
onClick={e => {
12+
e.stopPropagation()
13+
openFile({
14+
filePath
15+
})
16+
}}
17+
fontWeight="500"
18+
display="inline-flex"
19+
cursor="pointer"
20+
>
21+
<Text
22+
size="13"
23+
style={{
24+
textAlign: 'left',
25+
textOverflow: 'ellipsis',
26+
whiteSpace: 'nowrap',
27+
overflow: 'hidden'
28+
}}
29+
>
30+
{filePath}
31+
</Text>
32+
</Link>
33+
)
34+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { HStack, IconButton, VStack, Box } from '@chakra-ui/react'
2+
import { HiOutlineChevronDown, HiOutlineChevronRight } from 'react-icons/hi'
3+
import { useBoolean } from 'react-use'
4+
import type { SgSearch } from '../../../../types'
5+
import { CodeBlock } from './CodeBlock'
6+
import { FileLink } from './FileLink'
7+
8+
interface TreeItemProps {
9+
filePath: string
10+
matches: SgSearch[]
11+
}
12+
13+
const TreeItem = ({ filePath, matches }: TreeItemProps) => {
14+
const [isExpanded, toggleIsExpanded] = useBoolean(true)
15+
16+
return (
17+
<VStack w="100%" pl="10" p="2" gap="0">
18+
<HStack
19+
w="100%"
20+
onClick={toggleIsExpanded}
21+
cursor="pointer"
22+
_hover={{
23+
background: 'var(--vscode-list-inactiveSelectionBackground)'
24+
}}
25+
>
26+
<IconButton
27+
flex={0}
28+
color="var(--vscode-foreground)"
29+
background="transparent"
30+
aria-label="expand/collapse button"
31+
pointerEvents="none"
32+
icon={
33+
isExpanded ? <HiOutlineChevronDown /> : <HiOutlineChevronRight />
34+
}
35+
mr="2"
36+
/>
37+
<HStack
38+
flex={1}
39+
gap="4"
40+
px="2"
41+
alignItems="center"
42+
h="22px"
43+
lineHeight="22px"
44+
>
45+
<FileLink filePath={filePath} />
46+
<div>{matches.length.toString()}</div>
47+
</HStack>
48+
</HStack>
49+
50+
<VStack w="100%" alignItems="flex-start" gap="0">
51+
{isExpanded &&
52+
matches?.map(match => {
53+
const { file, range } = match
54+
return (
55+
<HStack
56+
w="100%"
57+
justifyContent="flex-start"
58+
key={file + range.start.line + range.start.column}
59+
>
60+
<Box w="20px" />
61+
<CodeBlock match={match} />
62+
</HStack>
63+
)
64+
})}
65+
</VStack>
66+
</VStack>
67+
)
68+
}
69+
export default TreeItem
Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
1+
import { useMemo } from 'react'
12
import { SgSearch } from '../../../types'
3+
import TreeItem from './comps/TreeItem'
4+
import { Box } from '@chakra-ui/react'
25

36
type SearchResultListProps = {
47
matches: Array<SgSearch>
8+
pattern: string
59
}
610

7-
const SearchResultList = ({ matches }: SearchResultListProps) => {
8-
// TODO
9-
return <pre>{JSON.stringify(matches, null, 2)}</pre>
11+
const displayLimit = 2000
12+
const SearchResultList = ({ matches, pattern }: SearchResultListProps) => {
13+
const groupedByFile = useMemo(() => {
14+
return matches.slice(0, displayLimit).reduce(
15+
(groups, match) => {
16+
if (!groups[match.file]) {
17+
groups[match.file] = []
18+
}
19+
20+
groups[match.file].push(match)
21+
22+
return groups
23+
},
24+
{} as Record<string, SgSearch[]>
25+
)
26+
}, [matches])
27+
28+
return (
29+
<Box mt="10">
30+
{Object.entries(groupedByFile).map(([filePath, match]) => {
31+
return <TreeItem filePath={filePath} matches={match} key={filePath} />
32+
})}
33+
</Box>
34+
)
1035
}
36+
1137
export default SearchResultList

src/webview/SearchSidebar/SearchWidgetContainer/index.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,24 @@ const SearchWidgetContainer = ({
1818
return (
1919
<HStack position="relative">
2020
<Center
21-
_hover={{
22-
background: 'var(--vscode-list-inactiveSelectionBackground)'
23-
}}
2421
w={16}
2522
h="100%"
2623
cursor="pointer"
24+
position="absolute"
25+
top="0"
26+
left="0"
2727
onClick={toggleIsExpanded}
28+
_hover={{
29+
background: 'var(--vscode-list-inactiveSelectionBackground)'
30+
}}
2831
>
2932
{isExpanded ? (
3033
<HiOutlineChevronDown pointerEvents="none" />
3134
) : (
3235
<HiOutlineChevronRight pointerEvents="none" />
3336
)}
3437
</Center>
35-
<VStack gap={6} flex={1}>
38+
<VStack gap={6} flex={1} ml="18px">
3639
<SearchInput
3740
value={inputValue}
3841
onChange={setInputValue}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
PropsWithChildren,
3+
createContext,
4+
useContext,
5+
useEffect,
6+
useState
7+
} from 'react'
8+
9+
const BODY_DARK_ATTRIBUTE = 'data-vscode-theme-kind'
10+
11+
function getIsDark() {
12+
return document.body.getAttribute(BODY_DARK_ATTRIBUTE) === 'vscode-dark'
13+
}
14+
15+
const UseDarkContext = createContext(true)
16+
17+
function useObserveDark() {
18+
const [isDark, setIsDark] = useState(getIsDark())
19+
useEffect(() => {
20+
const observer = new MutationObserver((mutationsList, _observer) => {
21+
for (let mutation of mutationsList) {
22+
if (mutation.attributeName === BODY_DARK_ATTRIBUTE) {
23+
setIsDark(() => getIsDark())
24+
}
25+
}
26+
})
27+
const config = { attributes: true, childList: false, subtree: false }
28+
observer.observe(document.body, config)
29+
}, [])
30+
return isDark
31+
}
32+
33+
export const UseDarkContextProvider = ({ children }: PropsWithChildren) => {
34+
const isDark = useObserveDark()
35+
return <UseDarkContext.Provider value={isDark} children={children} />
36+
}
37+
38+
export function useDark() {
39+
return useContext(UseDarkContext)
40+
}

0 commit comments

Comments
 (0)