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
-}