Replace default search panel with my own component

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.

1 Like

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. :pray:

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.

1 Like