Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): SearchResultList treeItem component #122

Merged
merged 1 commit into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading