Files
superset2/docs/developer_docs/extensions/extension-points/editors.md
Evan Rusackas 10f71bdcd5 fix(docs): finish bare-relative link conversion + add lint guardrail
Copilot flagged two stragglers on editors.md where the previous
file-by-file conversion stopped halfway. Sweeping for the same
pattern across the active content tree found 76 bare relative
internal links total — 14 in this PR's already-modified files
(Copilot's two plus twelve more) and 62 in unchanged files.

Why the build doesn't catch this
─────────────────────────────────
`onBrokenLinks: 'throw'` (set in this PR) only validates *file-based*
markdown references — links whose URL ends in `.md` / `.mdx`. Those
go through Docusaurus's file resolver, which can prove the target
exists. Bare relative URL paths like `[Foo](../foo)` skip that
resolver entirely; Docusaurus emits them as raw hrefs. The browser
then resolves them against the *current* page URL, and for
trailing-slash routes that almost always lands in the wrong
directory. Page navigates client-side and 404s. The linkinator job
in CI *can* catch these, but it's `continue-on-error: true` so
findings are advisory.

What this commit does
──────────────────────
1. Fix all 76 bare relative internal links across the active docs
   tree by appending `.md` to each one (preserving anchors / query
   strings). All 76 targets resolved to real files; no link
   targets changed, only the form of the reference.

2. Fix the component-page generator. 54 of the 76 bare links lived
   in two auto-generated index files (`components/ui/index.mdx`
   and `components/design-system/index.mdx`). The next regeneration
   would have undone the manual fixes without this. The two
   emission sites in `generate-superset-components.mjs` now emit
   `.md`-suffixed links; comment at the call site explains why.

3. Add `docs/scripts/lint-docs-links.mjs` — fast source-level
   linter that scans `.md`/`.mdx` files under the active content
   trees (skipping `versioned_docs/` snapshots) and fails if it
   finds any markdown link whose URL starts with `./` or `../` and
   does not end in `.md`/`.mdx`. Excludes asset paths (.png,
   .json, etc.) and ignores fenced code blocks. Wired up as
   `yarn lint:docs-links`.

4. Add a `Lint docs links` step to `superset-docs-verify.yml`,
   running before the build step so PRs that introduce the pattern
   fail in seconds rather than at build-time / not at all. Blocking,
   not advisory — exactly the gap linkinator's `continue-on-error`
   leaves open.

Verified
────────
- `yarn lint:docs-links` exits 0 on the cleaned tree
- Re-introducing one bare link makes the linter report the exact
  file:line with the offending URL, exit code 1
- All 76 originally-flagged targets resolved to real `.md` / `.mdx`
  files; only the form of the reference changed
2026-05-13 20:17:46 -07:00

6.9 KiB

title, sidebar_position
title sidebar_position
Editors 2

Editor Contributions

Extensions can replace Superset's default text editors with custom implementations. This allows you to provide enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions.

Overview

Superset uses text editors in various places throughout the application:

Language Locations
sql SQL Lab, Metric/Filter Popovers
json Dashboard Properties, Annotation Modal, Theme Modal
css Dashboard Properties, CSS Template Modal
markdown Dashboard Markdown component
yaml Template Params Editor
javascript Custom JavaScript editor contexts
python Custom Python editor contexts
text Plain text editor contexts

By registering an editor for a language, your extension replaces the default Ace editor in all locations that use that language.

Implementing an Editor

Your editor component must implement the EditorProps interface and expose an EditorHandle via forwardRef. For the complete interface definitions, see @apache-superset/core/api/editors.ts.

Key EditorProps

interface EditorProps {
  /** Controlled value */
  value: string;
  /** Content change handler */
  onChange: (value: string) => void;
  /** Language mode for syntax highlighting */
  language: EditorLanguage;
  /** Keyboard shortcuts to register */
  hotkeys?: EditorHotkey[];
  /** Callback when editor is ready with imperative handle */
  onReady?: (handle: EditorHandle) => void;
  /** Host-specific context (e.g., database info from SQL Lab) */
  metadata?: Record<string, unknown>;
  // ... additional props for styling, annotations, etc.
}

Key EditorHandle Methods

interface EditorHandle {
  /** Focus the editor */
  focus(): void;
  /** Get the current editor content */
  getValue(): string;
  /** Get the current cursor position */
  getCursorPosition(): Position;
  /** Move the cursor to a specific position */
  moveCursorToPosition(position: Position): void;
  /** Set the selection range */
  setSelection(selection: Range): void;
  /** Scroll to a specific line */
  scrollToLine(line: number): void;
  // ... additional methods for text manipulation, annotations, etc.
}

Example Implementation

Here's an example of a Monaco-based SQL editor implementing the key interfaces shown above:

MonacoSQLEditor.tsx

import { forwardRef, useRef, useImperativeHandle, useEffect } from 'react';
import * as monaco from 'monaco-editor';
import type { editors } from '@apache-superset/core';

const MonacoSQLEditor = forwardRef<editors.EditorHandle, editors.EditorProps>(
  (props, ref) => {
    const { value, onChange, hotkeys, onReady } = props;
    const containerRef = useRef<HTMLDivElement>(null);
    const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);

    // Implement EditorHandle interface
    const handle: editors.EditorHandle = {
      focus: () => editorRef.current?.focus(),
      getValue: () => editorRef.current?.getValue() ?? '',
      getCursorPosition: () => {
        const pos = editorRef.current?.getPosition();
        return { line: (pos?.lineNumber ?? 1) - 1, column: (pos?.column ?? 1) - 1 };
      },
      // ... implement remaining methods
    };

    useImperativeHandle(ref, () => handle, []);

    useEffect(() => {
      if (!containerRef.current) return;

      const editor = monaco.editor.create(containerRef.current, { value, language: 'sql' });
      editorRef.current = editor;

      editor.onDidChangeModelContent(() => onChange(editor.getValue()));

      // Register hotkeys
      hotkeys?.forEach(hotkey => {
        editor.addAction({
          id: hotkey.name,
          label: hotkey.name,
          run: () => hotkey.exec(handle),
        });
      });

      onReady?.(handle);
      return () => editor.dispose();
    }, []);

    return <div ref={containerRef} style={{ height: '100%', width: '100%' }} />;
  },
);

