From c26cd450d0bd7304d6d88e663e60dce2e1b42967 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Wed, 4 Mar 2026 09:12:26 -0800 Subject: [PATCH 1/6] feat(editor): add IPython syntax highlighting mode Add a new IPython language mode to the CodeMirror editor that highlights IPython-specific syntax including shell commands (!), line magics (%), cell magics (%%), and help operators (? and ??). Implements a ViewPlugin-based decoration system that preserves all CodeMirror 6 features. Also configure 4-space indentation for Python and IPython to match PEP 8 and ruff formatting. --- app/components/CodeMirrorEditorDemo.tsx | 14 +++ registry/editor/index.ts | 5 + registry/editor/ipython.ts | 140 ++++++++++++++++++++++++ registry/editor/languages.ts | 13 ++- 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 registry/editor/ipython.ts diff --git a/app/components/CodeMirrorEditorDemo.tsx b/app/components/CodeMirrorEditorDemo.tsx index 96c558f..c970749 100644 --- a/app/components/CodeMirrorEditorDemo.tsx +++ b/app/components/CodeMirrorEditorDemo.tsx @@ -19,6 +19,19 @@ def greet(name: str) -> str: return f"Hello, {name}!" print(greet("nteract"))`, + ipython: `# IPython example with magics and shell commands +%matplotlib inline +%time x = sum(range(1000000)) + +!pip install pandas + +import pandas as pd +df = pd.DataFrame({"a": [1, 2, 3]}) +df.head? + +%%bash +echo "Hello from bash" +ls -la`, markdown: `# Markdown Example This is a **bold** statement and this is *italic*. @@ -123,6 +136,7 @@ export function CodeMirrorEditorDemo({ className="text-sm bg-background border border-border rounded px-2 py-1" > + diff --git a/registry/editor/index.ts b/registry/editor/index.ts index cb685d1..094bc5e 100644 --- a/registry/editor/index.ts +++ b/registry/editor/index.ts @@ -10,6 +10,11 @@ export { minimalSetup, notebookEditorTheme, } from "./extensions"; +export { + ipythonHighlighting, + ipythonStyles, + ipythonStylesDark, +} from "./ipython"; export { detectLanguage, fileExtensionToLanguage, diff --git a/registry/editor/ipython.ts b/registry/editor/ipython.ts new file mode 100644 index 0000000..6ece7cf --- /dev/null +++ b/registry/editor/ipython.ts @@ -0,0 +1,140 @@ +import { RangeSetBuilder } from "@codemirror/state"; +import type { DecorationSet, ViewUpdate } from "@codemirror/view"; +import { Decoration, EditorView, ViewPlugin } from "@codemirror/view"; + +/** + * IPython syntax highlighting extension for CodeMirror 6 + * + * Highlights IPython-specific syntax on top of standard Python: + * - Shell commands: !ls, !pip install + * - Line magics: %time, %run script.py + * - Cell magics: %%bash, %%javascript + * - Help operators: object?, object?? + */ + +// Decoration marks for IPython syntax +const shellMark = Decoration.mark({ class: "cm-ipython-shell" }); +const magicMark = Decoration.mark({ class: "cm-ipython-magic" }); +const cellMagicMark = Decoration.mark({ class: "cm-ipython-cell-magic" }); +const helpMark = Decoration.mark({ class: "cm-ipython-help" }); + +// Patterns for IPython syntax (applied to line content) +const CELL_MAGIC_PATTERN = /^(%%[a-zA-Z_]\w*)/; +const LINE_MAGIC_PATTERN = /^(%[a-zA-Z_]\w*)/; +const SHELL_PATTERN = /^(!)/; +const HELP_PATTERN = /(\?\??)$/; + +class IPythonHighlighter { + 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 builder = new RangeSetBuilder(); + const doc = view.state.doc; + + for (const { from, to } of view.visibleRanges) { + // Get line numbers for the visible range + const startLine = doc.lineAt(from).number; + const endLine = doc.lineAt(to).number; + + for (let lineNum = startLine; lineNum <= endLine; lineNum++) { + const line = doc.line(lineNum); + const lineText = line.text; + const trimmedText = lineText.trimStart(); + const leadingSpaces = lineText.length - trimmedText.length; + const lineStart = line.from + leadingSpaces; + + // Check for cell magic (%%magic) + const cellMagicMatch = trimmedText.match(CELL_MAGIC_PATTERN); + if (cellMagicMatch) { + builder.add(lineStart, lineStart + cellMagicMatch[1].length, cellMagicMark); + continue; // Cell magics take precedence + } + + // Check for line magic (%magic) + const lineMagicMatch = trimmedText.match(LINE_MAGIC_PATTERN); + if (lineMagicMatch) { + builder.add(lineStart, lineStart + lineMagicMatch[1].length, magicMark); + continue; // Line magics take the whole line + } + + // Check for shell command (!command) + const shellMatch = trimmedText.match(SHELL_PATTERN); + if (shellMatch) { + // Highlight the entire shell command line + builder.add(lineStart, line.to, shellMark); + continue; + } + + // Check for help operator (object? or object??) + const helpMatch = lineText.match(HELP_PATTERN); + if (helpMatch && helpMatch.index !== undefined) { + const helpStart = line.from + helpMatch.index; + const helpEnd = helpStart + helpMatch[1].length; + builder.add(helpStart, helpEnd, helpMark); + } + } + } + + return builder.finish(); + } +} + +/** + * CodeMirror extension that adds IPython syntax highlighting + */ +export function ipythonHighlighting() { + return ViewPlugin.fromClass(IPythonHighlighter, { + decorations: (v) => v.decorations, + }); +} + +/** + * Light theme styles for IPython syntax + */ +export const ipythonStyles = EditorView.theme({ + ".cm-ipython-shell": { + color: "#0550ae", + }, + ".cm-ipython-magic": { + color: "#6639ba", + }, + ".cm-ipython-cell-magic": { + color: "#6639ba", + fontWeight: "bold", + }, + ".cm-ipython-help": { + color: "#0969da", + }, +}); + +/** + * Dark theme styles for IPython syntax + */ +export const ipythonStylesDark = EditorView.theme( + { + ".cm-ipython-shell": { + color: "#79c0ff", + }, + ".cm-ipython-magic": { + color: "#d2a8ff", + }, + ".cm-ipython-cell-magic": { + color: "#d2a8ff", + fontWeight: "bold", + }, + ".cm-ipython-help": { + color: "#58a6ff", + }, + }, + { dark: true }, +); diff --git a/registry/editor/languages.ts b/registry/editor/languages.ts index cb27383..492ecee 100644 --- a/registry/editor/languages.ts +++ b/registry/editor/languages.ts @@ -4,13 +4,17 @@ import { json } from "@codemirror/lang-json"; import { markdown } from "@codemirror/lang-markdown"; import { python } from "@codemirror/lang-python"; import { sql } from "@codemirror/lang-sql"; +import { indentUnit } from "@codemirror/language"; import type { Extension } from "@codemirror/state"; +import { ipythonHighlighting, ipythonStyles, ipythonStylesDark } from "./ipython"; + /** * Supported languages for the CodeMirror editor */ export type SupportedLanguage = | "python" + | "ipython" | "markdown" | "sql" | "html" @@ -22,10 +26,15 @@ export type SupportedLanguage = /** * Get the CodeMirror language extension for a given language */ +// PEP 8 specifies 4-space indentation for Python +const pythonIndent = indentUnit.of(" "); + export function getLanguageExtension(language: SupportedLanguage): Extension { switch (language) { case "python": - return python(); + return [python(), pythonIndent]; + case "ipython": + return [python(), pythonIndent, ipythonHighlighting(), ipythonStyles, ipythonStylesDark]; case "markdown": return markdown(); case "sql": @@ -48,6 +57,7 @@ export function getLanguageExtension(language: SupportedLanguage): Extension { */ export const languageDisplayNames: Record = { python: "Python", + ipython: "IPython", markdown: "Markdown", sql: "SQL", html: "HTML", @@ -62,6 +72,7 @@ export const languageDisplayNames: Record = { */ export const fileExtensionToLanguage: Record = { ".py": "python", + ".ipy": "ipython", ".md": "markdown", ".markdown": "markdown", ".sql": "sql", From 803db6d1795a94068b0ef40ebab2632045786517 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Wed, 4 Mar 2026 09:13:31 -0800 Subject: [PATCH 2/6] style: fix biome formatting --- registry/editor/ipython.ts | 12 ++++++++++-- registry/editor/languages.ts | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/registry/editor/ipython.ts b/registry/editor/ipython.ts index 6ece7cf..4f6d24e 100644 --- a/registry/editor/ipython.ts +++ b/registry/editor/ipython.ts @@ -56,14 +56,22 @@ class IPythonHighlighter { // Check for cell magic (%%magic) const cellMagicMatch = trimmedText.match(CELL_MAGIC_PATTERN); if (cellMagicMatch) { - builder.add(lineStart, lineStart + cellMagicMatch[1].length, cellMagicMark); + builder.add( + lineStart, + lineStart + cellMagicMatch[1].length, + cellMagicMark, + ); continue; // Cell magics take precedence } // Check for line magic (%magic) const lineMagicMatch = trimmedText.match(LINE_MAGIC_PATTERN); if (lineMagicMatch) { - builder.add(lineStart, lineStart + lineMagicMatch[1].length, magicMark); + builder.add( + lineStart, + lineStart + lineMagicMatch[1].length, + magicMark, + ); continue; // Line magics take the whole line } diff --git a/registry/editor/languages.ts b/registry/editor/languages.ts index 492ecee..b1806e1 100644 --- a/registry/editor/languages.ts +++ b/registry/editor/languages.ts @@ -7,7 +7,11 @@ import { sql } from "@codemirror/lang-sql"; import { indentUnit } from "@codemirror/language"; import type { Extension } from "@codemirror/state"; -import { ipythonHighlighting, ipythonStyles, ipythonStylesDark } from "./ipython"; +import { + ipythonHighlighting, + ipythonStyles, + ipythonStylesDark, +} from "./ipython"; /** * Supported languages for the CodeMirror editor @@ -34,7 +38,13 @@ export function getLanguageExtension(language: SupportedLanguage): Extension { case "python": return [python(), pythonIndent]; case "ipython": - return [python(), pythonIndent, ipythonHighlighting(), ipythonStyles, ipythonStylesDark]; + return [ + python(), + pythonIndent, + ipythonHighlighting(), + ipythonStyles, + ipythonStylesDark, + ]; case "markdown": return markdown(); case "sql": From e1f7b16f57f8b259fbb2529fe6528fee5b0cfb4f Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Wed, 4 Mar 2026 09:40:54 -0800 Subject: [PATCH 3/6] fix: add ipython.ts to codemirror-editor registry --- registry.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/registry.json b/registry.json index 0adac32..fdf8579 100644 --- a/registry.json +++ b/registry.json @@ -401,7 +401,7 @@ "name": "codemirror-editor", "type": "registry:component", "title": "CodeMirror Editor", - "description": "A CodeMirror 6 editor component for notebook cells with syntax highlighting, key bindings, and language support for Python, Markdown, SQL, HTML, JavaScript, TypeScript, and JSON.", + "description": "A CodeMirror 6 editor component for notebook cells with syntax highlighting, key bindings, and language support for Python, IPython (with magics and shell commands), Markdown, SQL, HTML, JavaScript, TypeScript, and JSON.", "dependencies": [ "@codemirror/view", "@codemirror/state", @@ -439,6 +439,11 @@ "type": "registry:component", "target": "components/editor/themes.ts" }, + { + "path": "registry/editor/ipython.ts", + "type": "registry:component", + "target": "components/editor/ipython.ts" + }, { "path": "registry/editor/index.ts", "type": "registry:component", From 7a30bbbda28f22aa14abc66b325bd74b4346179b Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Wed, 4 Mar 2026 11:03:14 -0800 Subject: [PATCH 4/6] feat(editor): add cell magic language detection and switching - Add detectCellMagic() to detect %%magic at start of content - Add getCellMagicLanguage() to map magics to language modes - Add getIPythonExtension() for automatic cell magic language switching - Export CELL_MAGIC_LANGUAGES mapping for consumers - Update IPython highlighter to only decorate first line for cell magics - Simplify demo example (cell magics should be in separate cells) Cell magics like %%html, %%javascript, %%sql now properly switch the editor to the appropriate language mode while keeping the %%magic line decorated as IPython. --- app/components/CodeMirrorEditorDemo.tsx | 6 +- registry/editor/index.ts | 4 + registry/editor/ipython.ts | 120 ++++++++++++++++++++---- registry/editor/languages.ts | 61 ++++++++++++ 4 files changed, 169 insertions(+), 22 deletions(-) diff --git a/app/components/CodeMirrorEditorDemo.tsx b/app/components/CodeMirrorEditorDemo.tsx index c970749..d1697fa 100644 --- a/app/components/CodeMirrorEditorDemo.tsx +++ b/app/components/CodeMirrorEditorDemo.tsx @@ -27,11 +27,7 @@ print(greet("nteract"))`, import pandas as pd df = pd.DataFrame({"a": [1, 2, 3]}) -df.head? - -%%bash -echo "Hello from bash" -ls -la`, +df.head?`, markdown: `# Markdown Example This is a **bold** statement and this is *italic*. diff --git a/registry/editor/index.ts b/registry/editor/index.ts index 094bc5e..eb91b87 100644 --- a/registry/editor/index.ts +++ b/registry/editor/index.ts @@ -11,6 +11,9 @@ export { notebookEditorTheme, } from "./extensions"; export { + CELL_MAGIC_LANGUAGES, + detectCellMagic, + getCellMagicLanguage, ipythonHighlighting, ipythonStyles, ipythonStylesDark, @@ -18,6 +21,7 @@ export { export { detectLanguage, fileExtensionToLanguage, + getIPythonExtension, getLanguageExtension, languageDisplayNames, type SupportedLanguage, diff --git a/registry/editor/ipython.ts b/registry/editor/ipython.ts index 4f6d24e..0392a5a 100644 --- a/registry/editor/ipython.ts +++ b/registry/editor/ipython.ts @@ -8,8 +8,13 @@ import { Decoration, EditorView, ViewPlugin } from "@codemirror/view"; * Highlights IPython-specific syntax on top of standard Python: * - Shell commands: !ls, !pip install * - Line magics: %time, %run script.py - * - Cell magics: %%bash, %%javascript + * - Cell magics: %%bash, %%javascript (first line only) * - Help operators: object?, object?? + * + * Note: For cell magics (%%), only the first line is decorated. + * The rest of the cell should use the appropriate language mode. + * Use detectCellMagic() and CELL_MAGIC_LANGUAGES to determine + * the correct language for cell magic content. */ // Decoration marks for IPython syntax @@ -24,6 +29,73 @@ const LINE_MAGIC_PATTERN = /^(%[a-zA-Z_]\w*)/; const SHELL_PATTERN = /^(!)/; const HELP_PATTERN = /(\?\??)$/; +/** + * Mapping of cell magic names to language identifiers. + * Use with getLanguageExtension() from languages.ts + */ +export const CELL_MAGIC_LANGUAGES: Record = { + // HTML/SVG + html: "html", + HTML: "html", + svg: "html", + SVG: "html", + // JavaScript + javascript: "javascript", + js: "javascript", + // TypeScript + typescript: "typescript", + ts: "typescript", + // SQL + sql: "sql", + SQL: "sql", + // Markdown + markdown: "markdown", + md: "markdown", + // JSON + json: "json", + // Shell (falls back to plain since we don't have shell lang) + bash: "plain", + sh: "plain", + // Python (stays as Python) + python: "python", + python3: "python", + // Others without specific support fall back to plain +}; + +/** + * Detect cell magic from content. + * Cell magics must be on the first line. + * + * @param content - The editor content + * @returns The magic name (without %%) if found, null otherwise + * + * @example + * detectCellMagic("%%html\n
Hello
") // "html" + * detectCellMagic("%time x = 1") // null (line magic, not cell magic) + * detectCellMagic("print('hello')") // null + */ +export function detectCellMagic(content: string): string | null { + const firstLine = content.split("\n")[0].trim(); + const match = firstLine.match(/^%%([a-zA-Z_]\w*)/); + return match ? match[1] : null; +} + +/** + * Get the language identifier for a cell magic. + * + * @param magic - The cell magic name (without %%) + * @returns The language identifier to use with getLanguageExtension(), + * or "plain" for unsupported magics + * + * @example + * getCellMagicLanguage("html") // "html" + * getCellMagicLanguage("bash") // "plain" (no shell support) + * getCellMagicLanguage("unknown") // "plain" + */ +export function getCellMagicLanguage(magic: string): string { + return CELL_MAGIC_LANGUAGES[magic] ?? "plain"; +} + class IPythonHighlighter { decorations: DecorationSet; @@ -41,8 +113,11 @@ class IPythonHighlighter { const builder = new RangeSetBuilder(); const doc = view.state.doc; + // Check if we're in cell magic mode (first line is %%) + const firstLineText = doc.line(1).text.trim(); + const isInCellMagicMode = CELL_MAGIC_PATTERN.test(firstLineText); + for (const { from, to } of view.visibleRanges) { - // Get line numbers for the visible range const startLine = doc.lineAt(from).number; const endLine = doc.lineAt(to).number; @@ -53,18 +128,26 @@ class IPythonHighlighter { const leadingSpaces = lineText.length - trimmedText.length; const lineStart = line.from + leadingSpaces; - // Check for cell magic (%%magic) - const cellMagicMatch = trimmedText.match(CELL_MAGIC_PATTERN); - if (cellMagicMatch) { - builder.add( - lineStart, - lineStart + cellMagicMatch[1].length, - cellMagicMark, - ); - continue; // Cell magics take precedence + // Cell magic on first line - decorate it + if (lineNum === 1) { + const cellMagicMatch = trimmedText.match(CELL_MAGIC_PATTERN); + if (cellMagicMatch) { + builder.add( + lineStart, + lineStart + cellMagicMatch[1].length, + cellMagicMark, + ); + continue; + } + } + + // If we're in cell magic mode, don't apply IPython decorations + // to subsequent lines (they belong to the cell magic's language) + if (isInCellMagicMode && lineNum > 1) { + continue; } - // Check for line magic (%magic) + // Line magic (%magic) const lineMagicMatch = trimmedText.match(LINE_MAGIC_PATTERN); if (lineMagicMatch) { builder.add( @@ -72,18 +155,17 @@ class IPythonHighlighter { lineStart + lineMagicMatch[1].length, magicMark, ); - continue; // Line magics take the whole line + continue; } - // Check for shell command (!command) + // Shell command (!command) const shellMatch = trimmedText.match(SHELL_PATTERN); if (shellMatch) { - // Highlight the entire shell command line builder.add(lineStart, line.to, shellMark); continue; } - // Check for help operator (object? or object??) + // Help operator (object? or object??) const helpMatch = lineText.match(HELP_PATTERN); if (helpMatch && helpMatch.index !== undefined) { const helpStart = line.from + helpMatch.index; @@ -98,7 +180,11 @@ class IPythonHighlighter { } /** - * CodeMirror extension that adds IPython syntax highlighting + * CodeMirror extension that adds IPython syntax highlighting. + * + * For cells with cell magics (%%html, %%bash, etc.), only the first + * line is decorated. Use detectCellMagic() and getLanguageExtension() + * to set the appropriate language for the cell content. */ export function ipythonHighlighting() { return ViewPlugin.fromClass(IPythonHighlighter, { diff --git a/registry/editor/languages.ts b/registry/editor/languages.ts index b1806e1..4cea142 100644 --- a/registry/editor/languages.ts +++ b/registry/editor/languages.ts @@ -8,6 +8,9 @@ import { indentUnit } from "@codemirror/language"; import type { Extension } from "@codemirror/state"; import { + CELL_MAGIC_LANGUAGES, + detectCellMagic, + getCellMagicLanguage, ipythonHighlighting, ipythonStyles, ipythonStylesDark, @@ -62,6 +65,64 @@ export function getLanguageExtension(language: SupportedLanguage): Extension { } } +/** + * Get the language extension for IPython content, detecting cell magics. + * + * If the content starts with a cell magic (e.g., %%html, %%bash), + * returns the appropriate language extension for that magic. + * Otherwise returns the standard IPython extension. + * + * @param content - The editor content to analyze + * @returns Object with language extension and detected cell magic (if any) + * + * @example + * // Cell magic - returns HTML language + * getIPythonExtension("%%html\n
Hello
") + * // { extension: html(), cellMagic: "html", language: "html" } + * + * // No cell magic - returns IPython + * getIPythonExtension("%time x = sum(range(100))") + * // { extension: [python(), ...], cellMagic: null, language: "ipython" } + */ +export function getIPythonExtension(content: string): { + extension: Extension; + cellMagic: string | null; + language: SupportedLanguage; +} { + const magic = detectCellMagic(content); + + if (magic) { + const langId = getCellMagicLanguage(magic); + const language = ( + langId in languageDisplayNames ? langId : "plain" + ) as SupportedLanguage; + + // For cell magics, use the target language but add IPython decoration + // for the first line (the %%magic declaration) + const baseExtension = getLanguageExtension(language); + return { + extension: [ + baseExtension, + ipythonHighlighting(), + ipythonStyles, + ipythonStylesDark, + ], + cellMagic: magic, + language, + }; + } + + // No cell magic - use standard IPython mode + return { + extension: getLanguageExtension("ipython"), + cellMagic: null, + language: "ipython", + }; +} + +// Re-export cell magic utilities for consumers +export { CELL_MAGIC_LANGUAGES, detectCellMagic, getCellMagicLanguage }; + /** * Language display names for UI */ From 7bd3f8b14f89e8d309ecd7776175e2397dcdd355 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Wed, 4 Mar 2026 11:12:53 -0800 Subject: [PATCH 5/6] fix(editor): prevent auto-indent after IPython magics and shell commands Add ipythonIndent service that returns 0 indent after: - Line magics (%time, %matplotlib, etc.) - Shell commands (!pip, !ls, etc.) - Cell magic declarations (%%bash, %%html, etc.) This prevents Python's parser from auto-indenting after IPython-specific syntax that it doesn't understand. --- registry/editor/index.ts | 1 + registry/editor/ipython.ts | 33 +++++++++++++++++++++++++++++++++ registry/editor/languages.ts | 2 ++ 3 files changed, 36 insertions(+) diff --git a/registry/editor/index.ts b/registry/editor/index.ts index eb91b87..ac0c68e 100644 --- a/registry/editor/index.ts +++ b/registry/editor/index.ts @@ -15,6 +15,7 @@ export { detectCellMagic, getCellMagicLanguage, ipythonHighlighting, + ipythonIndent, ipythonStyles, ipythonStylesDark, } from "./ipython"; diff --git a/registry/editor/ipython.ts b/registry/editor/ipython.ts index 0392a5a..0ff63f8 100644 --- a/registry/editor/ipython.ts +++ b/registry/editor/ipython.ts @@ -1,3 +1,4 @@ +import { indentService } from "@codemirror/language"; import { RangeSetBuilder } from "@codemirror/state"; import type { DecorationSet, ViewUpdate } from "@codemirror/view"; import { Decoration, EditorView, ViewPlugin } from "@codemirror/view"; @@ -29,6 +30,38 @@ const LINE_MAGIC_PATTERN = /^(%[a-zA-Z_]\w*)/; const SHELL_PATTERN = /^(!)/; const HELP_PATTERN = /(\?\??)$/; +/** + * Custom indentation service for IPython. + * + * Prevents auto-indent after: + * - Line magics (%time, %matplotlib, etc.) + * - Shell commands (!pip, !ls, etc.) + * - Cell magic declarations (%%bash, %%html, etc.) + * + * These lines don't follow Python's indentation rules, + * so we reset to column 0 after them. + */ +export const ipythonIndent = indentService.of((context, pos) => { + // Get the previous line + const line = context.state.doc.lineAt(pos); + if (line.number <= 1) return null; // Let Python handle first line + + const prevLine = context.state.doc.line(line.number - 1); + const prevText = prevLine.text.trim(); + + // After line magic, shell command, or cell magic - don't auto-indent + if ( + LINE_MAGIC_PATTERN.test(prevText) || + SHELL_PATTERN.test(prevText) || + CELL_MAGIC_PATTERN.test(prevText) + ) { + return 0; + } + + // Let Python's indentation handle everything else + return null; +}); + /** * Mapping of cell magic names to language identifiers. * Use with getLanguageExtension() from languages.ts diff --git a/registry/editor/languages.ts b/registry/editor/languages.ts index 4cea142..e6adf33 100644 --- a/registry/editor/languages.ts +++ b/registry/editor/languages.ts @@ -12,6 +12,7 @@ import { detectCellMagic, getCellMagicLanguage, ipythonHighlighting, + ipythonIndent, ipythonStyles, ipythonStylesDark, } from "./ipython"; @@ -44,6 +45,7 @@ export function getLanguageExtension(language: SupportedLanguage): Extension { return [ python(), pythonIndent, + ipythonIndent, ipythonHighlighting(), ipythonStyles, ipythonStylesDark, From 768b2c8d5d182b5081093ccb532db1bdfeea58b9 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Wed, 4 Mar 2026 11:20:38 -0800 Subject: [PATCH 6/6] fix(editor): auto-detect cell magics when IPython language is selected When IPython is selected, the editor now uses getIPythonExtension(value) to analyze content and automatically switch to the appropriate language mode for cell magics (%%html, %%markdown, etc.). --- registry/editor/codemirror-editor.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/registry/editor/codemirror-editor.tsx b/registry/editor/codemirror-editor.tsx index 92c85d8..43c0e42 100644 --- a/registry/editor/codemirror-editor.tsx +++ b/registry/editor/codemirror-editor.tsx @@ -19,7 +19,11 @@ import { import { cn } from "@/lib/utils"; import { defaultExtensions } from "./extensions"; -import { getLanguageExtension, type SupportedLanguage } from "./languages"; +import { + getIPythonExtension, + getLanguageExtension, + type SupportedLanguage, +} from "./languages"; import { darkTheme, isDarkMode, lightTheme, type ThemeMode } from "./themes"; export interface CodeMirrorEditorRef { @@ -140,10 +144,13 @@ export const CodeMirrorEditor = forwardRef< }; }, [theme]); - const langExtension = useMemo( - () => getLanguageExtension(language), - [language], - ); + // For IPython, detect cell magics and switch to appropriate language mode + const langExtension = useMemo(() => { + if (language === "ipython") { + return getIPythonExtension(value).extension; + } + return getLanguageExtension(language); + }, [language, value]); // Determine which theme to use const themeExtension = useMemo(() => {