← All posts
Oscar Salazar Saturday, May 14, 2022

Implementing CodeMirror 6 in react with code snippets autocompletion

Share

AUTHOR

Oscar Salazar, Senior Software Engineer

Oscar is a Software Engineer passionate about frontend development and creative coding, he has worked in several projects involving from video games to rich interactive experiences in different web applications. He loves studying and playing with the newest CSS features to create fantastic art.

See all articles

Implementing CodeMirror 6 in React

CodeMirror 6 is a complete rewrite with focus in accessibility and mobile support. Its API is not backwards compatible so we can't use any of the previous libraries.

If you are new to CodeMirror 6 go ahead and read the official documentation. It will guide you through the new modules and teach you how they work together to build a basic editor.

Base editor

First we start by creating a new ref callback, we will use this ref to get our DOM element when the component mounts.

const [element, setElement] = useState<HTMLElement>();

const ref = useCallback((node: HTMLElement | null) => {
  if (!node) return;

  setElement(node);
}, []);

Now that we have a DOM element available we can create our CodeMirror instance.

import { EditorState, EditorView, basicSetup } from "@codemirror/basic-setup";
import { javascript } from "@codemirror/lang-javascript";

useEffect(() => {
  if (!element) return;

  const view = new EditorView({
    state: EditorState.create({
      extensions: [basicSetup, javascript()],
    }),
    parent: element,
  });

  return () => view.destroy();
}, [element]);

As you can see we are using the package @codemirror/basic-setup. This is a convenient extension with the base functionality of a common code editor. You can see every extension here.

Let's wrap everything in a hook, add the ability to extend the editor and access it's state.

import React, { useCallback, useEffect, useState } from "react";
import { EditorState, EditorView, basicSetup } from "@codemirror/basic-setup";
import { Extension } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript";

export default function useCodeMirror(extensions: Extension[]) {
  const [element, setElement] = useState<HTMLElement>();

  const ref = useCallback((node: HTMLElement | null) => {
    if (!node) return;

    setElement(node);
  }, []);

  useEffect(() => {
    if (!element) return;

    const view = new EditorView({
      state: EditorState.create({
        extensions: [basicSetup, javascript(), ...extensions],
      }),
      parent: element,
    });

    return () => view?.destroy();
  }, [element]);

  return { ref };
}

We can use this hook anywhere in out application, We are going to create a reusable component.

import useCodeMirror from "./useCodeMirror";

type CodeMirrorProps = {
  extensions: Extension[];
};

const CodeMirror = ({ extensions }: CodeMirrorProps) => {
  const { ref } = useCodeMirror(extensions);

  return <div ref={ref} />;
};

export default CodeMirror;

Extensions and async autocomplete

Extensions in CodeMirror let you change and access the state of the editor internals. You can add widgets, tooltips, lint messages, highlight, get the code, etc.

We are going to use the autocomplete extension to suggest code snippets. To provide completions we are going to need a completion source. This function receives a completion context.

Completion context give us information about the current state of the editor. We can use the methods tokenBefore and matchBefore to make suggestions based in the content of the editor.

Let's create our completion source.

import {
  Completion,
  CompletionContext,
  CompletionResult,
} from "@codemirror/autocomplete";

export default async function completionSource(
  context: CompletionContext
): Promise<CompletionResult> {
  // match everything behind the editor cursor position
  const word = context.matchBefore(/.*/);

  // continue with a completion only if there is actual text
  if (word.from == word.to || word.text.trim().length <= 0) return null;

  // implement your data fetching
  const options: Completion = await fetchOptions(word.text.trim());

  return {
    from: word.from,
    options,
    filter: false,
  };
}

The previous completion source function will fetch an API to bring code suggestions. Once an option is selected CodeMirror will insert it at the from position in the editor.

By default the autocomplete extension use the context to filter the provided options. We disabled this functionality by returning filter false since we always want our options visible.

Now let's create our autocomplete extension.

import {
  autocompletion,
  closeCompletion,
  startCompletion,
} from "@codemirror/autocomplete";
import completionSource from "./completionSource";
import { debounce } from "lodash";

const debouncedStartCompletion = debounce((view) => {
  startCompletion(view);
}, 300);

function customCompletionDisplay() {
  return EditorView.updateListener.of(({ view, docChanged }) => {
    if (docChanged) {
      // when a completion is active each keystroke triggers the
      // completion source function, to avoid it we close any open
      // completion inmediatly.
      closeCompletion(view);

      debouncedStartCompletion(view);
    }
  });
}

const extensions = [
  autocompletion({
    activateOnTyping: false,
    override: [completionSource],
  }),
  customCompletionDisplay(),
];

export default autcomplete;

We need to debounce the start of a completion since most of the time we don't want to call our API with each keystroke. Thats what customCompletionDisplay will do for us.

Conclusion

CodeMirror 6 is a fantastic web code editor with touchscreen support. It's API is stable enough to use in mid-sized projects and very well documented.

You can get the useCodeMirror hook and many more useful React snippets at Codiga.

Schedule a demo

Code analyzed in seconds with Codiga Automated Code Reviews.

Write code faster with the Codiga Coding Assistant.

Schedule a Demo