Example: Language Configuration

Language-related functionality in CodeMirror has a number of forms. The main thing you'll probably associate with language support is syntax highlighting. But there's also language-aware indentation, code folding, autocompletion, commenting, and so on.

The recommended way to distribute language support for CodeMirror is as a package that exports a syntax (usually with some low-overhead metadata like comment, indentation, and folding metadata included), an optional “support” extension (which holds optional additions like completion and linting functionality), and a convenience function that returns both of those, and is the default way for users to include in the configuration of an editor for this language.

This example sets up an editor that dynamically changes its language setup in response to the (auto-detected) language of the editor content.

Like other state changes, changing the editor configuration is done through a transaction. You may completely replace the state's configuration, but also replace just parts of this, which have to be tagged in advance.

That is the mechanism we'll use here.

In order to be able to affect transactions as they are being created (as opposed to dispatching a separate language-changing one after the change), we'll use a transaction filter that, whenever the document content changs, does a crude check (whether the doc starts with a < character) to determine whether to enable HTML or JavaScript syntax.

When the detected language disagrees with the (primary) syntax configured for the state, the transaction is extended with a reconfigure field that switches the language extensions to the appropriate ones. Since languageTag is always used as tag, if the extension previously set a language, that language is overwritten.

import {EditorState} from "@codemirror/next/state"
import {htmlSyntax, html} from "@codemirror/next/lang-html"
import {javascript} from "@codemirror/next/lang-javascript"

const languageTag = Symbol("language")

const autoLanguage = EditorState.transactionFilter.of((spec, _prev, get) => {
  if (!spec.changes) return spec
  let {state} = get()
  let docIsHTML = /^\s*</.test(state.sliceDoc(0, 100))
  let stateIsHTML = state.facet(EditorState.syntax)[0] == htmlSyntax
  if (docIsHTML == stateIsHTML) return spec
  return [spec, {
    reconfigure: {[languageTag]: docIsHTML ? html() : javascript()}
  }]
})

If we specify an initial language configuration, we must be careful to tag it with our languageTag value, so that when the extension updates the language, that part of the configuration gets replaced.

import {EditorView, basicSetup} from "@codemirror/next/basic-setup"
import {tagExtension} from "@codemirror/next/state"

new EditorView({
  state: EditorState.create({
    doc: 'console.log("hello")',
    extensions: [
      basicSetup,
      tagExtension(languageTag, javascript()),
      autoLanguage
    ]
  }),
  parent: document.querySelector("#editor")
})

The result acts like this: