diff --git a/packages/editor/src/extensions/search-replace/search-replace.ts b/packages/editor/src/extensions/search-replace/search-replace.ts index 6acf330ef..5419cb454 100644 --- a/packages/editor/src/extensions/search-replace/search-replace.ts +++ b/packages/editor/src/extensions/search-replace/search-replace.ts @@ -18,7 +18,7 @@ along with this program. If not, see . */ import { Extension } from "@tiptap/core"; -import { Decoration, DecorationSet } from "prosemirror-view"; +import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { EditorState, Plugin, @@ -28,6 +28,7 @@ import { } from "prosemirror-state"; import { SearchSettings } from "../../toolbar/stores/search-store.js"; import { tiptapKeys } from "@notesnook/common"; +import { toggleNodesUnderPos } from "../heading/index.js"; type DispatchFn = (tr: Transaction) => void; declare module "@tiptap/core" { @@ -295,12 +296,11 @@ export const SearchReplace = Extension.create({ ) ); - const domNode = this.editor.view.domAtPos(from).node; - scrollIntoView(domNode); - this.storage.selectedIndex = nextIndex; + tr.setMeta("isSearching", true); tr.setMeta("selectedIndex", nextIndex); if (dispatch) updateView(state, dispatch); + return true; }, moveToPreviousResult: @@ -322,10 +322,8 @@ export const SearchReplace = Extension.create({ ) ); - const domNode = this.editor.view.domAtPos(from).node; - scrollIntoView(domNode); - this.storage.selectedIndex = prevIndex; + tr.setMeta("isSearching", true); tr.setMeta("selectedIndex", prevIndex); if (dispatch) updateView(state, dispatch); @@ -470,14 +468,90 @@ export const SearchReplace = Extension.create({ decorations(state) { return key.getState(state).results; } + }, + appendTransaction: (transactions, oldState, newState) => { + const isSearchTransaction = transactions.find((t) => + t.getMeta("isSearching") + ); + const selectedResult = + this.storage.results?.[this.storage.selectedIndex]; + if (!isSearchTransaction || !selectedResult) return; + + const tr = newState.tr; + + scrollIntoView(this.editor.view, selectedResult.from); + if (expandCollapsedParents(tr, selectedResult.from)) { + return tr; + } } }) ]; } }); -function scrollIntoView(domNode: Node) { +function expandCollapsedParents(tr: Transaction, pos: number) { + try { + let changed = false; + + const $pos = tr.doc.resolve(pos); + + for (let depth = 1; depth <= $pos.depth; depth++) { + const node = $pos.node(depth); + const nodePos = $pos.before(depth); + + if ( + (node.type.name === "callout" || + node.type.name === "outlineListItem") && + node.attrs.collapsed + ) { + tr.setNodeAttribute(nodePos, "collapsed", false); + changed = true; + } + + // expand collapsed heading that hid this node via hidden attribute + if (node.attrs.hidden) { + const parentNode = $pos.node(depth - 1); + const parentContentStart = depth === 1 ? 0 : $pos.before(depth - 1) + 1; + + let collapsedHeadingPos = -1; + let collapsedHeadingLevel = -1; + + parentNode.forEach((child, offset) => { + const childAbsPos = parentContentStart + offset; + if (childAbsPos >= nodePos) return; + if ( + child.type.name === "heading" && + child.attrs.collapsed && + !child.attrs.hidden + ) { + collapsedHeadingPos = childAbsPos; + collapsedHeadingLevel = child.attrs.level; + } + }); + + if (collapsedHeadingPos !== -1) { + tr.setNodeAttribute(collapsedHeadingPos, "collapsed", false); + toggleNodesUnderPos( + tr, + collapsedHeadingPos, + collapsedHeadingLevel, + false + ); + changed = true; + } + } + } + + if (changed) tr.setMeta("preventSave", true); + return changed; + } catch (e) { + console.error("Error expanding collapsed parents: ", e); + } +} + +function scrollIntoView(view: EditorView, pos: number) { setTimeout(() => { + const domNode = view.domAtPos(pos).node; if ("scrollIntoView" in domNode) { (domNode as Element).scrollIntoView({ behavior: "instant", diff --git a/packages/editor/src/toolbar/popups/search-replace.tsx b/packages/editor/src/toolbar/popups/search-replace.tsx index f2c463c9e..b55a55ea7 100644 --- a/packages/editor/src/toolbar/popups/search-replace.tsx +++ b/packages/editor/src/toolbar/popups/search-replace.tsx @@ -25,6 +25,7 @@ import { ToolButton } from "../components/tool-button.js"; import { Editor } from "../../types.js"; import { useEditorSearchStore } from "../stores/search-store.js"; import { strings } from "@notesnook/intl"; +import { inlineDebounce } from "@notesnook/common"; export type SearchReplacePopupProps = { editor: Editor }; export function SearchReplacePopup(props: SearchReplacePopupProps) { @@ -94,7 +95,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) { sx={{ p: 0, fontFamily: "monospace" }} value={searchTerm} onChange={(e) => { - search(e.target.value); + inlineDebounce("search", () => search(e.target.value), 100); useEditorSearchStore.setState({ searchTerm: e.target.value }); }} onKeyDown={(e) => {