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

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

is created to make the default search panel 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 });
}


1 Like