CM6 dynamically switching syntax theme w/ reconfigure

Hi. I’m trying to dynamically switch the theme with reconfigure(). I find that the view theme and the mode switch, but not the highlight theme, until I edit the text.

Here is an example. When rebuild is on, it creates a new CodeMirror view instance, and switching the theme switches the view theme and the highlight theme.

When rebuild is off, it tries to do a full reconfigure. It switches the language and the view theme just fine, but the highlight theme doesn’t change until I edit the text.

Is there something I should do to get it to update the highlighting, or might this be a bug? I tried adding changes and that works, but only if I actually change the text. I looked for a way to refresh and redraw, and didn’t find any. That’s probably best, but without a manual redraw, automatic redraw will need to work, or I will just need to recreate the view (which I am doing for now).

The code is also here:

import React, { useState, useRef, useEffect } from "react";
import {
  EditorView,
  keymap,
} from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { history, historyKeymap } from '@codemirror/history'
import { indentOnInput, LanguageSupport } from '@codemirror/language'
import { defaultKeymap } from '@codemirror/commands'
import { javascriptLanguage } from '@codemirror/lang-javascript'
import { pythonLanguage } from '@codemirror/lang-python'
import { defaultHighlightStyle } from '@codemirror/highlight'
import {
  oneDarkTheme,
  oneDarkHighlightStyle
} from '@codemirror/theme-one-dark'
import pickBy from 'lodash/pickBy'
import "./style.css";

const languageExtensions = {
  javascript: [new LanguageSupport(javascriptLanguage)],
  python: [new LanguageSupport(pythonLanguage)],
}

const themeExtensions = {
  light: [defaultHighlightStyle],
  dark: [oneDarkTheme, oneDarkHighlightStyle]
}

const exampleCode = `// JavaScript line comment
# Python line comment

for (let i=1; i <= 100; i++) {
  let s = '';
  if (i % 3 == 0) s += 'Fizz';
  if (i % 5 == 0) s += 'Buzz';
  console.log(s.length ? s : i);
}

for i in range(50):
    print(i)`;

export default function App() {
  const [rebuild, setRebuild] = useState(true)
  const [language, setLanguage] = useState('javascript');
  const [theme, setTheme] = useState('light');
  const container = useRef(null);
  const editor = useRef(null);

  useEffect(() => {
    if (container.current) {
      const extensions = [
        history(),
        indentOnInput(),
        keymap.of([
          ...defaultKeymap,
          ...historyKeymap,
        ]),
        ...languageExtensions[language],
        ...themeExtensions[theme],
      ]
      if (!editor.current) {
        editor.current = new EditorView({
          state: EditorState.create({
            doc: exampleCode,
            extensions,
          }),
          parent: container.current,
        })
      } else if (rebuild) {
        const doc = editor.current.state.doc
        editor.current.destroy()
        editor.current = new EditorView({
          state: EditorState.create({
            doc: exampleCode,
            extensions,
          }),
          parent: container.current,
        })
      } else {
        editor.current.dispatch({
          reconfigure: {
            full: extensions,
          }
        })
      }
    }
  }, [rebuild, language, theme, container, editor])

  return (
    <div>
      <p className="buttons">
      rebuild {rebuild ? 'on' : 'off'}{' '}
        <button onClick={() => setRebuild(true)}>On</button>
        <button onClick={() => setRebuild(false)}>Off</button>
      </p>
      <p className="buttons">
      {language}{' '}
        <button onClick={() => setLanguage('javascript')}>JavaScript</button>
        <button onClick={() => setLanguage('python')}>Python</button>
      </p>
      <p className="buttons">
        {theme}{' '}
        <button onClick={() => setTheme('light')}>Light</button>
        <button onClick={() => setTheme('dark')}>Dark</button>
      </p>
      <div className="code" ref={container}></div>
    </div>
  );
}

Thanks!

1 Like

Yes, that was definitely a bug. @codemirror/highlight 0.17.2 should work better.

1 Like

Thanks! I updated the example and confirmed that it works!

I’m thinking of building a playground for CodeMirror 6. It would be the only current one that I know of that allows trying out different themes and different syntaxes. Would you want to review it before I blog about it, or prefer to get an official one built first?

It’s possible to do that with two code themes and a variety of languages in https://console.resources.co/ (or with only two languages in my above example), but it isn’t presented as a playground and doesn’t document how to do it (though the code is open source).

Trying out the different syntaxes on console.resources.co paste this into the console and press enter:
{
  "js": "export default () => {\n  console.log(\"Hello, World\")\n}"
}

Then click the gray bubble that says js with the left mouse button. A menu will appear. Select View > Code > JavaScript.

Reproducing the old bug 🐛To reproduce the old bug, you'll need to grab the code and set up a build that has the old version of `@codemirror/highlight`, either by downloading it and running it yourself or by upgrading to the paid version of StackBlitz.

I’ll probably get around to a playground on the website at some point, but if you want to set one up already that’d be cool. And feel free to blog!

1 Like

Nice, this is very good idea.

1 Like

@marijn It seems like this API changed at some point but I’m having trouble finding it in the docs.

Is there an official method for reconfiguring the editor without destroying / rebuilding it?

EDIT: Seem to have found it:

editor.current.dispatch({
    effects: StateEffect.reconfigure.of(extensions)
});
4 Likes

I don’t believe that’s the ideal solution to the problem of dynamically switching the theme. If you know in advance that the theme is the only thing that needs to change, you can set up a Compartment like this example cm6-themes/example/index.ts at main · craftzdog/cm6-themes · GitHub which looks something like this:

import { EditorView, basicSetup } from 'codemirror'
import { Compartment } from '@codemirror/state'
import testDoc from './doc-example'
import themes from './themes'

const elCM = document.querySelector('#codemirror')

const themeConfig = new Compartment()

let editor = new EditorView({
  doc: testDoc,
  extensions: [
    basicSetup,

    // Yo check this out! This is the thing that we use to change themes
    themeConfig.of([themes[0]])
  ],
  parent: elCM
})

const elList = document.querySelector('#theme-list')
if (elList) {
  for (let i = 0; i < themes.length; ++i) {
    const elItem = document.createElement('option')
    elItem.setAttribute('value', i.toString())
    elItem.textContent = themes[i].name
    elList.appendChild(elItem)
  }

  elList.addEventListener('change', e => {
    if (e.currentTarget instanceof HTMLSelectElement) {
      const i = Number(e.currentTarget.value)

      // This is the part where we surgically reconfigure just one thing
      // without the need for a top-level reconfiguration.
      editor.dispatch({
        effects: themeConfig.reconfigure([themes[i]])
      })
    }
  })
}

export default editor
1 Like