Example: Autocompletion

The @codemirror/autocomplete package provides functionality for displaying input suggestions in the editor. This example shows how to enable it and how to write your own completion sources.

Setting Up

Autocompletion is enabled by including the autocompletion extension (which is included in the basic setup) in your configuration. Some language packages come with support for proper autocompletion built in, such as the HTML package.

By default, the plugin will look for completions whenever the user types something, but you can configure it to only run when activated explicitly via a command.

The default completion keymap binds Ctrl-Space to start completion, arrows to select a completion, Enter to pick it, and Escape to close the tooltip. It is activated by default when you add the extension, but you can disable that if you want to provide your own bindings.

The default bindings do not bind Tab to acceptCompletion, for reasons outlined in the Tab-handling example.

Providing Completions

The completions that the extension shows come from one or more completion sources, which are functions that take a completion context—an object with information about the completion being requeststed—and return an object that describes the range that's being completed and the options to show. Sources may run asynchronously by returning a promise.

The easiest way to connect a completion source to an editor is to use the override option.

This editor uses the following completion function:

import {CompletionContext} from "@codemirror/autocomplete"

function myCompletions(context: CompletionContext) {
  let word = context.matchBefore(/\w*/)
  if (word.from == word.to && !context.explicit)
    return null
  return {
    from: word.from,
    options: [
      {label: "match", type: "keyword"},
      {label: "hello", type: "variable", info: "(World)"},
      {label: "magic", type: "text", apply: "⠁⭒*.✩.*⭒⠁", detail: "macro"}
    ]
  }
}

This is a very crude way to provide completions, without really looking at the editing context at all. But it demonstrates the basic things a completion function must do.

Completions themselves are objects with a label property, which provides both the text to show in the options list and the text to insert when the completion is picked.

By default, the completion list shows only the label. You'll usually also want to provide a type property, which determines the icon shown next to the completion. detail can be given to show a short string after the label, and info can be used for longer text, shown in a window to the side of the list when the completion is selected.

To override what happens when a completion is picked, you can use the apply property, which can be either a string to replace the completion range with, or a function that will be called to apply an arbitrary action.

When you are providing your completion source as a generic extension, or working with mixed-language documents, setting a global source is not practical. When no override is given, the plugin uses EditorState.languageDataAt with the name "autocomplete" to look up language-appropriate completion functions. Registering those is done with a language object's data facet. For example by including something like this in your state configuration:

myLanguage.data.of({
  autocomplete: myCompletions
})

You can also directly put an array of completion objects in this property, which will cause the library to simply use those (wrapped by completeFromList) as a source.

Sorting and Filtering

The trivial completion source used above didn't have to filter completions against the input—the plugin will take care of that. It uses a form of fuzzy matching to filter and rank completions against the currently typed text, and will highlight the letters in each completion that match.

To influence the ranking of completions, you can give completion objects a boost property, which adds to or subtracts from their match score.

If you really do want to filter and order completions yourself, you can include a filter: false property in your result object to disable the built-in filtering.

Completion Result Validity

Some sources need to recompute their results on every keypress, but for many of them, this is unnecessary and inefficient. They return a full list of completions for a given construct, and as long as the user is typing (or backspacing) inside that construct, that same list can be used (filtered for the currently typed input) to populate the completion list.

This is why it is very much recommended to provide a validFor property on your completion result. It should contain a function or regular expression that tells the extension that, as long as the updated input (the range between the result's from property and the completion point) matches that value, it can continue to use the list of completions.

In the myCompletions function above, since all its completions are simple words, a value like validFor: /^\w*$/ would be appropriate.

Completing from Syntax

To make a completion source a bit more intelligent, it is often useful to inspect the syntax tree around the completion point, and use that to get a better picture of what kind of construct is being completed.

As an example, this completion source for JavaScript will complete global variables and properties on global variables by inspecting the running JavaScript environment.

import {syntaxTree} from "@codemirror/language"

const completePropertyAfter = ["PropertyName", ".", "?."]
const dontCompleteIn = ["TemplateString", "LineComment", "BlockComment",
                        "VariableDefinition", "PropertyDefinition"]

function completeFromGlobalScope(context: CompletionContext) {
  let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)

  if (completePropertyAfter.includes(nodeBefore.name) &&
      nodeBefore.parent?.name == "MemberExpression") {
    let object = nodeBefore.parent.getChild("Expression")
    if (object?.name == "VariableName") {
      let from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from
      let variableName = context.state.sliceDoc(object.from, object.to)
      if (typeof window[variableName] == "object")
        return completeProperties(from, window[variableName])
    }
  } else if (nodeBefore.name == "VariableName") {
    return completeProperties(nodeBefore.from, window)
  } else if (context.explicit && !dontCompleteIn.includes(nodeBefore.name)) {
    return completeProperties(context.pos, window)
  }
  return null
}

The function starts by finding the syntax node directly in front of the completion position, which we'll base the completion behavior on.

If it is a property name or a period, and the parent node above it is a member expression, we can try to complete a property. This code only handles the case where the object is a regular variable (as opposed to, say, another member expression, or a function call).

Working with a tree like this involves knowledge about node names and tree structure. You can find the type of trees the parser creates for various constructs by looking at the parser's test cases or by converting syntax trees to strings.

If the completion happens inside a variable name, or this is an explicit completion and we're not in one of the nodes where no completion should happen, this looks up global variable names on the window object.

function completeProperties(from: number, object: Object) {
  let options = []
  for (let name in object) {
    options.push({
      label: name,
      type: typeof object[name] == "function" ? "function" : "variable"
    })
  }
  return {
    from,
    options,
    validFor: /^[\w$]*$/
  }
}

completeProperties crudely iterates an object's properties and creates a completion object for each. This could be cached with a WeakMap, or go over the object's prototypes and gather completions for that, or assign more precise types, but that's left as an exercise for the reader.

You can now use an extension like this to enable this completion source for JavaScript content.

import {javascriptLanguage} from "@codemirror/lang-javascript"

const globalJavaScriptCompletions = javascriptLanguage.data.of({
  autocomplete: completeFromGlobalScope
})

Try it out: