Skip to main content

Overview

Polaris IDE’s code editor is built on CodeMirror 6, a powerful and extensible editor framework. This guide shows you how to create custom extensions to enhance the editor.

CodeMirror 6 Architecture

CodeMirror 6 uses a functional, immutable architecture:

State

Immutable document and editor state

Transactions

State changes via transactions

Extensions

Modular functionality plugins

Core Concepts

import { EditorState, StateField, StateEffect } from "@codemirror/state";
import { EditorView, ViewPlugin, Decoration } from "@codemirror/view";

// State: What the editor knows
const state = EditorState.create({
  doc: "Hello, world!",
  extensions: [/* extensions */]
});

// Transaction: How state changes
view.dispatch({
  changes: { from: 0, insert: "// " },
  effects: customEffect.of(value)
});

// Extension: What the editor can do
const myExtension = StateField.define({ /* ... */ });

Existing Extensions in Polaris

Polaris includes several custom extensions you can learn from:
Location: src/features/editor/extensions/suggestion/index.tsProvides ghost text suggestions as you type:
// Key components:
- StateField: Stores suggestion text
- StateEffect: Updates suggestion
- ViewPlugin: Fetches suggestions on typing
- WidgetType: Renders ghost text
- Keymap: Tab to accept
View Source

Creating Your First Extension

Let’s build a simple extension that highlights TODO comments:

Step 1: Set Up Extension File

Create src/features/editor/extensions/todo-highlighter.ts:
import { Extension } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";

// Define decoration style
const todoMark = Decoration.mark({
  class: "cm-todo",
  attributes: { style: "background-color: rgba(255, 200, 0, 0.3); font-weight: bold;" }
});

// Create ViewPlugin to find TODOs
const todoHighlighter = ViewPlugin.fromClass(
  class {
    decorations: DecorationSet;

    constructor(view: EditorView) {
      this.decorations = this.buildDecorations(view);
    }

    update(update: ViewUpdate) {
      if (update.docChanged || update.viewportChanged) {
        this.decorations = this.buildDecorations(update.view);
      }
    }

    buildDecorations(view: EditorView): DecorationSet {
      const decorations: any[] = [];
      const doc = view.state.doc;

      for (let i = 1; i <= doc.lines; i++) {
        const line = doc.line(i);
        const todoMatch = /\/\/\s*TODO:?/gi.exec(line.text);
        
        if (todoMatch) {
          decorations.push(
            todoMark.range(
              line.from + todoMatch.index,
              line.from + todoMatch.index + todoMatch[0].length
            )
          );
        }
      }

      return Decoration.set(decorations);
    }
  },
  {
    decorations: (plugin) => plugin.decorations,
  }
);

// Export extension
export const todoHighlight = (): Extension => [
  todoHighlighter
];

Step 2: Register Extension

Add to src/features/editor/components/code-editor.tsx:
import { todoHighlight } from "../extensions/todo-highlighter";

const extensions = [
  // ... existing extensions
  todoHighlight(),
];

Step 3: Test It

Type in the editor:
// TODO: Fix this bug
function example() {
  // TODO: Add error handling
}
The TODO comments should now be highlighted!

Advanced Extension Patterns

State Management with StateField

Create stateful extensions:
import { StateField, StateEffect } from "@codemirror/state";

// Define effect to update state
const setBookmarkEffect = StateEffect.define<{ pos: number; label: string }>();

// Define state field
interface Bookmark {
  pos: number;
  label: string;
}

const bookmarkState = StateField.define<Bookmark[]>({
  create() {
    return [];
  },
  
  update(bookmarks, transaction) {
    // Update positions when document changes
    bookmarks = bookmarks.map(b => ({
      ...b,
      pos: transaction.changes.mapPos(b.pos)
    }));
    
    // Process effects
    for (const effect of transaction.effects) {
      if (effect.is(setBookmarkEffect)) {
        bookmarks = [...bookmarks, effect.value];
      }
    }
    
    return bookmarks;
  }
});

// Usage in code
view.dispatch({
  effects: setBookmarkEffect.of({ pos: 100, label: "Important" })
});

Custom Widgets

Render custom DOM elements:
import { WidgetType } from "@codemirror/view";

class LineBreakWidget extends WidgetType {
  toDOM() {
    const elem = document.createElement("div");
    elem.className = "cm-line-break";
    elem.innerHTML = `
      <div style="
        border-top: 2px dashed #888;
        margin: 10px 0;
        position: relative;
      ">
        <span style="
          background: var(--background);
          padding: 0 10px;
          position: absolute;
          top: -10px;
          left: 50%;
          transform: translateX(-50%);
        ">--- Break ---</span>
      </div>
    `;
    return elem;
  }
}

// Use in decorations
Decoration.widget({
  widget: new LineBreakWidget(),
  block: true
}).range(pos);

Interactive Tooltips

Create custom tooltips:
import { Tooltip, showTooltip } from "@codemirror/view";
import { StateField } from "@codemirror/state";

const hoverTooltip = StateField.define<readonly Tooltip[]>({
  create() {
    return [];
  },
  
  update(tooltips, transaction) {
    // Logic to show/hide tooltip
    return [];
  },
  
  provide: (field) => showTooltip.computeN(
    [field],
    (state) => state.field(field)
  )
});

function createTooltip(pos: number, content: string): Tooltip {
  return {
    pos,
    above: true,
    strictSide: true,
    create() {
      const dom = document.createElement("div");
      dom.className = "cm-tooltip-custom";
      dom.textContent = content;
      return { dom };
    }
  };
}

