Skip to main content
Polaris uses CodeMirror 6 as its core editor, enhanced with custom extensions for AI features, language support, and UI interactions. This guide explains how extensions work and how to create your own.

Extension Architecture

CodeMirror 6 extensions are composable pieces that add functionality to the editor. Polaris combines built-in extensions with custom ones:
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";

const state = EditorState.create({
  doc: content,
  extensions: [
    customSetup,              // Built-in features
    getLanguageExtension(filename),  // Syntax highlighting
    customTheme,             // Styling
    minimap(),              // Overview sidebar
    suggestion(filename),   // AI suggestions
    quickEdit(filename),    // Cmd+K editing
    selectionTooltip(),     // Selection actions
  ],
});

Core Extensions

Custom Setup (custom-setup.ts)

Bundles essential CodeMirror features:
import {
  keymap,
  highlightSpecialChars,
  drawSelection,
  highlightActiveLine,
  dropCursor,
  rectangularSelection,
  crosshairCursor,
  lineNumbers,
  highlightActiveLineGutter,
} from "@codemirror/view";
import { Extension, EditorState } from "@codemirror/state";
import {
  defaultHighlightStyle,
  syntaxHighlighting,
  indentOnInput,
  bracketMatching,
  foldGutter,
  foldKeymap,
} from "@codemirror/language";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
import {
  autocompletion,
  completionKeymap,
  closeBrackets,
  closeBracketsKeymap,
} from "@codemirror/autocomplete";

export const customSetup: Extension = (() => [
  lineNumbers(),
  highlightActiveLineGutter(),
  highlightSpecialChars(),
  history(),
  foldGutter({
    markerDOM(open) {
      const icon = document.createElement("div");
      icon.className =
        "flex items-center justify-center size-4 cursor-pointer pt-0.5";
      icon.innerHTML = open ? foldGutterOpenSvg : foldGutterClosedSvg;
      return icon;
    },
  }),
  drawSelection(),
  dropCursor(),
  EditorState.allowMultipleSelections.of(true),
  indentOnInput(),
  syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
  bracketMatching(),
  closeBrackets(),
  autocompletion(),
  rectangularSelection(),
  crosshairCursor(),
  highlightActiveLine(),
  highlightSelectionMatches(),
  keymap.of([
    ...closeBracketsKeymap,
    ...defaultKeymap,
    ...searchKeymap,
    ...historyKeymap,
    ...foldKeymap,
    ...completionKeymap,
    ...lintKeymap,
  ]),
])();
The custom fold gutter uses SVG icons from Lucide for a consistent UI look.

Language Extension (language-extension.ts)

Provides syntax highlighting based on file extension:
import { Extension } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { json } from "@codemirror/lang-json";
import { markdown } from "@codemirror/lang-markdown";
import { python } from "@codemirror/lang-python";

export const getLanguageExtension = (filename: string): Extension => {
  const ext = filename.split(".").pop()?.toLowerCase();

  switch(ext) {
    case "js":
      return javascript();
    case "jsx":
      return javascript({ jsx: true });
    case "ts":
      return javascript({ typescript: true });
    case "tsx":
      return javascript({ typescript: true, jsx: true });
    case "html":
      return html();
    case "css":
      return css();
    case "json":
      return json();
    case "md":
    case "mdx":
      return markdown();
    case "py":
      return python();
    default:
      return [];
  }
};

Custom Theme (theme.ts)

Styles the editor to match Polaris design:
import { EditorView } from "@codemirror/view";

export const customTheme = EditorView.theme({
  "&": {
    outline: "none !important",
    height: "100%",
  },
  ".cm-content": {
    fontFamily: "var(--font-plex-mono), monospace",
    fontSize: "14px",
  },
  ".cm-scroller": {
    scrollbarWidth: "thin",
    scrollbarColor: "#3f3f46 transparent",
  },
});

AI-Powered Extensions

Suggestion Extension (suggestion/index.ts)

Provides ghost text suggestions as you type:
import { StateEffect, StateField } from "@codemirror/state";

// Define an effect to update suggestion text
const setSuggestionEffect = StateEffect.define<string | null>();

// Store the current suggestion in editor state
const suggestionState = StateField.define<string | null>({
  create() {
    return null;
  },
  update(value, transaction) {
    for (const effect of transaction.effects) {
      if (effect.is(setSuggestionEffect)) {
        return effect.value;
      }
    }
    return value;
  },
});
The suggestion extension sends context to /api/suggestion including the current line, 5 lines before/after, and cursor position.

Quick Edit Extension (quick-edit/index.ts)

