or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

bidi.mdbuiltin-extensions.mddecorations.mdeditor-view.mdextensions.mdgutters.mdindex.mdkeybindings.mdlayout.mdpanels.mdtooltips.md
tile.json

extensions.mddocs/

Extension System

CodeMirror's extension system provides a powerful plugin architecture for adding functionality to editors. ViewPlugins can manage state, handle events, and integrate with the editor's lifecycle.

ViewPlugin Class

class ViewPlugin<T> {
  static define<T>(
    create: (view: EditorView) => T,
    spec?: PluginSpec<T>
  ): ViewPlugin<T>;
  
  static fromClass<T>(
    cls: {new(view: EditorView): T},
    spec?: PluginSpec<T>
  ): ViewPlugin<T>;
}

Plugin Value Interface

interface PluginValue {
  update?(update: ViewUpdate): void;
  destroy?(): void;
}

Plugin Specification

interface PluginSpec<T> {
  eventHandlers?: DOMEventHandlers<T>;
  eventObservers?: DOMEventHandlers<T>;
  provide?: (plugin: ViewPlugin<T>) => Extension;
  decorations?: (value: T) => DecorationSet;
}

ViewUpdate Class

class ViewUpdate {
  readonly view: EditorView;
  readonly state: EditorState;
  readonly transactions: readonly Transaction[];
  readonly changes: ChangeSet;
  readonly startState: EditorState;
  
  // Computed properties
  readonly docChanged: boolean;
  readonly focusChanged: boolean;
  readonly selectionSet: boolean;
  readonly viewportChanged: boolean;
  readonly heightChanged: boolean;
  readonly geometryChanged: boolean;
}

Command Type

type Command = (target: EditorView) => boolean;

Key Extension Facets

const clickAddsSelectionRange: Facet<(event: MouseEvent) => boolean>;
const dragMovesSelection: Facet<(event: MouseEvent) => boolean>;
const mouseSelectionStyle: Facet<MakeSelectionStyle>;
const exceptionSink: Facet<(exception: any) => void>;
const updateListener: Facet<(update: ViewUpdate) => void>;
const inputHandler: Facet<(view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean>;
const focusChangeEffect: Facet<(state: EditorState, focusing: boolean) => StateEffect<any> | null>;
const scrollHandler: Facet<(view: EditorView, range: SelectionRange, options: ScrollOptions) => boolean>;

Extension Utilities

function logException(state: EditorState, exception: any, context?: string): void;

const decorations: Facet<DecorationSet>;
const outerDecorations: Facet<DecorationSet>;
const atomicRanges: Facet<(view: EditorView) => DecorationSet>;
const scrollMargins: Facet<(view: EditorView) => Partial<Rect>>;
const styleModule: Facet<StyleModule>;
const contentAttributes: Facet<AttrSource>;
const editorAttributes: Facet<AttrSource>;
const editable: Facet<boolean>;

Usage Examples

Creating a Simple Plugin

import { ViewPlugin, ViewUpdate, EditorView } from "@codemirror/view";

const myPlugin = ViewPlugin.define((view: EditorView) => {
  console.log("Plugin initialized");
  
  return {
    update(update: ViewUpdate) {
      if (update.docChanged) {
        console.log("Document changed");
      }
      if (update.selectionSet) {
        console.log("Selection changed");
      }
    },
    
    destroy() {
      console.log("Plugin destroyed");
    }
  };
});

// Use the plugin
const view = new EditorView({
  state: EditorState.create({
    extensions: [myPlugin]
  }),
  parent: document.body
});

Plugin with Event Handlers

const eventHandlerPlugin = ViewPlugin.define(() => ({}), {
  eventHandlers: {
    mousedown(event, view) {
      console.log("Mouse down at", event.clientX, event.clientY);
      return false; // Don't prevent default
    },
    
    keydown(event, view) {
      if (event.key === "Tab") {
        // Handle tab key
        view.dispatch(view.state.replaceSelection("  "));
        return true; // Prevent default
      }
      return false;
    }
  }
});

Plugin with Decorations

import { Decoration, DecorationSet } from "@codemirror/view";

const highlightPlugin = ViewPlugin.define((view) => {
  return {
    decorations: DecorationSet.empty,
    
    update(update: ViewUpdate) {
      if (update.docChanged || update.viewportChanged) {
        this.decorations = this.buildDecorations(update.view);
      }
    },
    
    buildDecorations(view: EditorView) {
      const decorations = [];
      const text = view.state.doc.toString();
      
      // Highlight all instances of "TODO"
      let match;
      const regex = /TODO/g;
      while ((match = regex.exec(text)) !== null) {
        decorations.push(
          Decoration.mark({ class: "todo-highlight" })
            .range(match.index, match.index + match[0].length)
        );
      }
      
      return Decoration.set(decorations);
    }
  };
}, {
  decorations: (value) => value.decorations
});

Class-based Plugin

class StatusBarPlugin implements PluginValue {
  statusBar: HTMLElement;
  
  constructor(view: EditorView) {
    this.statusBar = document.createElement("div");
    this.statusBar.className = "cm-status-bar";
    this.updateStatus(view);
  }
  
  update(update: ViewUpdate) {
    if (update.docChanged || update.selectionSet) {
      this.updateStatus(update.view);
    }
  }
  
  updateStatus(view: EditorView) {
    const selection = view.state.selection.main;
    const line = view.state.doc.lineAt(selection.head);
    this.statusBar.textContent = `Line ${line.number}, Column ${selection.head - line.from + 1}`;
  }
  
  destroy() {
    this.statusBar.remove();
  }
}

const statusBarPlugin = ViewPlugin.fromClass(StatusBarPlugin, {
  provide: (plugin) => showPanel.of(() => ({
    dom: plugin.value?.statusBar || document.createElement("div"),
    top: false
  }))
});

Command Implementation

const insertTimeCommand: Command = (view) => {
  const now = new Date().toLocaleTimeString();
  const selection = view.state.selection.main;
  
  view.dispatch({
    changes: {
      from: selection.from,
      to: selection.to,
      insert: now
    },
    selection: {
      anchor: selection.from + now.length
    }
  });
  
  return true; // Command succeeded
};

// Use with keymap
const timeKeymap = keymap.of([
  { key: "Ctrl-t", run: insertTimeCommand }
]);

Update Listener

const updateLogger = updateListener.of((update: ViewUpdate) => {
  console.log("Update:", {
    docChanged: update.docChanged,
    selectionSet: update.selectionSet,
    focusChanged: update.focusChanged,
    viewportChanged: update.viewportChanged,
    heightChanged: update.heightChanged
  });
  
  if (update.transactions.length > 0) {
    console.log("Transactions:", update.transactions.length);
  }
});

Exception Handling

const errorHandler = exceptionSink.of((exception) => {
  console.error("CodeMirror error:", exception);
  // Could send to error reporting service
});

// Log an exception manually
logException(view.state, new Error("Something went wrong"), "my-plugin");

Input Handler

const customInputHandler = inputHandler.of((view, from, to, text, insert) => {
  // Handle auto-closing brackets
  if (text === "(") {
    view.dispatch({
      changes: [{from, to, insert: "()"}],
      selection: {anchor: from + 1}
    });
    return true; // Handled
  }
  
  return false; // Let default handler run
});