From 02697e045d03a01df887f3026230d17de6fa4562 Mon Sep 17 00:00:00 2001 From: Nived Shaji <83071573+Nivet2006@users.noreply.github.com> Date: Tue, 23 Jun 2026 00:57:28 +0530 Subject: [PATCH 1/2] added find and replace option with separace replace and replace all optioons --- src-tauri/tauri.conf.json | 2 +- src/components/editor/Editor.tsx | 73 +++++++++++- src/components/editor/SearchToolbar.tsx | 148 +++++++++++++++++++----- src/components/icons/index.tsx | 41 +++++++ vite.config.ts | 2 +- 5 files changed, 227 insertions(+), 39 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b4e2ab1b..db22c1e2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -5,7 +5,7 @@ "identifier": "com.scratch.app", "build": { "beforeDevCommand": "npm run dev", - "devUrl": "http://localhost:1420", + "devUrl": "http://127.0.0.1:1420", "beforeBuildCommand": "npm run build", "frontendDist": "../dist" }, diff --git a/src/components/editor/Editor.tsx b/src/components/editor/Editor.tsx index dcf07161..496af0fa 100644 --- a/src/components/editor/Editor.tsx +++ b/src/components/editor/Editor.tsx @@ -576,6 +576,8 @@ export function Editor({ // Search state const [searchOpen, setSearchOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); + const [replaceQuery, setReplaceQuery] = useState(""); + const [isReplaceOpen, setIsReplaceOpen] = useState(false); const [searchMatches, setSearchMatches] = useState< Array<{ from: number; to: number }> >([]); @@ -1305,6 +1307,46 @@ export function Editor({ setSearchQuery(query); }, []); + const replaceCurrent = useCallback((replaceText: string) => { + if (!editor || searchMatches.length === 0) return; + const match = searchMatches[currentMatchIndex]; + if (!match) return; + + editor.view.dispatch( + editor.state.tr.insertText(replaceText, match.from, match.to) + ); + + const newMatches = findMatches(searchQuery, editor); + setSearchMatches(newMatches); + + if (newMatches.length > 0) { + const nextIndex = currentMatchIndex % newMatches.length; + setCurrentMatchIndex(nextIndex); + updateSearchDecorations(newMatches, nextIndex, editor); + } else { + setCurrentMatchIndex(0); + updateSearchDecorations([], 0, editor); + } + }, [editor, searchQuery, searchMatches, currentMatchIndex, findMatches, updateSearchDecorations]); + + const replaceAll = useCallback((replaceText: string) => { + if (!editor || !searchQuery) return; + const currentMatches = findMatches(searchQuery, editor); + if (currentMatches.length === 0) return; + + const tr = editor.state.tr; + for (let i = currentMatches.length - 1; i >= 0; i--) { + const match = currentMatches[i]; + tr.insertText(replaceText, match.from, match.to); + } + editor.view.dispatch(tr); + + const newMatches = findMatches(searchQuery, editor); + setSearchMatches(newMatches); + setCurrentMatchIndex(0); + updateSearchDecorations(newMatches, 0, editor); + }, [editor, searchQuery, findMatches, updateSearchDecorations]); + // Debounced search effect useEffect(() => { if (!searchQuery.trim()) { @@ -1764,23 +1806,26 @@ export function Editor({ }); }, []); - // Cmd+F to open search (works when document/editor area is focused) + // Cmd+F to open search, Cmd+H to open replace (works when document/editor area is focused) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { + const isF = e.key.toLowerCase() === "f"; + const isH = e.key.toLowerCase() === "h"; if ( (e.metaKey || e.ctrlKey) && !e.shiftKey && - e.key.toLowerCase() === "f" + (isF || isH) ) { if (!currentNote || !editor) return; const target = e.target as HTMLElement; const tagName = target.tagName.toLowerCase(); - // Don't intercept if user is in an input/textarea (except the editor itself) + // Don't intercept if user is in an input/textarea (except the editor itself or search toolbar) if ( (tagName === "input" || tagName === "textarea") && - !target.closest(".ProseMirror") + !target.closest(".ProseMirror") && + !target.closest(".search-toolbar-container") ) { return; } @@ -1792,18 +1837,26 @@ export function Editor({ // Open search for the editor e.preventDefault(); - openEditorSearch(); + setSearchOpen(true); + if (isH) { + setIsReplaceOpen(true); + } + requestAnimationFrame(() => { + searchInputRef.current?.focus(); + }); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [editor, currentNote, openEditorSearch]); + }, [editor, currentNote]); // Clear search on note switch useEffect(() => { if (currentNote?.id) { setSearchOpen(false); setSearchQuery(""); + setReplaceQuery(""); + setIsReplaceOpen(false); setSearchMatches([]); setCurrentMatchIndex(0); // Clear decorations @@ -2415,6 +2468,8 @@ export function Editor({ onClose={() => { setSearchOpen(false); setSearchQuery(""); + setReplaceQuery(""); + setIsReplaceOpen(false); setSearchMatches([]); setCurrentMatchIndex(0); // Clear decorations and refocus editor @@ -2427,6 +2482,12 @@ export function Editor({ searchMatches.length === 0 ? 0 : currentMatchIndex + 1 } totalMatches={searchMatches.length} + replaceQuery={replaceQuery} + onReplaceChange={setReplaceQuery} + onReplace={() => replaceCurrent(replaceQuery)} + onReplaceAll={() => replaceAll(replaceQuery)} + isReplaceOpen={isReplaceOpen} + onToggleReplace={() => setIsReplaceOpen(!isReplaceOpen)} /> diff --git a/src/components/editor/SearchToolbar.tsx b/src/components/editor/SearchToolbar.tsx index 58ac474c..b9653b4d 100644 --- a/src/components/editor/SearchToolbar.tsx +++ b/src/components/editor/SearchToolbar.tsx @@ -1,6 +1,14 @@ import { useEffect, type RefObject } from "react"; import { Input, IconButton } from "../ui"; -import { ArrowUpIcon, ArrowDownIcon, XIcon } from "../icons"; +import { + ArrowUpIcon, + ArrowDownIcon, + XIcon, + ChevronDownIcon, + ChevronRightIcon, + ReplaceIcon, + ReplaceAllIcon, +} from "../icons"; import { shift } from "../../lib/platform"; interface SearchToolbarProps { @@ -12,6 +20,13 @@ interface SearchToolbarProps { currentMatch: number; totalMatches: number; inputRef: RefObject; + // Replace functionality props + replaceQuery: string; + onReplaceChange: (value: string) => void; + onReplace: () => void; + onReplaceAll: () => void; + isReplaceOpen: boolean; + onToggleReplace: () => void; } export function SearchToolbar({ @@ -23,6 +38,12 @@ export function SearchToolbar({ currentMatch, totalMatches, inputRef, + replaceQuery, + onReplaceChange, + onReplace, + onReplaceAll, + isReplaceOpen, + onToggleReplace, }: SearchToolbarProps) { // Auto-focus input on mount useEffect(() => { @@ -49,43 +70,108 @@ export function SearchToolbar({ } }; - return ( -
- onChange(e.target.value)} - placeholder="Find in note..." - className="w-55 h-8 text-sm" - onKeyDown={handleKeyDown} - /> - - - {totalMatches > 0 ? `${currentMatch}/${totalMatches}` : "Not found"} - + const handleReplaceKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + if (e.metaKey || e.ctrlKey || e.altKey) { + onReplaceAll(); + } else { + onReplace(); + } + } else if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + onClose(); + } else if (e.key === "Tab") { + e.stopPropagation(); + } + }; -
+ return ( +
+ {/* First Row: Find */} +
- + {isReplaceOpen ? ( + + ) : ( + + )} - - - + onChange(e.target.value)} + placeholder="Find in note..." + className="flex-1 h-8 text-sm" + onKeyDown={handleKeyDown} + /> - - - + + {totalMatches > 0 ? `${currentMatch}/${totalMatches}` : "0/0"} + + +
+ + + + + + + + + + + +
+ + {/* Second Row: Replace */} + {isReplaceOpen && ( +
+ onReplaceChange(e.target.value)} + placeholder="Replace with..." + className="flex-1 h-8 text-sm" + onKeyDown={handleReplaceKeyDown} + /> + +
+ + + + + + + +
+
+ )}
); } diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx index c2647688..756b41bd 100644 --- a/src/components/icons/index.tsx +++ b/src/components/icons/index.tsx @@ -1518,3 +1518,44 @@ export function OllamaIcon({ ); } + +export function ReplaceIcon({ className = "w-4.5 h-4.5" }: IconProps) { + return ( + + + + + + + ); +} + +export function ReplaceAllIcon({ className = "w-4.5 h-4.5" }: IconProps) { + return ( + + + + + + + + + ); +} + diff --git a/vite.config.ts b/vite.config.ts index 157677f8..2c3e2a98 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig(async () => ({ server: { port: 1420, strictPort: true, - host: host || false, + host: host || "127.0.0.1", hmr: host ? { protocol: "ws", From db19095b8b1779115a572c348d5be2ceee6622ef Mon Sep 17 00:00:00 2001 From: Nived Shaji <83071573+Nivet2006@users.noreply.github.com> Date: Tue, 23 Jun 2026 08:32:37 +0530 Subject: [PATCH 2/2] Address review comments --- src/components/editor/Editor.tsx | 23 ++++++++++++++++------- src/components/editor/SearchToolbar.tsx | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/editor/Editor.tsx b/src/components/editor/Editor.tsx index 496af0fa..d78a22bf 100644 --- a/src/components/editor/Editor.tsx +++ b/src/components/editor/Editor.tsx @@ -1308,8 +1308,14 @@ export function Editor({ }, []); const replaceCurrent = useCallback((replaceText: string) => { - if (!editor || searchMatches.length === 0) return; - const match = searchMatches[currentMatchIndex]; + if (!editor || !searchQuery.trim()) return; + + // Recompute from current doc state to avoid stale debounced matches. + const currentMatches = findMatches(searchQuery, editor); + if (currentMatches.length === 0) return; + + const safeIndex = Math.min(currentMatchIndex, currentMatches.length - 1); + const match = currentMatches[safeIndex]; if (!match) return; editor.view.dispatch( @@ -1318,16 +1324,19 @@ export function Editor({ const newMatches = findMatches(searchQuery, editor); setSearchMatches(newMatches); - + if (newMatches.length > 0) { - const nextIndex = currentMatchIndex % newMatches.length; - setCurrentMatchIndex(nextIndex); - updateSearchDecorations(newMatches, nextIndex, editor); + // Move to the first match after the replaced range. + const nextPos = match.from + replaceText.length; + const nextIndex = newMatches.findIndex((m) => m.from >= nextPos); + const resolvedIndex = nextIndex === -1 ? 0 : nextIndex; + setCurrentMatchIndex(resolvedIndex); + updateSearchDecorations(newMatches, resolvedIndex, editor); } else { setCurrentMatchIndex(0); updateSearchDecorations([], 0, editor); } - }, [editor, searchQuery, searchMatches, currentMatchIndex, findMatches, updateSearchDecorations]); + }, [editor, searchQuery, currentMatchIndex, findMatches, updateSearchDecorations]); const replaceAll = useCallback((replaceText: string) => { if (!editor || !searchQuery) return; diff --git a/src/components/editor/SearchToolbar.tsx b/src/components/editor/SearchToolbar.tsx index b9653b4d..81f128e0 100644 --- a/src/components/editor/SearchToolbar.tsx +++ b/src/components/editor/SearchToolbar.tsx @@ -74,7 +74,7 @@ export function SearchToolbar({ if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); - if (e.metaKey || e.ctrlKey || e.altKey) { + if (e.metaKey || e.ctrlKey) { onReplaceAll(); } else { onReplace(); @@ -165,7 +165,7 @@ export function SearchToolbar({