export default MonacoSQLEditor;

index.tsx

Register the editor at module load time from your extension's entry point:

import { editors } from '@apache-superset/core';
import MonacoSQLEditor from './MonacoSQLEditor';

editors.registerEditor(
  {
    id: 'my-extension.monaco-sql',
    name: 'Monaco SQL Editor',
    languages: ['sql'],
  },
  MonacoSQLEditor,
);

Handling Hotkeys

Superset passes keyboard shortcuts via the hotkeys prop. Each hotkey includes an exec function that receives the EditorHandle:

interface EditorHotkey {
  name: string;
  key: string;  // e.g., "Ctrl-Enter", "Alt-Shift-F"
  description?: string;
  exec: (handle: EditorHandle) => void;
}

Your editor must register these hotkeys with your editor framework and call exec(handle) when triggered.

Keywords

Superset passes static autocomplete suggestions via the keywords prop. These include table names, column names, and SQL functions based on the current database context:

interface EditorKeyword {
  name: string;
  value?: string;  // Text to insert (defaults to name)
  meta?: string;   // Category like "table", "column", "function"
  score?: number;  // Sorting priority
}

Your editor should convert these to your framework's completion format and register them for autocomplete.

Completion Providers

For dynamic autocomplete (e.g., fetching suggestions as the user types), implement and register a CompletionProvider via the EditorHandle:

const provider: CompletionProvider = {
  id: 'my-sql-completions',
  triggerCharacters: ['.', ' '],
  provideCompletions: async (content, position, context) => {
    // Use context.metadata for database info
    // Return array of CompletionItem
    return [
      { label: 'SELECT', insertText: 'SELECT', kind: 'keyword' },
      // ...
    ];
  },
};

// Register during editor initialization
const disposable = handle.registerCompletionProvider(provider);

Next Steps