I need to replace the default search component with my own custom component but still use the same search related functionalities of the codemirror library. I found out that createPanel
allows doing this. But the docs aren’t clear enough on how to utilize the search functionalities on the new component that i build.
The implementation of the built-in search panel is probably a good place to start.
search_pannel.jsx → My custom search panel
import React from "react";
import { SearchQuery, selectMatches } from "@codemirror/search";
export default function SearchPanel({ view }) {
return (
<div className="search-panel">
<input/>
<button
type={BUTTON_TYPES.TERTIARY}
size={BUTTON_SIZES.SMALL}
onClick={() => {
let searchQuery = new SearchQuery({
search: "visual"
});
selectMatches(view, searchQuery);
}}
>
Search
</button>
</div>
);
}
Editor usage
let defaultExtensions = [
basicSetup,
search({
createPanel: (view) => {
const dom = document.createElement("div");
render(<SearchPanel view={view} />, dom);
return { dom };
}
})
];
const view = new EditorView({
extensions: defaultExtensions,
doc: data,
parent: element
});
This is my current setup. But on clicking the button which triggers selectMatches, nothing gets highlighted in my editor. But the file contains the text which i have passed in search (“visual”)
Am i wrong somewhere? @marijn
selectMatches
is documented to be a command, which takes a single argument, so calling it with two arguments is unlikely to do what you want. You may be looking for setSearchQuery
, but I don’t want to explain every step of the way. It might help to copy the code for the default search panel and adjust that piece by piece, following its general structure.
is it possible to still search and highlight matched texts without the search panel?
If you find some way to let your user enter a query, you can use setSearchQuery
and findNext
without a panel. The highlighting of matches is only active when the panel is open, though.
Thanks! Yes, I did use the two, but now I see it must be a React state issue I’m running into so it’s not working… but thank you! At least I know i’m going in the right direction.
I kept arriving at this post and tried a variety of things to customize a search panel but in the end didn’t have the control I wanted. Luckily the search API that is exposed works great (as long as you openSearchPanel to start). Here is a quick example that will hopefully help others get started:
import React, { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { Button } from "../ui/button"
import { useStore } from "@/store/document-provider"
import { observer } from "mobx-react-lite"
import {
openSearchPanel,
setSearchQuery,
SearchQuery,
findNext,
findPrevious,
replaceNext,
replaceAll,
selectMatches,
} from "@codemirror/search"
type FindFormValues = {
search?: string
replace?: string
caseSensitive?: boolean
regexp?: boolean
wholeWord?: boolean
}
// Note: within the default extensions for my editor I have added:
//
// import { search, searchKeymap } from "@codemirror/search"
//
// ...
// search({ top: true }),
// keymap.of([...searchKeymap]),
// ...
//
const Find = () => {
// I'm using mobx to get the view from the store, but you can use any other state management library
// or just pass the view as a prop
const store = useStore()
const [query, setQuery] = useState<SearchQuery | null>(null)
useEffect(() => {
if (!store.view) return
const view = store.view
// Note: you have to open the search panel to make the search work and highlight the matches
// If you don't want the search panel visible, you can hide it with CSS
//
// .cm-panels-top {
// border: 0 !important;
// }
// .cm-search {
// display: none;
// }
openSearchPanel(view)
}, [store.view])
const { register, handleSubmit, watch } = useForm({
defaultValues: {
search: "",
replace: "",
caseSensitive: false,
regexp: false,
wholeWord: false,
},
})
const onSubmit = async (data: FindFormValues) => {
const view = store.view
const newQuery = new SearchQuery({
search: data.search,
caseSensitive: data.caseSensitive,
regexp: data.regexp,
wholeWord: data.wholeWord,
replace: data.replace,
})
view.dispatch({ effects: setSearchQuery.of(newQuery) })
if (!query || !query.eq(newQuery)) {
setQuery(newQuery)
view.dispatch({ effects: setSearchQuery.of(newQuery) })
}
}
watch((data) => {
onSubmit(data)
})
return (
<div className="bg-rose fixed z-10 p-2">
<form className="w-full" onSubmit={handleSubmit(onSubmit)}>
<div className="mt-8">
<div className="m-1">
<input
id="search"
placeholder="Find"
{...register("search")}
className="placeholder-gray-500 text-xs w-full h-8 p-1 border-1 outline-none appearance-none focus:outline-none focus:placeholder-gray-400 transition duration-150 ease-in-out"
/>
</div>
<div className="m-1">
<input
id="replace"
placeholder="Replace"
{...register("replace")}
className="placeholder-gray-500 text-xs w-full h-8 p-1 border-1 outline-none appearance-none focus:outline-none focus:placeholder-gray-400 transition duration-150 ease-in-out"
/>
</div>
<div className="m-1">
<label htmlFor="caseSensitive" className="text-xs">
<input id="caseSensitive" type="checkbox" {...register("caseSensitive")} className="mr-1" />
Match case
</label>
<label htmlFor="regexp" className="text-xs">
<input id="regexp" type="checkbox" {...register("regexp")} className="mr-1" />
Regular expression
</label>
<label htmlFor="wholeWord" className="text-xs">
<input id="wholeWord" type="checkbox" {...register("wholeWord")} className="mr-1" />
Whole word
</label>
</div>
</div>
<div className="space-y-2 mt-2">
<Button
className="mx-1"
onClick={(event) => {
event.preventDefault()
findNext(store.view)
}}>
Next
</Button>
<Button
className="mx-1"
onClick={(event) => {
event.preventDefault()
findPrevious(store.view)
}}>
Previous
</Button>
<Button
className="mx-1"
onClick={(event) => {
event.preventDefault()
selectMatches(store.view)
}}>
All
</Button>
<Button
className="mx-1"
onClick={(event) => {
event.preventDefault()
replaceNext(store.view)
}}>
Replace
</Button>
<Button
className="mx-1"
onClick={(event) => {
event.preventDefault()
replaceAll(store.view)
}}>
Replace All
</Button>
</div>
</form>
</div>
)
}
export default observer(Find)
Note, you don’t have to use mobx (or the HOC observer)… you can just pass the view as props.
Hi Everyone,
I’ve encountered this discussion, and would like to offer my solution. This code customizes search in a CodeMirror editor by removing the default panel, enabling case insensitivity, and scrolling to match occurrences. Additionally, an empty
Furthermore, the code includes functions for replacing the next occurrence of the search query (replaceNext) and replacing all occurrences (replaceAll). Additionally, there are functions for undoing (undoEditor) and redoing (redoEditor) changes in the editor.
This can be further enhanced and applied to other use cases.
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { undo, redo } from "@codemirror/commands";
import {
search,
openSearchPanel,
replaceAll,
SearchQuery,
setSearchQuery,
replaceNext,
} from "@codemirror/search";
// Remove default search options
const removeDefaultSearch = search({
caseSensitive: false, // Ignore case sensitivity
scrollToMatch: range => EditorView.scrollIntoView(range), // Scroll to the match
createPanel: view => ({
// Create an empty search panel
dom: document.createElement("div")
})
});
// Create an editor state with basic setup and custom search options
const state = EditorState.create({
doc: `function krishna(){
console.log("Krishna");
}`,
extensions: [
basicSetup,
removeDefaultSearch // Apply custom search options
]
});
// Create an EditorView with the defined state
const editorView = new EditorView({
parent,
state
});
// Function to perform search in the editor
function searchInEditor(editorView: EditorView, query: string) {
// Open the search panel
openSearchPanel(view);
const newQuery = new SearchQuery({
search: query, // Set the search query
caseSensitive: false, // Ignore case sensitivity
regexp: false, // Do not use regular expressions
wholeWord: false, // Search for whole words only
replace: "${replace word from state or input}" // Specify replacement text
});
// Apply the new search query
editorView.dispatch({ effects: setSearchQuery.of(newQuery) });
}
// Function to replace the next occurrence of the search query
replaceNext(view);
// Function to replace all occurrences of the search query
replaceAll(view);
// Function to undo changes in the editor
function undoEditor(view: EditorView) {
const state = view.state;
const dispatch = view.dispatch;
undo({ state, dispatch });
}
// Function to redo changes in the editor
function redoEditor(view: EditorView) {
const state = view.state;
const dispatch = view.dispatch;
redo({ state, dispatch });
}