From d57a2b2aace4e35b50723a1d132c4b62017d90f3 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 27 May 2026 23:54:55 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=88=A0=E9=99=A4=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=90=9C=E7=B4=A2=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20CodeMirror=206=20=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除文件: - SearchBar/SearchBar.tsx — 自定义搜索栏组件 - stores/searchStore.ts — 自定义搜索状态管理 - lib/searchEngine.ts — 自定义搜索匹配引擎 - types/search.ts — 搜索相关类型定义 清理代码: - App.tsx:移除 SearchBar 导入和渲染 - useKeyboard.ts:移除 searchStore 引用和搜索快捷键 - Editor.tsx:移除 searchStore 引用和搜索逻辑 - types/index.ts:移除 search 类型导出 - global.css:删除旧搜索栏样式(~113 行) CodeMirror 6 搜索面板移到顶部: - .cm-panels { top: 0; bottom: auto; } - .cm-panels-bottom { top: 0; bottom: auto; } 快捷键(CodeMirror 内置): - Ctrl+F 搜索 - Ctrl+H 替换 - Enter/Shift+Enter 下一个/上一个 - Alt+C 大小写 - Alt+R 正则 --- src/renderer/App.tsx | 13 +- src/renderer/components/Editor/Editor.tsx | 16 +- .../components/SearchBar/SearchBar.tsx | 147 ------------------ src/renderer/hooks/useKeyboard.ts | 12 -- src/renderer/lib/searchEngine.ts | 56 ------- src/renderer/stores/searchStore.ts | 64 -------- src/renderer/styles/global.css | 126 ++------------- src/renderer/types/index.ts | 1 - src/renderer/types/search.ts | 9 -- 9 files changed, 19 insertions(+), 425 deletions(-) delete mode 100644 src/renderer/components/SearchBar/SearchBar.tsx delete mode 100644 src/renderer/lib/searchEngine.ts delete mode 100644 src/renderer/stores/searchStore.ts delete mode 100644 src/renderer/types/search.ts diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 8f4e3d8..a5dc9e1 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,25 +1,23 @@ import React, { useState, useCallback, useEffect, useRef } from 'react' import { useTabStore } from './stores/tabStore' import { useEditorStore } from './stores/editorStore' -import { useSearchStore } from './stores/searchStore' import { useTheme } from './hooks/useTheme' import { useSettings } from './hooks/useSettings' import { useKeyboard } from './hooks/useKeyboard' import { useUnsavedWarning } from './hooks/useUnsavedWarning' import { useFileWatch } from './hooks/useFileWatch' import { recentFilesRepository } from './db/recentFilesRepository' +import { useDragDrop } from './hooks/useDragDrop' import { Toolbar } from './components/Toolbar/Toolbar' import { TabBar } from './components/TabBar/TabBar' import { Editor } from './components/Editor/Editor' import { Preview } from './components/Preview/Preview' import { Sidebar } from './components/Sidebar/Sidebar' -import { SearchBar } from './components/SearchBar/SearchBar' import { StatusBar } from './components/StatusBar/StatusBar' import { WelcomeScreen } from './components/WelcomeScreen/WelcomeScreen' import { Toast } from './components/Toast/Toast' import { ModifiedBanner } from './components/ModifiedBanner/ModifiedBanner' import { DropOverlay } from './components/DropOverlay/DropOverlay' -import { useDragDrop } from './hooks/useDragDrop' export default function App() { const tabs = useTabStore(s => s.tabs) @@ -29,7 +27,6 @@ export default function App() { const setModified = useTabStore(s => s.setModified) const updateTabContent = useTabStore(s => s.updateTabContent) const loadFromDB = useTabStore(s => s.loadFromDB) - const saveToDB = useTabStore(s => s.saveToDB) const viewMode = useEditorStore(s => s.viewMode) const splitRatio = useEditorStore(s => s.splitRatio) @@ -80,9 +77,7 @@ export default function App() { const result = await window.electronAPI.openFile() if (result && 'filePath' in result) { createTab(result.filePath, result.content) - // 追踪最近打开文件 if (result.filePath) recentFilesRepository.add(result.filePath) - // 保存标签页到 DB setTimeout(() => useTabStore.getState().saveToDB(), 100) } } @@ -137,7 +132,7 @@ export default function App() { saveViewMode(mode) }, [saveViewMode]) - // M-11: 主进程事件注册(使用 ref 保持最新回调引用 + cleanup 清理) + // M-11: 主进程事件注册 const handleSaveRef = useRef(handleSave) const handleSaveAsRef = useRef(handleSaveAs) handleSaveRef.current = handleSave @@ -149,7 +144,7 @@ export default function App() { const onOpen = (data: { filePath: string; content: string }) => { createTab(data.filePath, data.content) - recentFilesRepository.add(data.filePath) + if (data.filePath) recentFilesRepository.add(data.filePath) setTimeout(() => useTabStore.getState().saveToDB(), 100) } const onSave = () => handleSaveRef.current() @@ -218,7 +213,6 @@ export default function App() {
{viewMode !== 'preview' && (
-
)} @@ -270,7 +264,6 @@ function Resizer({ onResize }: { onResize: (ratio: number) => void }) { document.removeEventListener('mouseup', handleMouseUp) document.body.style.cursor = '' document.body.style.userSelect = '' - // 保存最终比例 const wrapper = document.getElementById('content-wrapper') if (wrapper) { const rect = wrapper.getBoundingClientRect() diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 84eba1b..695e044 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useCallback } from 'react' import { useTabStore } from '../../stores/tabStore' -import { useSearchStore } from '../../stores/searchStore' import { useCodeMirror } from './useCodeMirror' import { EditorToolbar } from './EditorToolbar' @@ -20,12 +19,10 @@ export function Editor({ darkMode }: EditorProps) { containerRef, viewRef, setContent, - getContent, getScrollTop, setScrollTop, getSelection, - setSelection, - scrollTo + setSelection } = useCodeMirror({ content: activeTab?.content ?? '', onChange: useCallback((value: string) => { @@ -43,15 +40,13 @@ export function Editor({ darkMode }: EditorProps) { useEffect(() => { if (!activeTab) return setContent(activeTab.content) - - // 恢复滚动和选区 requestAnimationFrame(() => { setScrollTop(activeTab.scrollTop) setSelection(activeTab.selectionStart, activeTab.selectionEnd) }) }, [activeTabId]) - // 保存当前标签状态(切换前) + // 保存当前标签状态 useEffect(() => { return () => { if (!activeTabId) return @@ -63,12 +58,10 @@ export function Editor({ darkMode }: EditorProps) { } }, [activeTabId]) - // 搜索:Ctrl+F 聚焦搜索框,Enter 导航 + // Ctrl+B 粗体, Ctrl+I 斜体 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const isCtrl = e.ctrlKey || e.metaKey - - // Ctrl+B 粗体 if (isCtrl && e.key === 'b') { e.preventDefault() const view = viewRef.current @@ -80,8 +73,6 @@ export function Editor({ darkMode }: EditorProps) { selection: { anchor: from + 2, head: from + 2 + selected.length } }) } - - // Ctrl+I 斜体 if (isCtrl && e.key === 'i') { e.preventDefault() const view = viewRef.current @@ -94,7 +85,6 @@ export function Editor({ darkMode }: EditorProps) { }) } } - document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [viewRef]) diff --git a/src/renderer/components/SearchBar/SearchBar.tsx b/src/renderer/components/SearchBar/SearchBar.tsx deleted file mode 100644 index 50beaea..0000000 --- a/src/renderer/components/SearchBar/SearchBar.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { useCallback, useRef, useEffect } from 'react' -import { useSearchStore } from '../../stores/searchStore' -import { useTabStore } from '../../stores/tabStore' -import { findMatches } from '../../lib/searchEngine' -import { ChevronUp, ChevronDown, X } from '../Icons' - -export function SearchBar() { - const searchInputRef = useRef(null) - - const isVisible = useSearchStore(s => s.isVisible) - const showReplace = useSearchStore(s => s.showReplace) - const searchText = useSearchStore(s => s.searchText) - const replaceText = useSearchStore(s => s.replaceText) - const matches = useSearchStore(s => s.matches) - const currentIndex = useSearchStore(s => s.currentIndex) - const options = useSearchStore(s => s.options) - const setSearchText = useSearchStore(s => s.setSearchText) - const setReplaceText = useSearchStore(s => s.setReplaceText) - const setShowReplace = useSearchStore(s => s.setShowReplace) - const setMatches = useSearchStore(s => s.setMatches) - const findNext = useSearchStore(s => s.findNext) - const findPrev = useSearchStore(s => s.findPrev) - const toggleCaseSensitive = useSearchStore(s => s.toggleCaseSensitive) - const toggleRegex = useSearchStore(s => s.toggleRegex) - const close = useSearchStore(s => s.close) - - const getActiveTab = useTabStore(s => s.getActiveTab) - - const doSearch = useCallback((text: string) => { - const tab = getActiveTab() - if (!tab || !text) { - setMatches([]) - return - } - const found = findMatches(tab.content, text, options) - setMatches(found) - }, [getActiveTab, options, setMatches]) - - const handleSearchChange = useCallback((e: React.ChangeEvent) => { - const text = e.target.value - setSearchText(text) - // Editor 会监听 searchText 变化并执行搜索和高亮 - }, [setSearchText]) - - const handleReplaceCurrent = useCallback(() => { - const tab = getActiveTab() - if (!tab || matches.length === 0 || currentIndex < 0) return - const m = matches[currentIndex] - const newContent = tab.content.substring(0, m.start) + replaceText + tab.content.substring(m.end) - useTabStore.getState().updateTabContent(tab.id, newContent) - doSearch(searchText) - }, [getActiveTab, matches, currentIndex, replaceText, searchText, doSearch]) - - // B-02: 全部替换(从后向前逐个替换) - const handleReplaceAll = useCallback(() => { - const tab = getActiveTab() - if (!tab || matches.length === 0) return - let result = tab.content - for (let i = matches.length - 1; i >= 0; i--) { - const m = matches[i] - result = result.substring(0, m.start) + replaceText + result.substring(m.end) - } - useTabStore.getState().updateTabContent(tab.id, result) - doSearch(searchText) - }, [getActiveTab, matches, replaceText, searchText, doSearch]) - - useEffect(() => { - if (isVisible && searchInputRef.current) { - searchInputRef.current.focus() - } - }, [isVisible]) - - if (!isVisible) return null - - return ( - - ) -} diff --git a/src/renderer/hooks/useKeyboard.ts b/src/renderer/hooks/useKeyboard.ts index 34c3e92..cf9b57a 100644 --- a/src/renderer/hooks/useKeyboard.ts +++ b/src/renderer/hooks/useKeyboard.ts @@ -1,13 +1,11 @@ import { useEffect, useCallback } from 'react' import { useTabStore } from '../stores/tabStore' import { useEditorStore } from '../stores/editorStore' -import { useSearchStore } from '../stores/searchStore' import type { ViewMode } from '../types/settings' export function useKeyboard(handleOpenFile: () => void, handleSave: () => void, handleSaveAs: () => void) { const setViewMode = useEditorStore(s => s.setViewMode) - // M-08: 使用 getState() 而非依赖 tabs/activeTabId 等频繁变化的值 const handleKeydown = useCallback((e: KeyboardEvent) => { const isCtrl = e.ctrlKey || e.metaKey @@ -18,7 +16,6 @@ export function useKeyboard(handleOpenFile: () => void, handleSave: () => void, if (isCtrl && e.key === '2') { e.preventDefault(); setViewMode('editor'); return } if (isCtrl && e.key === '3') { e.preventDefault(); setViewMode('preview'); return } - // M-08: 通过 getState() 读取最新状态 const tabState = useTabStore.getState() if (isCtrl && e.key === 't') { e.preventDefault(); tabState.createTab(null, ''); return } if (isCtrl && e.key === 'w') { @@ -47,15 +44,6 @@ export function useKeyboard(handleOpenFile: () => void, handleSave: () => void, } return } - - // Search & Replace - const searchState = useSearchStore.getState() - if (isCtrl && e.key === 'f') { e.preventDefault(); searchState.setVisible(true); searchState.setShowReplace(false); return } - if (isCtrl && e.key === 'h') { e.preventDefault(); searchState.setVisible(true); searchState.setShowReplace(true); return } - if (e.key === 'Escape' && searchState.isVisible) { e.preventDefault(); searchState.close(); return } - if (e.altKey && e.key === 'c') { e.preventDefault(); searchState.toggleCaseSensitive(); return } - if (e.altKey && e.key === 'r') { e.preventDefault(); searchState.toggleRegex(); return } - if (isCtrl && e.shiftKey && e.key === 'G') { e.preventDefault(); searchState.findPrev(); return } }, [handleOpenFile, handleSave, handleSaveAs, setViewMode]) useEffect(() => { diff --git a/src/renderer/lib/searchEngine.ts b/src/renderer/lib/searchEngine.ts deleted file mode 100644 index a367fea..0000000 --- a/src/renderer/lib/searchEngine.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { SearchMatch, SearchOptions } from '../types/search' - -export function findMatches( - content: string, - searchText: string, - options: SearchOptions -): SearchMatch[] { - if (!searchText) return [] - - const matches: SearchMatch[] = [] - - if (options.useRegex) { - try { - const flags = options.caseSensitive ? 'g' : 'gi' - const regex = new RegExp(searchText, flags) - let m: RegExpExecArray | null - while ((m = regex.exec(content)) !== null) { - matches.push({ start: m.index, end: m.index + m[0].length }) - if (m[0].length === 0) regex.lastIndex++ - } - } catch { - // invalid regex — return empty - } - } else { - const haystack = options.caseSensitive ? content : content.toLowerCase() - const needle = options.caseSensitive ? searchText : searchText.toLowerCase() - let idx = 0 - while ((idx = haystack.indexOf(needle, idx)) !== -1) { - matches.push({ start: idx, end: idx + needle.length }) - idx += needle.length - } - } - - return matches -} - -// 构建行首位置缓存(用于 O(log N) 二分查找行号) -export function buildLineStarts(content: string): number[] { - const starts = [0] - for (let i = 0; i < content.length; i++) { - if (content.charCodeAt(i) === 10) starts.push(i + 1) // '\n' = 0x0A - } - return starts -} - -// 二分查找:返回 pos 所在行号 -export function getLineNumber(lineStarts: number[], pos: number): number { - let lo = 0 - let hi = lineStarts.length - 1 - while (lo < hi) { - const mid = (lo + hi + 1) >> 1 - if (lineStarts[mid] <= pos) lo = mid - else hi = mid - 1 - } - return lo -} diff --git a/src/renderer/stores/searchStore.ts b/src/renderer/stores/searchStore.ts deleted file mode 100644 index 089ca06..0000000 --- a/src/renderer/stores/searchStore.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { create } from 'zustand' -import type { SearchMatch, SearchOptions } from '../types/search' - -interface SearchState { - isVisible: boolean - showReplace: boolean - searchText: string - replaceText: string - matches: SearchMatch[] - currentIndex: number - options: SearchOptions - - setVisible: (visible: boolean) => void - setShowReplace: (show: boolean) => void - setSearchText: (text: string) => void - setReplaceText: (text: string) => void - setMatches: (matches: SearchMatch[]) => void - setCurrentIndex: (index: number) => void - toggleCaseSensitive: () => void - toggleRegex: () => void - findNext: () => void - findPrev: () => void - close: () => void -} - -export const useSearchStore = create((set, get) => ({ - isVisible: false, - showReplace: false, - searchText: '', - replaceText: '', - matches: [], - currentIndex: -1, - options: { caseSensitive: false, useRegex: false }, - - setVisible: (visible) => set({ isVisible: visible }), - setShowReplace: (show) => set({ showReplace: show }), - setSearchText: (text) => set({ searchText: text }), - setReplaceText: (text) => set({ replaceText: text }), - setMatches: (matches) => set({ matches, currentIndex: matches.length > 0 ? 0 : -1 }), - setCurrentIndex: (index) => set({ currentIndex: index }), - toggleCaseSensitive: () => - set(state => ({ options: { ...state.options, caseSensitive: !state.options.caseSensitive } })), - toggleRegex: () => - set(state => ({ options: { ...state.options, useRegex: !state.options.useRegex } })), - findNext: () => { - const { matches, currentIndex } = get() - if (matches.length === 0) return - set({ currentIndex: (currentIndex + 1) % matches.length }) - }, - findPrev: () => { - const { matches, currentIndex } = get() - if (matches.length === 0) return - set({ currentIndex: (currentIndex - 1 + matches.length) % matches.length }) - }, - close: () => - set({ - isVisible: false, - showReplace: false, - searchText: '', - replaceText: '', - matches: [], - currentIndex: -1 - }) -})) diff --git a/src/renderer/styles/global.css b/src/renderer/styles/global.css index 4b2ef40..ac3ac4e 100644 --- a/src/renderer/styles/global.css +++ b/src/renderer/styles/global.css @@ -208,6 +208,19 @@ html, body { /* ===== CodeMirror 搜索样式美化 ===== */ +/* 搜索面板移到顶部 */ +.cm-panels { + top: 0 !important; + bottom: auto !important; +} + +.cm-panels-bottom { + top: 0 !important; + bottom: auto !important; + border-bottom: 1px solid var(--border) !important; + border-top: none !important; +} + /* 搜索面板整体 */ .cm-panel.cm-search { background: var(--bg) !important; @@ -984,119 +997,6 @@ html, body { background: var(--primary); } -/* Search & Replace Bar */ -#btn-toggle-replace { - font-size: 10px; - transition: transform 0.15s ease; -} - -#btn-toggle-replace.expanded { transform: rotate(90deg); } - -#search-bar { - background: var(--search-bg); - border-bottom: 1px solid var(--search-border); - padding: 6px 10px; - flex-shrink: 0; -} - -.search-row, #replace-row { - display: flex; - align-items: center; - gap: 4px; -} - -#replace-row { margin-top: 4px; } - -#search-input, #replace-input { - flex: 1; - min-width: 0; - height: 26px; - padding: 0 8px; - border: 1px solid var(--border); - border-radius: 4px; - background: var(--bg); - color: var(--text); - font-size: 13px; - font-family: var(--font-ui); - outline: none; - transition: border-color 0.15s ease; -} - -#search-input:focus, #replace-input:focus { border-color: var(--primary); } - -#search-count { - font-size: 11px; - color: var(--text-tertiary); - white-space: nowrap; - min-width: 40px; - text-align: center; -} - -.search-opt-btn { - display: flex; - align-items: center; - justify-content: center; - min-width: 26px; - height: 26px; - padding: 0 4px; - border: 1px solid var(--border); - border-radius: 4px; - background: transparent; - color: var(--text-secondary); - font-size: 11px; - font-weight: 600; - font-family: var(--font-mono); - cursor: pointer; - transition: all 0.15s ease; -} - -.search-opt-btn:hover { background: var(--bg-tertiary); } - -.search-opt-btn.active { - background: var(--primary-light); - color: var(--primary); - border-color: var(--primary); -} - -.search-nav-btn { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: none; - background: transparent; - color: var(--text-secondary); - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: all 0.15s ease; -} - -.search-nav-btn:hover { - background: var(--bg-tertiary); - color: var(--text); -} - -.replace-btn { - height: 26px; - padding: 0 10px; - border: 1px solid var(--border); - border-radius: 4px; - background: transparent; - color: var(--text-secondary); - font-size: 12px; - font-family: var(--font-ui); - cursor: pointer; - white-space: nowrap; - transition: all 0.15s ease; -} - -.replace-btn:hover { - background: var(--bg-tertiary); - color: var(--text); -} - /* View Modes */ #app.mode-preview #editor-panel, #app.mode-preview #resizer { display: none; } diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 30dd8b3..f98e33b 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -1,5 +1,4 @@ export type { Tab } from './tab' export type { FileNode, DirTreeResult } from './file' export type { ViewMode, Settings } from './settings' -export type { SearchMatch, SearchOptions } from './search' export type { ElectronAPI, IpcInvokeMap } from './ipc' diff --git a/src/renderer/types/search.ts b/src/renderer/types/search.ts deleted file mode 100644 index 9617be0..0000000 --- a/src/renderer/types/search.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface SearchMatch { - start: number - end: number -} - -export interface SearchOptions { - caseSensitive: boolean - useRegex: boolean -}