Custom Keybindings

Add keyboard shortcuts:
import { keymap } from "@codemirror/view";

const customKeybindings = keymap.of([
  {
    key: "Ctrl-Shift-d",
    run(view) {
      // Duplicate current line
      const { state } = view;
      const line = state.doc.lineAt(state.selection.main.head);
      
      view.dispatch({
        changes: {
          from: line.to,
          insert: "\n" + line.text
        }
      });
      
      return true;
    }
  },
  {
    key: "Ctrl-/",
    run(view) {
      // Toggle comment
      // ... implementation
      return true;
    }
  }
]);

export const customKeymap = () => [customKeybindings];

Language Support

Add syntax highlighting for new languages:
import { StreamLanguage } from "@codemirror/language";
import { rust } from "@codemirror/legacy-modes/mode/rust";

const rustSupport = StreamLanguage.define(rust);

// Or use modern parser
import { parser } from "@lezer/rust";
import { LRLanguage } from "@codemirror/language";

const rustLanguage = LRLanguage.define({
  parser: parser,
  languageData: {
    commentTokens: { line: "//", block: { open: "/*", close: "*/" } }
  }
});
Update language-extension.ts:
export function languageExtension(fileName: string) {
  const ext = fileName.split(".").pop()?.toLowerCase();
  
  switch (ext) {
    case "rs":
      return rustLanguage;
    // ... other cases
  }
}

Performance Optimization

Debouncing Updates

let updateTimer: number | null = null;

const debouncedPlugin = ViewPlugin.fromClass(
  class {
    update(update: ViewUpdate) {
      if (updateTimer) clearTimeout(updateTimer);
      
      updateTimer = window.setTimeout(() => {
        // Expensive operation here
        this.processUpdate(update);
      }, 300);
    }
    
    processUpdate(update: ViewUpdate) {
      // Heavy computation
    }
  }
);

Viewport-Based Rendering

Only process visible content:
buildDecorations(view: EditorView): DecorationSet {
  const decorations: any[] = [];
  const { from, to } = view.viewport;
  
  // Only process visible range
  for (let pos = from; pos <= to;) {
    const line = view.state.doc.lineAt(pos);
    // Process line
    pos = line.to + 1;
  }
  
  return Decoration.set(decorations);
}

Testing Extensions

import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { describe, it, expect } from "vitest";
import { todoHighlight } from "./todo-highlighter";

describe("TODO Highlighter", () => {
  it("highlights TODO comments", () => {
    const doc = "// TODO: Fix this\nfunction test() {}";
    
    const state = EditorState.create({
      doc,
      extensions: [todoHighlight()]
    });
    
    const view = new EditorView({
      state,
      parent: document.body
    });
    
    // Check decorations
    const decorations = view.plugin(todoHighlighter)?.decorations;
    expect(decorations?.size).toBeGreaterThan(0);
    
    view.destroy();
  });
});

Extension Examples

Color-code matching brackets:
import { syntaxTree } from "@codemirror/language";

const bracketColors = ["#ffd700", "#da70d6", "#87ceeb"];

const bracketColorizer = ViewPlugin.fromClass(
  class {
    decorations: DecorationSet;
    
    constructor(view: EditorView) {
      this.decorations = this.buildDecorations(view);
    }
    
    buildDecorations(view: EditorView) {
      const decorations: any[] = [];
      let depth = 0;
      
      syntaxTree(view.state).iterate({
        enter(node) {
          if (node.name === "{") {
            const color = bracketColors[depth % bracketColors.length];
            decorations.push(
              Decoration.mark({ class: "bracket", attributes: { style: `color: ${color}` } })
                .range(node.from, node.to)
            );
            depth++;
          }
        },
        leave(node) {
          if (node.name === "}") {
            depth--;
          }
        }
      });
      
      return Decoration.set(decorations);
    }
  },
  { decorations: (p) => p.decorations }
);
Show git blame information:
import { execSync } from "child_process";

const gitBlameWidget = ViewPlugin.fromClass(
  class {
    decorations: DecorationSet;
    
    async getBlame(filePath: string) {
      try {
        const blame = execSync(`git blame ${filePath} --line-porcelain`)
          .toString();
        return parseBlame(blame);
      } catch {
        return null;
      }
    }
    
    buildDecorations(view: EditorView) {
      // Show author and date for each line
    }
  }
);
Preview markdown/HTML in real-time:
const livePreview = ViewPlugin.fromClass(
  class {
    update(update: ViewUpdate) {
      if (update.docChanged) {
        const content = update.state.doc.toString();
        const html = renderMarkdown(content);
        updatePreviewPanel(html);
      }
    }
  }
);

Publishing Extensions

1

Create npm package

package.json
{
  "name": "codemirror-todo-highlight",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "peerDependencies": {
    "@codemirror/state": "^6.0.0",
    "@codemirror/view": "^6.0.0"
  }
}
2

Build and publish

npm run build
npm publish
3

Install in Polaris

npm install codemirror-todo-highlight
import { todoHighlight } from "codemirror-todo-highlight";

const extensions = [
  todoHighlight()
];

Resources

CodeMirror Docs

Official documentation and examples

Extension Examples

Learn from official examples

Polaris Source

Study existing extensions

Community Extensions

Discover community packages

Next Steps

Architecture

Understand the editor architecture

Contributing

Contribute your extensions

Background Jobs

Integrate with Trigger.dev

Setup

Set up development environment

Build docs developers (and LLMs) love