Enables Cmd+K to edit selected code with natural language:
import { StateEffect, StateField } from "@codemirror/state";

export const showQuickEditEffect = StateEffect.define<boolean>();

export const quickEditState = StateField.define<boolean>({
  create() {
    return false;
  },
  update(value, transaction) {
    for (const effect of transaction.effects) {
      if (effect.is(showQuickEditEffect)) {
        return effect.value;
      }
    }
    // Close if selection becomes empty
    if (transaction.selection) {
      const selection = transaction.state.selection.main;
      if (selection.empty) return false;
    }
    return value;
  }
});

Selection Tooltip (selection-tooltip.ts)

Shows action buttons when text is selected:
import { Tooltip, showTooltip } from "@codemirror/view";
import { StateField } from "@codemirror/state";

const createTooltipForSelection = (state: EditorState): readonly Tooltip[] => {
  const selection = state.selection.main;
  if (selection.empty) return [];

  // Don't show if quick edit is active
  const isQuickEditActive = state.field(quickEditState);
  if (isQuickEditActive) return [];

  return [{
    pos: selection.to,
    above: false,
    create() {
      const dom = document.createElement("div");
      dom.className = "bg-popover border shadow-md flex gap-2";

      const addToChatButton = document.createElement("button");
      addToChatButton.textContent = "Add to Chat";

      const quickEditButton = document.createElement("button");
      quickEditButton.textContent = "Quick Edit";
      quickEditButton.onclick = () => {
        editorView.dispatch({
          effects: showQuickEditEffect.of(true),
        });
      };

      dom.appendChild(addToChatButton);
      dom.appendChild(quickEditButton);
      return { dom };
    },
  }];
};

const selectionTooltipField = StateField.define<readonly Tooltip[]>({
  create(state) {
    return createTooltipForSelection(state);
  },
  update(tooltips, transaction) {
    if (transaction.docChanged || transaction.selection) {
      return createTooltipForSelection(transaction.state);
    }
    return tooltips;
  },
  provide: (field) => showTooltip.computeN(
    [field],
    (state) => state.field(field)
  ),
});

Creating Custom Extensions

Here’s a template for creating your own extension:
import { Extension } from "@codemirror/state";
import { ViewPlugin, EditorView, Decoration } from "@codemirror/view";

export const myExtension = (): Extension => {
  // 1. Create a ViewPlugin for editor interactions
  const plugin = ViewPlugin.fromClass(
    class {
      decorations: DecorationSet;

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

      update(update: ViewUpdate) {
        // React to document changes
        if (update.docChanged) {
          // Update decorations or trigger actions
        }
      }

      destroy() {
        // Cleanup
      }
    },
    {
      decorations: (v) => v.decorations,
    }
  );

  // 2. Define keybindings
  const keymaps = keymap.of([{
    key: "Ctrl-Space",
    run: (view) => {
      // Handle key press
      return true; // Prevent default
    },
  }]);

  // 3. Return array of extensions
  return [plugin, keymaps];
};

Extension Concepts

StateFields store data in the editor state and update via transactions:
const myState = StateField.define<MyData>({
  create() {
    return initialValue;
  },
  update(value, transaction) {
    // Return new value or keep current
    return value;
  },
});
Effects are messages that modify state:
const myEffect = StateEffect.define<string>();

// Dispatch an effect
view.dispatch({
  effects: myEffect.of("new value"),
});

// Handle in StateField.update
for (const effect of transaction.effects) {
  if (effect.is(myEffect)) {
    return effect.value;
  }
}
Decorations add visual elements without changing document content:
import { Decoration, DecorationSet } from "@codemirror/view";

// Add a widget at cursor
const decoration = Decoration.widget({
  widget: new MyWidget(),
  side: 1,
}).range(cursorPos);

return Decoration.set([decoration]);
ViewPlugins react to editor changes and manage side effects:
ViewPlugin.fromClass(class {
  constructor(view: EditorView) {
    // Initialize
  }

  update(update: ViewUpdate) {
    // React to changes
  }

  destroy() {
    // Cleanup timers, listeners, etc.
  }
});
All extensions in src/features/editor/extensions/ are imported and applied in the main editor component at src/features/editor/components/code-editor.tsx.

Testing Extensions

Test extensions by importing them in the editor component:
import { myExtension } from "../extensions/my-extension";

const state = EditorState.create({
  doc: content,
  extensions: [
    // ... other extensions
    myExtension(),
  ],
});
Use the browser DevTools to inspect StateFields: view.state.field(myState)

Build docs developers (and LLMs) love