Browse Source

Update search functions.

oldj 4 years ago
parent
commit
65aa7466fd

+ 2 - 0
src/common/i18n/languages/en.ts

@@ -94,12 +94,14 @@ export default {
   never: 'Never',
   new: 'New',
   new_version_found: 'New version found',
+  next: 'Next',
   no_access_to_hosts: 'No permission to write to the Hosts file.',
   no_record: 'No record',
   password: 'Password',
   paste: 'Paste',
   port: 'Port',
   preferences: 'Preferences',
+  previous: 'Previous',
   protocol: 'Protocol',
   proxy: 'Proxy',
   quit: 'Quit',

+ 2 - 0
src/common/i18n/languages/zh.ts

@@ -96,12 +96,14 @@ const lang: LanguageDict = {
   never: '从不',
   new: '新建',
   new_version_found: '发现新版本',
+  next: '下一个',
   no_access_to_hosts: '没有写入 Hosts 文件的权限。',
   no_record: '没有记录',
   password: '密码',
   paste: '粘贴',
   port: '端口',
   preferences: '选项',
+  previous: '上一个',
   protocol: '协议',
   proxy: '代理',
   quit: '退出',

+ 18 - 7
src/common/types.d.ts

@@ -22,10 +22,7 @@ export interface IPopupMenuOption {
   items: IMenuItemOption[];
 }
 
-export interface IFindResultItem {
-  item_title: string;
-  item_id: string;
-  item_type: HostsType;
+export interface IFindPosition {
   start: number;
   end: number;
   line: number;
@@ -37,8 +34,22 @@ export interface IFindResultItem {
   after: string;
 }
 
-export type IFindShowSourceParam = Pick<IFindResultItem,
-  'item_id' | 'start' | 'end' | 'line' | 'line_pos'
-  | 'end_line' | 'end_line_pos'> & {
+export interface IFindSpliter {
+  before: string;
+  match: string;
+  after: string;
+  replace?: string;
+}
+
+export interface IFindItem {
+  item_id: string;
+  item_title: string;
+  item_type: HostsType;
+  positions: IFindPosition[];
+  spliters: IFindSpliter[];
+}
+
+export type IFindShowSourceParam = IFindPosition & {
+  item_id: string;
   [key: string]: any;
 }

+ 11 - 8
src/main/actions/find/findBy.ts

@@ -3,20 +3,21 @@
  * @homepage: https://oldj.net
  */
 
+import splitContent from '@main/actions/find/splitContent'
 import getContentOfHosts from '@main/actions/hosts/getContent'
 import { flatten } from '@root/common/hostsFn'
-import { IFindResultItem } from '@root/common/types'
+import { IFindItem } from '@root/common/types'
+import findInContent from 'src/main/actions/find/findPositionsInContent'
 import { getList } from '../index'
-import findInContent from './findInContent'
 
 export interface IFindOptions {
   is_regexp: boolean;
   is_ignore_case: boolean;
 }
 
-export default async (keyword: string, options: IFindOptions): Promise<IFindResultItem[]> => {
+export default async (keyword: string, options: IFindOptions): Promise<IFindItem[]> => {
   console.log(keyword)
-  let result_items: IFindResultItem[] = []
+  let result_items: IFindItem[] = []
 
   let tree = await getList()
   let items = flatten(tree)
@@ -35,13 +36,15 @@ export default async (keyword: string, options: IFindOptions): Promise<IFindResu
       continue
     }
     let content = await getContentOfHosts(item.id)
-    let found = findInContent(content, exp)
-    result_items = [...result_items, ...found.map(i => ({
-      ...i,
+    let positions = findInContent(content, exp)
+
+    result_items.push({
       item_title: item.title || '',
       item_id: item.id,
       item_type,
-    }))]
+      positions,
+      spliters: splitContent(content, positions),
+    })
   }
 
   return result_items

+ 2 - 2
src/main/actions/find/findInContent.ts → src/main/actions/find/findPositionsInContent.ts

@@ -3,9 +3,9 @@
  * @homepage: https://oldj.net
  */
 
-import { IFindResultItem } from '@root/common/types'
+import { IFindPosition } from '@root/common/types'
 
-type MatchResult = Pick<IFindResultItem, 'start' | 'end' | 'before' | 'match' | 'after' | 'line' | 'line_pos' | 'end_line' | 'end_line_pos'>
+type MatchResult = Pick<IFindPosition, 'start' | 'end' | 'before' | 'match' | 'after' | 'line' | 'line_pos' | 'end_line' | 'end_line_pos'>
 
 export default (content: string, exp: RegExp): MatchResult[] => {
   let result_items: MatchResult[] = []

+ 34 - 0
src/main/actions/find/splitContent.ts

@@ -0,0 +1,34 @@
+/**
+ * @author: oldj
+ * @homepage: https://oldj.net
+ */
+
+import { IFindPosition, IFindSpliter } from '@root/common/types'
+
+type MatchResult = Pick<IFindPosition, 'start' | 'end' | 'match'> & {
+  [key: string]: any;
+}
+
+export default (content: string, find_results: MatchResult[]): IFindSpliter[] => {
+  let spliters: IFindSpliter[] = []
+
+  let last_end = 0
+  find_results.map((r, idx) => {
+    let { start, match } = r
+    let before = content.slice(last_end, start)
+    let after = ''
+
+    last_end += before.length + match.length
+    if (idx === find_results.length - 1) {
+      after = content.slice(last_end)
+    }
+
+    let spliter: IFindSpliter = {
+      before, after, match,
+    }
+
+    spliters.push(spliter)
+  })
+
+  return spliters
+}

+ 11 - 1
src/renderer/components/Editor/HostsEditor.tsx

@@ -162,10 +162,16 @@ const HostsEditor = (props: Props) => {
     })
 
     // console.log(doc.getSelection())
+    await wait(200)
     if (!doc.getSelection()) {
-      await wait(200)
       await setSelection(params)
     }
+    cm_editor.focus()
+  }
+
+  const replaceOne = async (params: IFindShowSourceParam) => {
+    if (!cm_editor) return
+    let doc = cm_editor.getDoc()
   }
 
   useOnBroadcast('show_source', async (params: IFindShowSourceParam) => {
@@ -182,6 +188,10 @@ const HostsEditor = (props: Props) => {
     setSelection(params)
   }, [hosts, cm_editor])
 
+  useOnBroadcast('replace_one', async (params: IFindShowSourceParam) => {
+    if (!cm_editor) return
+  }, [hosts, cm_editor])
+
   return (
     <div className={styles.root}>
       <div

+ 4 - 0
src/renderer/pages/find.less

@@ -14,6 +14,10 @@
   padding-left: 8px;
   cursor: pointer;
   user-select: none;
+
+  &.selected {
+    background: var(--swh-tree-selected-bg);
+  }
 }
 
 .result_content {

+ 123 - 32
src/renderer/pages/find.tsx

@@ -7,32 +7,43 @@ import { useModel } from '@@/plugin-model/useModel'
 import {
   Box,
   Button,
+  ButtonGroup,
   Checkbox,
   HStack,
+  IconButton,
   Input,
   InputGroup,
   InputLeftElement,
   Spacer,
-  Tooltip,
+  Spinner,
   useColorMode,
   VStack,
 } from '@chakra-ui/react'
 import ItemIcon from '@renderer/components/ItemIcon'
 import { actions, agent } from '@renderer/core/agent'
 import useOnBroadcast from '@renderer/core/useOnBroadcast'
-import { IFindResultItem, IFindShowSourceParam } from '@root/common/types'
+import { HostsType } from '@root/common/data'
+import { IFindItem, IFindPosition, IFindShowSourceParam } from '@root/common/types'
 import { useDebounce } from 'ahooks'
+import clsx from 'clsx'
 import lodash from 'lodash'
 import React, { useEffect, useRef, useState } from 'react'
-import { IoSearch } from 'react-icons/io5'
-import { FixedSizeList as List, ListChildComponentProps } from 'react-window'
+import { IoArrowBackOutline, IoArrowForwardOutline, IoSearch } from 'react-icons/io5'
 import AutoSizer from 'react-virtualized-auto-sizer'
+import { FixedSizeList as List, ListChildComponentProps } from 'react-window'
+import scrollIntoView from 'smooth-scroll-into-view-if-needed'
 import styles from './find.less'
 
 interface Props {
 
 }
 
+interface IFindPositionShow extends IFindPosition {
+  item_id: string;
+  item_title: string;
+  item_type: HostsType;
+}
+
 const find = (props: Props) => {
   const { lang, i18n, setLocale } = useModel('useI18n')
   const { configs, loadConfigs } = useModel('useConfigs')
@@ -41,7 +52,11 @@ const find = (props: Props) => {
   const [replact_to, setReplaceTo] = useState('')
   const [is_regexp, setIsRegExp] = useState(false)
   const [is_ignore_case, setIsIgnoreCase] = useState(false)
-  const [find_result, setFindResult] = useState<IFindResultItem[]>([])
+  const [find_result, setFindResult] = useState<IFindItem[]>([])
+  const [find_positions, setFindPositions] = useState<IFindPositionShow[]>([])
+  const [is_searching, setIsSearching] = useState(false)
+  const [current_result_idx, setCurrentResultIdx] = useState(0)
+  const [last_scroll_result_idx, setlastScrollResultIdx] = useState(-1)
   const debounced_keyword = useDebounce(keyword, { wait: 500 })
   const ipt_kw = useRef<HTMLInputElement>(null)
 
@@ -87,6 +102,23 @@ const find = (props: Props) => {
 
   useOnBroadcast('config_updated', loadConfigs)
 
+  const parsePositionShow = (find_items: IFindItem[]) => {
+    let positions_show: IFindPositionShow[] = []
+
+    find_items.map((item) => {
+      let { item_id, item_title, item_type, positions } = item
+      positions.map((p) => {
+        positions_show.push({
+          item_id, item_title, item_type,
+          ...p,
+        })
+      })
+    })
+
+    console.log(positions_show)
+    setFindPositions(positions_show)
+  }
+
   const doFind = lodash.debounce(async (v: string) => {
     console.log('find by:', v)
     if (!v) {
@@ -94,15 +126,20 @@ const find = (props: Props) => {
       return
     }
 
+    setIsSearching(true)
     let result = await actions.findBy(v, {
       is_regexp,
       is_ignore_case,
     })
+    setCurrentResultIdx(0)
+    setlastScrollResultIdx(0)
     setFindResult(result)
+    parsePositionShow(result)
+    setIsSearching(false)
   }, 500)
 
-  const toShowSource = async (result_item: IFindResultItem) => {
-    console.log(result_item)
+  const toShowSource = async (result_item: IFindPositionShow) => {
+    // console.log(result_item)
     await actions.cmdFocusMainWindow()
     agent.broadcast('show_source', lodash.pick<IFindShowSourceParam>(result_item, [
       'item_id', 'start', 'end', 'match',
@@ -110,29 +147,51 @@ const find = (props: Props) => {
     ]))
   }
 
+  const replaceOne = async (result_item: IFindPositionShow) => {
+    await actions.cmdFocusMainWindow()
+    agent.broadcast('replace_one', lodash.pick<IFindShowSourceParam>(result_item, [
+      'item_id', 'start', 'end', 'match',
+      'line', 'line_pos', 'end_line', 'end_line_pos',
+    ]))
+  }
+
   const ResultRow = (row_data: ListChildComponentProps) => {
-    let data = find_result[row_data.index]
+    const data = find_positions[row_data.index]
+    const el = useRef<HTMLDivElement>(null)
+    const is_selected = current_result_idx === row_data.index
+
+    useEffect(() => {
+      if (el.current && is_selected && current_result_idx !== last_scroll_result_idx) {
+        setlastScrollResultIdx(current_result_idx)
+        scrollIntoView(el.current, {
+          behavior: 'smooth',
+          scrollMode: 'if-needed',
+        })
+      }
+    }, [el, current_result_idx, last_scroll_result_idx])
+
     return (
-      <Tooltip label={lang.to_show_source} placement="top" hasArrow>
-        <Box
-          style={row_data.style}
-          className={styles.result_row}
-          borderBottomWidth={1}
-          borderBottomColor={configs?.theme === 'dark' ? 'gray.600' : 'gray.200'}
-          onDoubleClick={e => toShowSource(data)}
-        >
-          <div className={styles.result_content}>
-            <span>{data.before}</span>
-            <span className={styles.highlight}>{data.match}</span>
-            <span>{data.after}</span>
-          </div>
-          <div className={styles.result_title}>
-            <ItemIcon type={data.item_type}/>
-            <span>{data.item_title}</span>
-          </div>
-          <div className={styles.result_line}>{data.line}</div>
-        </Box>
-      </Tooltip>
+      <Box
+        style={row_data.style}
+        className={clsx(styles.result_row, is_selected && styles.selected)}
+        borderBottomWidth={1}
+        borderBottomColor={configs?.theme === 'dark' ? 'gray.600' : 'gray.200'}
+        onClick={() => setCurrentResultIdx(row_data.index)}
+        onDoubleClick={() => toShowSource(data)}
+        ref={el}
+        title={lang.to_show_source}
+      >
+        <div className={styles.result_content}>
+          <span>{data.before}</span>
+          <span className={styles.highlight}>{data.match}</span>
+          <span>{data.after}</span>
+        </div>
+        <div className={styles.result_title}>
+          <ItemIcon type={data.item_type}/>
+          <span>{data.item_title}</span>
+        </div>
+        <div className={styles.result_line}>{data.line}</div>
+      </Box>
     )
   }
 
@@ -212,7 +271,7 @@ const find = (props: Props) => {
               <List
                 width={width}
                 height={height}
-                itemCount={find_result.length}
+                itemCount={find_positions.length}
                 itemSize={28}
               >
                 {ResultRow}
@@ -228,19 +287,51 @@ const find = (props: Props) => {
           spacing={4}
           // justifyContent="flex-end"
         >
-          <span>{i18n.trans(find_result.length > 1 ? 'items_found' : 'item_found', [find_result.length.toString()])}</span>
+          {is_searching ? (
+            <Spinner/>
+          ) : (
+            <span>{i18n.trans(
+              find_positions.length > 1 ? 'items_found' : 'item_found',
+              [find_positions.length.toLocaleString()],
+            )}</span>
+          )}
           <Spacer/>
           <Button
             size="sm"
             variant="outline"
-            isDisabled={find_result.length === 0}
+            isDisabled={is_searching || find_positions.length === 0}
           >{lang.replace_all}</Button>
           <Button
             size="sm"
             variant="solid"
             colorScheme="blue"
-            isDisabled={find_result.length === 0}
+            isDisabled={is_searching || find_positions.length === 0}
           >{lang.replace}</Button>
+
+          <ButtonGroup
+            size="sm"
+            isAttached variant="outline"
+            isDisabled={is_searching || find_positions.length === 0}
+          >
+            <IconButton
+              aria-label="previous" icon={<IoArrowBackOutline/>}
+              onClick={() => {
+                let idx = current_result_idx - 1
+                if (idx < 0) idx = 0
+                setCurrentResultIdx(idx)
+              }}
+              isDisabled={current_result_idx <= 1}
+            />
+            <IconButton
+              aria-label="next" icon={<IoArrowForwardOutline/>}
+              onClick={() => {
+                let idx = current_result_idx + 1
+                if (idx > find_positions.length) idx = find_positions.length
+                setCurrentResultIdx(idx)
+              }}
+              isDisabled={current_result_idx >= find_positions.length}
+            />
+          </ButtonGroup>
         </HStack>
       </VStack>
     </div>

+ 15 - 15
test/main/findInContent.test.ts

@@ -4,7 +4,7 @@
  */
 
 import assert = require('assert')
-import { default as findInContent } from 'src/main/actions/find/findInContent'
+import { default as findInContent } from 'src/main/actions/find/findPositionsInContent'
 
 describe('find in content test', () => {
   it('basic test 1', () => {
@@ -33,23 +33,23 @@ describe('find in content test', () => {
     assert(m[2].after === '')
   })
 
-  it.only('basic test 2', () => {
+  it('basic test 2', () => {
     let content = `abc12 abc123 abc\nxyza3b`
     let m = findInContent(content, /a\w*3/ig)
-    console.log(m)
+    // console.log(m)
     assert(m.length === 2)
-    assert(m[1].line === 1)
-    assert(m[1].start === 6)
-    assert(m[1].end === 12)
-    assert(m[1].before === 'abc12 ')
-    assert(m[1].match === 'abc123')
-    assert(m[1].after === ' abc')
+    assert(m[0].line === 1)
+    assert(m[0].start === 6)
+    assert(m[0].end === 12)
+    assert(m[0].before === 'abc12 ')
+    assert(m[0].match === 'abc123')
+    assert(m[0].after === ' abc')
 
-    assert(m[2].line === 2)
-    assert(m[2].start === 20)
-    assert(m[2].end === 22)
-    assert(m[2].before === 'xyz')
-    assert(m[2].match === 'a3')
-    assert(m[2].after === 'b')
+    assert(m[1].line === 2)
+    assert(m[1].start === 20)
+    assert(m[1].end === 22)
+    assert(m[1].before === 'xyz')
+    assert(m[1].match === 'a3')
+    assert(m[1].after === 'b')
   })
 })

+ 25 - 0
test/main/splitContent.test.ts

@@ -0,0 +1,25 @@
+/**
+ * @author: oldj
+ * @homepage: https://oldj.net
+ */
+
+import assert = require('assert')
+import { default as findInContent } from 'src/main/actions/find/findPositionsInContent'
+import { default as splitContent } from 'src/main/actions/find/splitContent'
+
+describe('split content test', () => {
+  it('basic test 1', () => {
+    let content = `abc12 abc123 abc44`
+    let m = findInContent(content, /bc/ig)
+    let sp = splitContent(content, m)
+    assert(sp[0].before === 'a')
+    assert(sp[0].after === '')
+    assert(sp[1].before === '12 a')
+    assert(sp[1].after === '')
+    assert(sp[2].before === '123 a')
+    assert(sp[2].after === '44')
+
+    let r = sp.map(i => `${i.before}${i.match}${i.after}`).join('')
+    assert(r === content)
+  })
+})