-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement find and replace functionality #7
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
Changes from all commits
03ea892
89f9e7b
c4c396a
6d6f176
4a21d3a
24dab4a
f8a98f4
1cbf60d
b198652
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # Guide | ||
|
|
||
| ## Avaliable commands | ||
|
|
||
| ```bash | ||
| bun lint | ||
| bun format | ||
| bun run test | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,157 @@ | ||
| import type { JSX } from "solid-js"; | ||
|
|
||
| import { createEffect, type JSX } from "solid-js"; | ||
| import { escapeHtml } from "#lib/escape-html"; | ||
| import { getMatches } from "#lib/get-matches"; | ||
| import type { EditorSettings } from "#types"; | ||
|
|
||
| function Editor(props: { | ||
| interface EditorProps { | ||
| content: string; | ||
| onChange: (content: string) => void; | ||
| settings: EditorSettings; | ||
| }): JSX.Element { | ||
| searchTerm?: string; | ||
| caseSensitive?: boolean; | ||
| currentMatchIndex?: number; | ||
| isSearchOpen?: boolean; | ||
| } | ||
|
|
||
| function setCaretPosition(element: HTMLDivElement, position: number) { | ||
| const range = document.createRange(); | ||
| const selection = window.getSelection(); | ||
|
|
||
| let charCount = 0; | ||
| let found = false; | ||
|
|
||
| function traverseNodes(node: Node) { | ||
| if (found) return; | ||
|
|
||
| if (node.nodeType === Node.TEXT_NODE) { | ||
| const nextCount = charCount + (node.textContent?.length ?? 0); | ||
| if (position <= nextCount) { | ||
| range.setStart(node, position - charCount); | ||
| range.setEnd(node, position - charCount); | ||
| found = true; | ||
| } | ||
| charCount = nextCount; | ||
| } else { | ||
| for (const child of Array.from(node.childNodes)) { | ||
| traverseNodes(child); | ||
| if (found) return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| traverseNodes(element); | ||
|
|
||
| if (!found) { | ||
| range.selectNodeContents(element); | ||
| range.collapse(false); | ||
| } | ||
|
|
||
| selection?.removeAllRanges(); | ||
| selection?.addRange(range); | ||
| } | ||
|
|
||
| function Editor(props: EditorProps): JSX.Element { | ||
| let editorRef: HTMLDivElement | undefined; | ||
|
|
||
| const renderContent = (): string => { | ||
| const text = props.content; | ||
| if (!props.searchTerm || !text) return escapeHtml(text); | ||
|
|
||
| const escaped = escapeHtml(text); | ||
| const searchEscaped = RegExp.escape(escapeHtml(props.searchTerm)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result:
Browser support (from “Can I use”): [1]
MDN summarizes this as becoming broadly available across “latest” browsers since May 2025. [2] Sources: Citations:
Add a polyfill or compatibility check for
🤖 Prompt for AI Agents |
||
| const flags = props.caseSensitive ? "" : "i"; | ||
| const regex = new RegExp(`(${searchEscaped})`, `g${flags}`); | ||
|
|
||
| return escaped.replace( | ||
| regex, | ||
| '<mark class="bg-yellow-300 dark:bg-yellow-600 rounded px-0.5">$1</mark>', | ||
| ); | ||
| }; | ||
|
Comment on lines
+56
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unify the matching rules used for highlights and indexes.
🔧 Minimal fix if overlaps are not intended while (found !== -1) {
matches.push({ start: found, end: found + searchTerm.length });
- pos = found + 1;
+ pos = found + searchTerm.length;
found = searchContent.indexOf(search, pos);
}Also applies to: 172-186 🤖 Prompt for AI Agents |
||
|
|
||
| const handleInput = () => { | ||
| if (!editorRef) return; | ||
| const text = editorRef.innerText; | ||
| props.onChange(text); | ||
| }; | ||
|
|
||
| createEffect(() => { | ||
| if (!editorRef) return; | ||
| const selection = window.getSelection(); | ||
| let savedOffset = 0; | ||
| let hadSelection = false; | ||
|
|
||
| if (selection && selection.rangeCount > 0) { | ||
| const range = selection.getRangeAt(0); | ||
| const preCaretRange = range.cloneRange(); | ||
| preCaretRange.selectNodeContents(editorRef); | ||
| preCaretRange.setEnd(range.endContainer, range.endOffset); | ||
| savedOffset = preCaretRange.toString().length; | ||
| hadSelection = true; | ||
| } | ||
|
|
||
| const html = renderContent(); | ||
| if (html !== editorRef.innerHTML) { | ||
| editorRef.innerHTML = html; | ||
| } | ||
|
|
||
| if (document.activeElement === editorRef && hadSelection) { | ||
| setCaretPosition(editorRef, savedOffset); | ||
| } | ||
| }); | ||
|
|
||
| createEffect(() => { | ||
| if (editorRef && props.searchTerm) { | ||
| const matches = getMatches( | ||
| props.content, | ||
| props.searchTerm, | ||
| props.caseSensitive, | ||
| ); | ||
| if (matches.length > 0 && props.currentMatchIndex !== undefined) { | ||
| const match = matches[props.currentMatchIndex]; | ||
| if (match && document.activeElement === editorRef) { | ||
| setCaretPosition(editorRef, match.start); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
|
Comment on lines
+102
to
+116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Effect reacts to content changes, causing caret to jump during typing. This effect tracks Consider using a previous-value pattern to only navigate when 🔧 Proposed fix using previous index tracking+import { createEffect, createSignal, type JSX } from "solid-js";
...
function Editor(props: EditorProps): JSX.Element {
let editorRef: HTMLDivElement | undefined;
+ const [prevMatchIndex, setPrevMatchIndex] = createSignal<number | undefined>(undefined);
// ... existing code ...
createEffect(() => {
- if (editorRef && props.searchTerm) {
+ const currentIndex = props.currentMatchIndex;
+ const searchTerm = props.searchTerm;
+
+ if (editorRef && searchTerm && currentIndex !== undefined && currentIndex !== prevMatchIndex()) {
+ setPrevMatchIndex(currentIndex);
const matches = getMatches(
props.content,
- props.searchTerm,
+ searchTerm,
props.caseSensitive,
);
- if (matches.length > 0 && props.currentMatchIndex !== undefined) {
- const match = matches[props.currentMatchIndex];
+ if (matches.length > 0) {
+ const match = matches[currentIndex];
if (match) {
setCaretPosition(editorRef, match.start);
}
}
}
});🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <main> | ||
| <textarea | ||
| class="w-full resize-none [outline:none] absolute h-screen overflow-y-auto overflow-x-hidden | ||
| <div | ||
| ref={editorRef} | ||
| contentEditable={true} | ||
| spellcheck={props.settings.spellcheck} | ||
| onInput={handleInput} | ||
| class="w-full absolute h-screen overflow-y-auto overflow-x-hidden | ||
| [word-break:break-word] text-black dark:text-white caret-blue-500 | ||
| bg-transparent delay-500 [scrollbar-width:thin] scroll-smooth | ||
| bg-transparent [scrollbar-width:thin] scroll-smooth | ||
| p-[calc(min(1em,20vh)+72px)_max(-372px+50vw,1em)_min(5em,15vh)] | ||
| scroll-pb-0 left-0 top-0" | ||
| scroll-pb-0 left-0 top-0 outline-none whitespace-pre-wrap" | ||
| style={{ | ||
| "font-size": `${props.settings.fontSize}px`, | ||
| "font-family": props.settings.fontFamily, | ||
| "text-align": props.settings.textAlign, | ||
| }} | ||
| spellcheck={props.settings.spellcheck} | ||
| onInput={(e) => props.onChange(e.currentTarget.value)} | ||
| value={props.content} | ||
| placeholder="Start writing..." | ||
| data-placeholder="Start writing..." | ||
| /> | ||
|
Comment on lines
+120
to
136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if there's any existing mechanism to preserve cursor position during typing
rg -n "selection|caret|cursor" src/components/Editor.tsxRepository: rmandotdev/editor Length of output: 259 🏁 Script executed: wc -l src/components/Editor.tsxRepository: rmandotdev/editor Length of output: 90 🏁 Script executed: cat -n src/components/Editor.tsxRepository: rmandotdev/editor Length of output: 5045 Setting When the user types, Consider using a diffing approach to update only the changed text nodes, or only update 🤖 Prompt for AI Agents |
||
| <style>{` | ||
| [contenteditable]:empty:before { | ||
| content: attr(data-placeholder); | ||
| color: #999; | ||
| } | ||
| mark { | ||
| background-color: #fde047; | ||
| border-radius: 2px; | ||
| padding: 0 2px; | ||
| margin: 0 -1px; | ||
| box-decoration-break: clone; | ||
| } | ||
| @media (prefers-color-scheme: dark) { | ||
| mark { | ||
| background-color: #ca8a04; | ||
| } | ||
| } | ||
| `}</style> | ||
| </main> | ||
| ); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.