Simple extension example?

Hey everybody.

I’m having trouble understanding how to use the library. I’m thankful for the examples provided in the docs, but can someone provide one or two more examples for me?

For example, how would I implement an extension that watches for a code block opening “```” and simply inserts “\n```” after the cursor to automatically close that block for you?

Thanks!

1 Like

Hi! I wouldn’t consider myself a CodeMirror expert, but I’ve struggled with creating my first plugins too and would love to try and help.
First, I think the best approach to your plugin depends on what exactly you want it to do - for example, should the insertion trigger only when the user types three backticks in a row, or when a backtick is inserted and there are another two behind it?
The difference may seem minor, but may produce different results when you consider copy/pasting, for example. More importantly - I think the implementation of the two options above differs greatly; in fact, I’m not sure how to implement the first option.

To implement the second option, I’d try a keymap extension - an example in the docs is the underlining command.
The key field should be a backtick. When it’s pressed, determine the current cursor/selection from the view’s state (passed to run), and check if it’s preceded by two backticks (one way to do that is state.doc.sliceString()).
If it is, use view’s dispatch to insert the text at the current position, and return true. Otherwise, return false.

I hope this helps!

Thanks for the response! Funny enough, I’m actually looking to do something closer to the first option you described. This would function similar to an “autocomplete” for me. And to be honest, it isn’t really even an important thing, this is more of a learning exercise. I’ve spent a significant amount of time today just trying to understand how to do this one basic thing. I can’t seem to find an example anywhere on github or in the docs that do something similar to this.

My original plan for implementing it after scanning over the documentation on the state module was this; create a statefield to act as a “buffer”. When the document is changed, determine if a single character was added and store that character in the buffer. Use my view’s “onchange” callback to poll the buffer, and if it matches “```” then push the contents to the page immediately after the cursor and empty the buffer.

I’m able to hack something like this together, but I get the impression that I am going about it the wrong way.

The way I’d approach this is to register an input handler, and when the user inserts a backtick at a place that has two backticks before it and doesn’t look (in the syntax tree) like it would close a code block, insert "`\n```" instead, and put the cursor after the first backtick. The bracket closing and automatic tag closing work in a similar way.

I reviewed the documentation on the facet you linked and I was able to implement it this way. It does feel better.

I’m including a full working example for anyone with a similar issue. Keep in mind, this doesn’t handle all edge cases. For example, if you have a pair of fences and you delete the very last backtick and then replace it, another set of fences will be inserted.

However, several larger projects like Obsidian that use CodeMirror seem to have this exact same issue. I’m not sure how I would prevent it.

This example uses React and Typescript, but should still be easy to understand.

import {useEffect, useState, useRef} from 'react';
import type {SelectionRange} from '@codemirror/state';
import {EditorState} from '@codemirror/state';
import {EditorView, keymap, highlightActiveLine} from '@codemirror/view';
import {defaultKeymap} from '@codemirror/commands';
import {indentOnInput} from '@codemirror/language';
import {markdown, markdownLanguage} from '@codemirror/lang-markdown';

import type {ViewUpdate} from '@codemirror/view';
import type React from 'react';

interface Props {
  doc: string;
  onChange?: (state: EditorState) => void;
}

const useCodeMirror = <T extends Element>({
  doc,
  onChange,
}: Props): [React.MutableRefObject<T | null>, EditorView?] => {
  const refContainer = useRef<T>(null);
  const [editorView, setEditorView] = useState<EditorView>();

  useEffect(() => {
    if (!refContainer.current) return;

    const startState = EditorState.create({
      doc,
      extensions: [
        keymap.of([...defaultKeymap]),
        indentOnInput(),
        highlightActiveLine(),
        markdown({
          base: markdownLanguage,
          addKeymap: true,
        }),
        EditorView.lineWrapping,
        EditorView.inputHandler.of(inputHandler),
        EditorView.updateListener.of(update => updateListener(update, onChange)),
      ],
    });

    const view = new EditorView({
      state: startState,
      parent: refContainer.current,
    });
    setEditorView(view);
  }, [refContainer]);

  return [refContainer, editorView];
};

export default useCodeMirror;

/**
 * Runs when changes to the DOM are detected.
 *
 * @param view The EditorView of the event.
 * @param from The selection 'from' index.
 * @param to The selection 'to' index.
 * @param text The document change.
 * @returns Returns true when an action was taken, and false otherwise.
 */
const inputHandler = (view: EditorView, from: number, to: number, text: string): boolean => {
  // Return false to allow default action, or true to block it.
  let result = false;
  result = matchBlocks(view, text);

  return result;
};

/**
 * If the given text is '`' and conditions are met, insert a matching ending
 * codeblock '```' and move the cursor into the middle.
 *
 * @param view The EditorView associated with the event.
 * @param text The inserted text.
 * @returns Returns true when an action was taken, and false otherwise.
 */
const matchBlocks = (view: EditorView, text: string): boolean => {
  const selection = view.state.selection.main;
  if (text != '`' || !isCursor(selection)) return false;

  const state = view.state;
  const position = selection.from;
  if (
    /// Previous two characters should be backticks,
    // but if the third is also a backtick ignore it to prevent repeat triggers.
    state.sliceDoc(position - 2, position) == '``' &&
    state.sliceDoc(position - 3, position - 2) != '`'
  ) {
    view.dispatch({
      changes: [{from: position, insert: '`\n\n```'}],
      selection: {anchor: position + 2},
    });
    return true;
  }

  return false;
};

/**
 * Determines if the given SelectionRange is a cursor.
 *
 * @param selection The SelectionRange to check.
 * @returns Returns true when `selection.from == selection.to`.
 */
const isCursor = (selection: SelectionRange): boolean => selection.from == selection.to;

/**
 * Runs when the associated EditorView is updated.
 *
 * @param update The ViewUpdate of the event.
 * @param onChange A function to run when the event fires.
 */
const updateListener = (
  update: ViewUpdate,
  onChange: ((state: EditorState) => void) | undefined,
) => {
  if (update.changes) {
    onChange && onChange(update.state);
  }
};

The above code produces this behavior:

Typing “```” will leave you with this text, where ‘n’ represents the cursor position:

```
n
```

Thanks for the help.

That’s why I mentioned the syntax tree. Assuming the document is being parsed as Markdown, the tree will have a code block starting before (and covering) the pair of backticks if they can close a block.