You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2026/01/29 11:22:40 UTC

(superset) branch master updated: feat(extensions): Allow replacing editors using extensions (#37499)

This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 6cb3cea960d feat(extensions): Allow replacing editors using extensions (#37499)
6cb3cea960d is described below

commit 6cb3cea960de67ff35b94880a4a0d7c5657426ec
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Thu Jan 29 08:22:32 2026 -0300

    feat(extensions): Allow replacing editors using extensions (#37499)
---
 .../extensions/contribution-types.md               |  31 +-
 .../extensions/extension-points/editors.md         | 245 +++++++++++++
 docs/developer_portal/extensions/registry.md       |   1 +
 docs/static/img/extensions/editors-bundle.png      | Bin 0 -> 397539 bytes
 scripts/check-type.js                              |   5 +
 .../cypress-base/cypress/e2e/sqllab/tabs.test.ts   |   4 +-
 superset-frontend/package.json                     |   2 +-
 .../superset-core/src/api/contributions.ts         |  24 +-
 .../packages/superset-core/src/api/editors.ts      | 381 +++++++++++++++++++++
 .../packages/superset-core/src/api/index.ts        |   2 +
 .../superset-ui-core/src/components/index.ts       |   1 +
 .../SqlLab/components/AceEditorWrapper/index.tsx   | 249 --------------
 .../EditorWrapper.test.tsx}                        |  47 +--
 .../src/SqlLab/components/EditorWrapper/index.tsx  | 363 ++++++++++++++++++++
 .../useAnnotations.test.ts                         |   0
 .../useAnnotations.ts                              |   0
 .../useKeywords.test.ts                            |   0
 .../useKeywords.ts                                 |   0
 .../SqlLab/components/SqlEditor/SqlEditor.test.tsx |   4 +-
 .../src/SqlLab/components/SqlEditor/index.tsx      | 155 ++++++---
 .../TemplateParamsEditor.test.tsx                  |   4 +-
 .../components/TemplateParamsEditor/index.tsx      |  14 +-
 .../components/SQLEditorWithValidation/index.tsx   |  50 ++-
 .../src/core/editors/AceEditorProvider.tsx         | 325 ++++++++++++++++++
 .../src/core/editors/EditorHost.test.tsx           |  90 +++++
 superset-frontend/src/core/editors/EditorHost.tsx  | 128 +++++++
 .../src/core/editors/EditorProviders.test.ts       | 339 ++++++++++++++++++
 .../src/core/editors/EditorProviders.ts            | 243 +++++++++++++
 superset-frontend/src/core/editors/index.ts        | 130 +++++++
 superset-frontend/src/core/index.ts                |   1 +
 .../dashboard/components/PropertiesModal/index.tsx |   3 -
 .../PropertiesModal/sections/AdvancedSection.tsx   |  35 +-
 .../PropertiesModal/sections/StylingSection.tsx    |  13 +-
 .../gridComponents/Markdown/Markdown.jsx           |  34 +-
 .../gridComponents/Markdown/Markdown.test.tsx      |  25 +-
 .../DndColumnSelectControl/ColumnSelectPopover.tsx |   8 +-
 .../DndFilterSelect.test.tsx                       |   8 +-
 .../DndMetricSelect.test.tsx                       |  14 +-
 .../AdhocFilterEditPopoverSqlTabContent.test.tsx   |  16 +-
 .../AdhocFilterEditPopoverSqlTabContent/index.tsx  |   7 +-
 .../MetricControl/AdhocMetricEditPopover/index.tsx |  11 +-
 .../src/extensions/ExtensionsManager.ts            |  66 ++--
 .../src/extensions/ExtensionsStartup.tsx           |  11 +-
 .../src/features/annotations/AnnotationModal.tsx   |  38 +-
 .../src/features/cssTemplates/CssTemplateModal.tsx |  11 +-
 .../src/features/themes/ThemeModal.tsx             |  45 ++-
 46 files changed, 2706 insertions(+), 477 deletions(-)

diff --git a/docs/developer_portal/extensions/contribution-types.md b/docs/developer_portal/extensions/contribution-types.md
index c8943bb568b..7beb5f7ecec 100644
--- a/docs/developer_portal/extensions/contribution-types.md
+++ b/docs/developer_portal/extensions/contribution-types.md
@@ -34,7 +34,7 @@ Frontend contribution types allow extensions to extend Superset's user interface
 
 Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Each view is registered with a unique ID and can be activated or deactivated as needed. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
 
-``` json
+```json
 "frontend": {
   "contributions": {
     "views": {
@@ -53,7 +53,7 @@ Extensions can add new views or panels to the host application, such as custom S
 
 Extensions can define custom commands that can be executed within the host application, such as context-aware actions or menu options. Each command can specify properties like a unique command identifier, an icon, a title, and a description. These commands can be invoked by users through menus, keyboard shortcuts, or other UI elements, enabling extensions to add rich, interactive functionality to Superset.
 
-``` json
+```json
 "frontend": {
   "contributions": {
     "commands": [
@@ -72,7 +72,7 @@ Extensions can define custom commands that can be executed within the host appli
 
 Extensions can contribute new menu items or context menus to the host application, providing users with additional actions and options. Each menu item can specify properties such as the target view, the command to execute, its placement (primary, secondary, or context), and conditions for when it should be displayed. Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor), allowing extensions to seamlessly integrate their functionality into specific [...]
 
-``` json
+```json
 "frontend": {
   "contributions": {
     "menus": {
@@ -101,6 +101,27 @@ Extensions can contribute new menu items or context menus to the host applicatio
 }
 ```
 
+### Editors
+
+Extensions can replace Superset's default text editors with custom implementations. This enables enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions. When an extension registers an editor for a language, it replaces the default Ace editor in all locations that use that language (SQL Lab, Dashboard Properties, CSS editors, etc.).
+
+```json
+"frontend": {
+  "contributions": {
+    "editors": [
+      {
+        "id": "my_extension.monaco_sql",
+        "name": "Monaco SQL Editor",
+        "languages": ["sql"],
+        "description": "Monaco-based SQL editor with IntelliSense"
+      }
+    ]
+  }
+}
+```
+
+See [Editors Extension Point](./extension-points/editors) for implementation details.
+
 ## Backend
 
 Backend contribution types allow extensions to extend Superset's server-side capabilities with new API endpoints, MCP tools, and MCP prompts.
@@ -109,7 +130,7 @@ Backend contribution types allow extensions to extend Superset's server-side cap
 
 Extensions can register custom REST API endpoints under the `/api/v1/extensions/` namespace. This dedicated namespace prevents conflicts with built-in endpoints and provides a clear separation between core and extension functionality.
 
-``` json
+```json
 "backend": {
   "entryPoints": ["my_extension.entrypoint"],
   "files": ["backend/src/my_extension/**/*.py"]
@@ -118,7 +139,7 @@ Extensions can register custom REST API endpoints under the `/api/v1/extensions/
 
 The entry point module registers the API with Superset:
 
-``` python
+```python
 from superset_core.api.rest_api import add_extension_api
 from .api import MyExtensionAPI
 
diff --git a/docs/developer_portal/extensions/extension-points/editors.md b/docs/developer_portal/extensions/extension-points/editors.md
new file mode 100644
index 00000000000..4cc1da5d4c1
--- /dev/null
+++ b/docs/developer_portal/extensions/extension-points/editors.md
@@ -0,0 +1,245 @@
+---
+title: Editors
+sidebar_position: 2
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# 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 |
+
+By registering an editor provider for a language, your extension replaces the default Ace editor in **all** locations that use that language.
+
+## Manifest Configuration
+
+Declare editor contributions in your `extension.json` manifest:
+
+```json
+{
+  "name": "monaco-editor",
+  "version": "1.0.0",
+  "frontend": {
+    "contributions": {
+      "editors": [
+        {
+          "id": "monaco-editor.sql",
+          "name": "Monaco SQL Editor",
+          "languages": ["sql"],
+          "description": "Monaco-based SQL editor with IntelliSense"
+        }
+      ]
+    }
+  }
+}
+```
+
+## 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
+
+```typescript
+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
+
+```typescript
+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
+
+```typescript
+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;
+```
+
+### activate.ts
+
+```typescript
+import { editors } from '@apache-superset/core';
+import MonacoSQLEditor from './MonacoSQLEditor';
+
+export function activate(context) {
+  // Register the Monaco editor for SQL
+  const disposable = editors.registerEditorProvider(
+    {
+      id: 'monaco-sql-editor.sql',
+      name: 'Monaco SQL Editor',
+      languages: ['sql'],
+    },
+    MonacoSQLEditor,
+  );
+
+  context.subscriptions.push(disposable);
+}
+```
+
+## Handling Hotkeys
+
+Superset passes keyboard shortcuts via the `hotkeys` prop. Each hotkey includes an `exec` function that receives the `EditorHandle`:
+
+```typescript
+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:
+
+```typescript
+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`:
+
+```typescript
+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
+
+- **[SQL Lab Extension Points](./sqllab)** - Learn about other SQL Lab customizations
+- **[Contribution Types](../contribution-types)** - Explore other contribution types
+- **[Development](../development)** - Set up your development environment
diff --git a/docs/developer_portal/extensions/registry.md b/docs/developer_portal/extensions/registry.md
index 35fa409a727..20ebfaf1596 100644
--- a/docs/developer_portal/extensions/registry.md
+++ b/docs/developer_portal/extensions/registry.md
@@ -38,6 +38,7 @@ This page serves as a registry of community-created Superset extensions. These e
 | [SQL Lab Result Stats](https://github.com/michael-s-molina/superset-extensions/tree/main/result_stats)              | A SQL Lab extension that automatically computes statistics for query results, providing type-aware analysis including numeric metrics (min, max, mean, median, std dev), string analysis (length, empty counts), and date range information.        | Michael S. Molina  | <a href="/img/extensions/result-stats.png" target="_blank"><img src="/img/extensions/result-stats.png" al [...]
 | [SQL Snippets](https://github.com/michael-s-molina/superset-extensions/tree/main/sql_snippets)                      | A SQL Lab extension that provides reusable SQL code snippets, enabling quick insertion of commonly used code blocks such as license headers, author information, and frequently used SQL patterns.                                                  | Michael S. Molina  | <a href="/img/extensions/sql-snippets.png" target="_blank"><img src="/img/extensions/sql-snippets.png" al [...]
 | [SQL Lab Query Estimator](https://github.com/michael-s-molina/superset-extensions/tree/main/query_estimator)        | A SQL Lab panel that analyzes query execution plans to estimate resource impact, detect performance issues like Cartesian products and high-cost operations, and visualize the query plan tree.                                                     | Michael S. Molina  | <a href="/img/extensions/query-estimator.png" target="_blank"><img src="/img/extensions/query-estimator.p [...]
+| [Editors Bundle](https://github.com/michael-s-molina/superset-extensions/tree/main/editors_bundle)                  | A Superset extension that demonstrates how to provide custom code editors for different languages. This extension showcases the editor contribution system by registering alternative editors that can replace Superset's default Ace editor.        | Michael S. Molina  | <a href="/img/extensions/editors-bundle.png" target="_blank"><img src="/img/extensions/editors-bundle.pn [...]
 
 ## How to Add Your Extension
 
diff --git a/docs/static/img/extensions/editors-bundle.png b/docs/static/img/extensions/editors-bundle.png
new file mode 100644
index 00000000000..229a9b0c21e
Binary files /dev/null and b/docs/static/img/extensions/editors-bundle.png differ
diff --git a/scripts/check-type.js b/scripts/check-type.js
index 76fb7c23888..609b0cb391d 100755
--- a/scripts/check-type.js
+++ b/scripts/check-type.js
@@ -27,6 +27,11 @@ const { existsSync } = require("node:fs");
 const { chdir, cwd } = require("node:process");
 const { createRequire } = require("node:module");
 
+// Increase memory limit for TypeScript compiler
+if (!process.env.NODE_OPTIONS?.includes("--max-old-space-size")) {
+  process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS || ""} --max-old-space-size=8192`.trim();
+}
+
 const SUPERSET_ROOT = dirname(__dirname);
 const PACKAGE_ARG_REGEX = /^package=/;
 const EXCLUDE_DECLARATION_DIR_REGEX = /^excludeDeclarationDir=/;
diff --git a/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts b/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts
index 56724e6e9ed..8ca480aab6d 100644
--- a/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts
@@ -61,8 +61,8 @@ describe('SqlLab query tabs', () => {
   });
 
   it('opens a new tab by a button and a shortcut', () => {
-    const editorContent = '#ace-editor .ace_content';
-    const editorInput = '#ace-editor textarea';
+    const editorContent = '.ace_editor .ace_content';
+    const editorInput = '.ace_editor textarea';
     const queryLimitSelector = '#js-sql-toolbar .limitDropdown';
     cy.get(tabSelector).then(tabs => {
       const initialTabCount = tabs.length;
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 8d9518381cc..c7e2a5f04a3 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -84,7 +84,7 @@
     "tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --watch",
     "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
     "test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
-    "type": "tsc --noEmit",
+    "type": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit",
     "update-maps": "cd plugins/legacy-plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
     "validate-release": "../RELEASING/validate_this_release.sh"
   },
diff --git a/superset-frontend/packages/superset-core/src/api/contributions.ts b/superset-frontend/packages/superset-core/src/api/contributions.ts
index 2cc3bbb6ae2..174e02848fd 100644
--- a/superset-frontend/packages/superset-core/src/api/contributions.ts
+++ b/superset-frontend/packages/superset-core/src/api/contributions.ts
@@ -74,7 +74,27 @@ export interface ViewContribution {
 }
 
 /**
- * Aggregates all contributions (commands, menus, and views) provided by an extension or module.
+ * Describes an editor that can be contributed to the application.
+ * Extensions declare editor contributions in their extension.json manifest.
+ */
+export interface EditorContribution {
+  /** Unique identifier for the editor (e.g., "acme.monaco-sql") */
+  id: string;
+  /** Display name of the editor */
+  name: string;
+  /** Languages this editor supports */
+  languages: EditorLanguage[];
+  /** Optional description of the editor */
+  description?: string;
+}
+
+/**
+ * Supported editor languages.
+ */
+export type EditorLanguage = 'sql' | 'json' | 'yaml' | 'markdown' | 'css';
+
+/**
+ * Aggregates all contributions (commands, menus, views, and editors) provided by an extension or module.
  */
 export interface Contributions {
   /** List of command contributions. */
@@ -87,4 +107,6 @@ export interface Contributions {
   views: {
     [key: string]: ViewContribution[];
   };
+  /** List of editor contributions. */
+  editors?: EditorContribution[];
 }
diff --git a/superset-frontend/packages/superset-core/src/api/editors.ts b/superset-frontend/packages/superset-core/src/api/editors.ts
new file mode 100644
index 00000000000..8551ceb6f0b
--- /dev/null
+++ b/superset-frontend/packages/superset-core/src/api/editors.ts
@@ -0,0 +1,381 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview Editors API for Superset extension editor contributions.
+ *
+ * This module defines the interfaces and types for editor contributions to the
+ * Superset platform. Extensions can register custom text editor implementations
+ * (e.g., Monaco, CodeMirror) through the extension manifest, replacing the
+ * default Ace editor for specific languages.
+ */
+
+import { ForwardRefExoticComponent, RefAttributes } from 'react';
+import { EditorContribution, EditorLanguage } from './contributions';
+import { Disposable, Event } from './core';
+import type { SupersetTheme } from '../ui';
+
+// Re-export contribution types for convenience
+export type { EditorContribution, EditorLanguage };
+
+/**
+ * Represents a position in the editor (line and column).
+ */
+export interface Position {
+  /** Zero-based line number */
+  line: number;
+  /** Zero-based column number */
+  column: number;
+}
+
+/**
+ * Represents a range in the editor with start and end positions.
+ */
+export interface Range {
+  /** Start position of the range */
+  start: Position;
+  /** End position of the range */
+  end: Position;
+}
+
+/**
+ * Represents a selection in the editor.
+ */
+export interface Selection extends Range {
+  /** Direction of the selection */
+  direction?: 'ltr' | 'rtl';
+}
+
+/**
+ * Annotation severity levels for editor markers.
+ */
+export type AnnotationSeverity = 'error' | 'warning' | 'info';
+
+/**
+ * Represents an annotation (marker/diagnostic) in the editor.
+ */
+export interface EditorAnnotation {
+  /** Zero-based line number */
+  line: number;
+  /** Zero-based column number (optional) */
+  column?: number;
+  /** Annotation message to display */
+  message: string;
+  /** Severity level of the annotation */
+  severity: AnnotationSeverity;
+  /** Optional source of the annotation (e.g., "linter", "typescript") */
+  source?: string;
+}
+
+/**
+ * Represents a keyboard shortcut binding.
+ */
+export interface EditorHotkey {
+  /** Unique name for the hotkey command */
+  name: string;
+  /** Key binding string (e.g., "Ctrl+Enter", "Alt+Enter") */
+  key: string;
+  /** Description of what the hotkey does */
+  description?: string;
+  /** Function to execute when the hotkey is triggered */
+  exec: (handle: EditorHandle) => void;
+}
+
+/**
+ * Completion item kinds for autocompletion.
+ */
+export type CompletionItemKind =
+  | 'text'
+  | 'method'
+  | 'function'
+  | 'constructor'
+  | 'field'
+  | 'variable'
+  | 'class'
+  | 'interface'
+  | 'module'
+  | 'property'
+  | 'unit'
+  | 'value'
+  | 'enum'
+  | 'keyword'
+  | 'snippet'
+  | 'color'
+  | 'file'
+  | 'reference'
+  | 'folder'
+  | 'constant'
+  | 'struct'
+  | 'event'
+  | 'operator'
+  | 'typeParameter'
+  | 'table'
+  | 'column'
+  | 'schema'
+  | 'catalog'
+  | 'database';
+
+/**
+ * Represents a completion item for autocompletion.
+ */
+export interface CompletionItem {
+  /** Display label for the completion item */
+  label: string;
+  /** Text to insert when the item is selected */
+  insertText: string;
+  /** Kind of completion item for icon display */
+  kind: CompletionItemKind;
+  /** Optional documentation to show in the completion popup */
+  documentation?: string;
+  /** Optional detail text to show alongside the label */
+  detail?: string;
+  /** Sorting priority (higher numbers appear first) */
+  sortText?: string;
+  /** Text used for filtering completions */
+  filterText?: string;
+}
+
+/**
+ * Context provided to completion providers.
+ */
+export interface CompletionContext {
+  /** Character that triggered the completion (if any) */
+  triggerCharacter?: string;
+  /** How the completion was triggered */
+  triggerKind: 'invoke' | 'automatic';
+  /** Language of the editor */
+  language: EditorLanguage;
+  /** Generic metadata passed from the host (e.g., SQL Lab can pass database context) */
+  metadata?: Record<string, unknown>;
+}
+
+/**
+ * Provider interface for dynamic completions.
+ */
+export interface CompletionProvider {
+  /** Unique identifier for this provider */
+  id: string;
+  /** Trigger characters that invoke this provider (e.g., '.', ' ') */
+  triggerCharacters?: string[];
+  /**
+   * Provide completions at the given position.
+   * @param content The editor content
+   * @param position The cursor position
+   * @param context Completion context with trigger info and metadata
+   * @returns Array of completion items or a promise that resolves to them
+   */
+  provideCompletions(
+    content: string,
+    position: Position,
+    context: CompletionContext,
+  ): CompletionItem[] | Promise<CompletionItem[]>;
+}
+
+/**
+ * A keyword for editor autocomplete.
+ * This is a generic format that editor implementations convert to their native format.
+ */
+export interface EditorKeyword {
+  /** Display name of the keyword */
+  name: string;
+  /** Value to insert when selected (defaults to name if not provided) */
+  value?: string;
+  /** Category/type of the keyword (e.g., "column", "table", "function") */
+  meta?: string;
+  /** Optional score for sorting (higher = more relevant) */
+  score?: number;
+}
+
+/**
+ * Props that all editor implementations must accept.
+ */
+export interface EditorProps {
+  /** Instance identifier */
+  id: string;
+  /** Controlled value */
+  value: string;
+  /** Content change handler */
+  onChange: (value: string) => void;
+  /** Blur handler */
+  onBlur?: (value: string) => void;
+  /** Cursor position change handler */
+  onCursorPositionChange?: (pos: Position) => void;
+  /** Selection change handler */
+  onSelectionChange?: (sel: Selection[]) => void;
+  /** Language mode for syntax highlighting */
+  language: EditorLanguage;
+  /** Whether the editor is read-only */
+  readOnly?: boolean;
+  /** Tab size in spaces */
+  tabSize?: number;
+  /** Whether to show line numbers */
+  lineNumbers?: boolean;
+  /** Whether to enable word wrap */
+  wordWrap?: boolean;
+  /** Linting/error annotations */
+  annotations?: EditorAnnotation[];
+  /** Keyboard shortcuts */
+  hotkeys?: EditorHotkey[];
+  /** Static keywords for autocomplete */
+  keywords?: EditorKeyword[];
+  /** CSS height (e.g., "100%", "500px") */
+  height?: string;
+  /** CSS width (e.g., "100%", "800px") */
+  width?: string;
+  /** 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>;
+  /** Theme object for styling the editor */
+  theme?: SupersetTheme;
+}
+
+/**
+ * Imperative API for controlling the editor programmatically.
+ */
+export interface EditorHandle {
+  /** Focus the editor */
+  focus(): void;
+  /** Get the current editor content */
+  getValue(): string;
+  /** Set the editor content */
+  setValue(value: string): void;
+  /** Get the current cursor position */
+  getCursorPosition(): Position;
+  /** Move the cursor to a specific position */
+  moveCursorToPosition(position: Position): void;
+  /** Get all selections in the editor */
+  getSelections(): Selection[];
+  /** Set the selection range */
+  setSelection(selection: Range): void;
+  /** Get the selected text */
+  getSelectedText(): string;
+  /** Insert text at the current cursor position */
+  insertText(text: string): void;
+  /** Execute a named editor command */
+  executeCommand(commandName: string): void;
+  /** Scroll to a specific line */
+  scrollToLine(line: number): void;
+  /** Set annotations (replaces existing) */
+  setAnnotations(annotations: EditorAnnotation[]): void;
+  /** Clear all annotations */
+  clearAnnotations(): void;
+  /**
+   * Register a completion provider for dynamic suggestions.
+   * @param provider The completion provider to register
+   * @returns A Disposable to unregister the provider
+   */
+  registerCompletionProvider(provider: CompletionProvider): Disposable;
+}
+
+/**
+ * React component type for editor implementations.
+ * Must be a forwardRef component to expose the EditorHandle.
+ */
+export type EditorComponent = ForwardRefExoticComponent<
+  EditorProps & RefAttributes<EditorHandle>
+>;
+
+/**
+ * A registered editor provider with its contribution metadata and component.
+ */
+export interface EditorProvider {
+  /** The editor contribution metadata */
+  contribution: EditorContribution;
+  /** The React component implementing the editor */
+  component: EditorComponent;
+}
+
+/**
+ * Event fired when an editor provider is registered.
+ */
+export interface EditorProviderRegisteredEvent {
+  /** The registered provider */
+  provider: EditorProvider;
+}
+
+/**
+ * Event fired when an editor provider is unregistered.
+ */
+export interface EditorProviderUnregisteredEvent {
+  /** The contribution that was unregistered */
+  contribution: EditorContribution;
+}
+
+/**
+ * Register an editor provider for specific languages.
+ * When an extension registers an editor, it replaces the default for those languages.
+ *
+ * @param contribution The editor contribution metadata from extension.json
+ * @param component The React component implementing EditorProps
+ * @returns A Disposable to unregister the provider
+ *
+ * @example
+ * ```typescript
+ * const disposable = registerEditorProvider(
+ *   {
+ *     id: 'acme.monaco-sql',
+ *     name: 'Monaco SQL Editor',
+ *     languages: ['sql'],
+ *   },
+ *   MonacoSQLEditor
+ * );
+ * context.disposables.push(disposable);
+ * ```
+ */
+export declare function registerEditorProvider(
+  contribution: EditorContribution,
+  component: EditorComponent,
+): Disposable;
+
+/**
+ * Get the editor provider for a specific language.
+ * Returns the extension's editor if registered, otherwise undefined.
+ *
+ * @param language The language to get an editor for
+ * @returns The editor provider or undefined if no extension provides one
+ */
+export declare function getEditorProvider(
+  language: EditorLanguage,
+): EditorProvider | undefined;
+
+/**
+ * Check if an extension has registered an editor for a language.
+ *
+ * @param language The language to check
+ * @returns True if an extension provides an editor for this language
+ */
+export declare function hasEditorProvider(language: EditorLanguage): boolean;
+
+/**
+ * Get all registered editor providers.
+ *
+ * @returns Array of all registered editor providers
+ */
+export declare function getAllEditorProviders(): EditorProvider[];
+
+/**
+ * Event fired when an editor provider is registered.
+ */
+export declare const onDidRegisterEditorProvider: Event<EditorProviderRegisteredEvent>;
+
+/**
+ * Event fired when an editor provider is unregistered.
+ */
+export declare const onDidUnregisterEditorProvider: Event<EditorProviderUnregisteredEvent>;
diff --git a/superset-frontend/packages/superset-core/src/api/index.ts b/superset-frontend/packages/superset-core/src/api/index.ts
index a1964a1eb87..d0df8513d16 100644
--- a/superset-frontend/packages/superset-core/src/api/index.ts
+++ b/superset-frontend/packages/superset-core/src/api/index.ts
@@ -28,6 +28,7 @@
  * - `commands`: Execute Superset commands and operations
  * - `contributions`: Register UI contributions and customizations
  * - `core`: Access fundamental Superset types and utilities
+ * - `editors`: Register custom text editor implementations
  * - `extensions`: Manage extension lifecycle and metadata
  * - `sqlLab`: Integrate with SQL Lab functionality
  */
@@ -36,5 +37,6 @@ export * as authentication from './authentication';
 export * as commands from './commands';
 export * as contributions from './contributions';
 export * as core from './core';
+export * as editors from './editors';
 export * as extensions from './extensions';
 export * as sqlLab from './sqlLab';
diff --git a/superset-frontend/packages/superset-ui-core/src/components/index.ts b/superset-frontend/packages/superset-ui-core/src/components/index.ts
index eecfdf90f88..30bad84f4d8 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts
@@ -46,6 +46,7 @@ export {
   ConfigEditor,
   type AsyncAceEditorProps,
   type Editor,
+  type AceCompleterKeyword,
 } from './AsyncAceEditor';
 export { AutoComplete, type AutoCompleteProps } from './AutoComplete';
 export {
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
deleted file mode 100644
index 0a9231e26a9..00000000000
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { useState, useEffect, useRef } from 'react';
-import type { IAceEditor } from 'react-ace/lib/types';
-import { shallowEqual, useDispatch, useSelector } from 'react-redux';
-import { usePrevious } from '@superset-ui/core';
-import { css, useTheme } from '@apache-superset/core/ui';
-import { Global } from '@emotion/react';
-
-import { SQL_EDITOR_LEFTBAR_WIDTH } from 'src/SqlLab/constants';
-import { queryEditorSetSelectedText } from 'src/SqlLab/actions/sqlLab';
-import { FullSQLEditor as AceEditor } from '@superset-ui/core/components';
-import type { KeyboardShortcut } from 'src/SqlLab/components/KeyboardShortcutButton';
-import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
-import { SqlLabRootState, type CursorPosition } from 'src/SqlLab/types';
-import { useAnnotations } from './useAnnotations';
-import { useKeywords } from './useKeywords';
-
-type HotKey = {
-  key: KeyboardShortcut;
-  descr?: string;
-  name: string;
-  func: (aceEditor: IAceEditor) => void;
-};
-
-type AceEditorWrapperProps = {
-  autocomplete: boolean;
-  onBlur: (sql: string) => void;
-  onChange: (sql: string) => void;
-  queryEditorId: string;
-  onCursorPositionChange: (position: CursorPosition) => void;
-  height: string;
-  hotkeys: HotKey[];
-};
-
-const AceEditorWrapper = ({
-  autocomplete,
-  onBlur = () => {},
-  onChange = () => {},
-  queryEditorId,
-  onCursorPositionChange,
-  height,
-  hotkeys,
-}: AceEditorWrapperProps) => {
-  const dispatch = useDispatch();
-  const queryEditor = useQueryEditor(queryEditorId, [
-    'id',
-    'dbId',
-    'sql',
-    'catalog',
-    'schema',
-    'templateParams',
-    'tabViewId',
-  ]);
-  // Prevent a maximum update depth exceeded error
-  // by skipping access the unsaved query editor state
-  const cursorPosition = useSelector<SqlLabRootState, CursorPosition>(
-    ({ sqlLab: { queryEditors } }) => {
-      const { cursorPosition } = {
-        ...queryEditors.find(({ id }) => id === queryEditorId),
-      };
-      return cursorPosition ?? { row: 0, column: 0 };
-    },
-    shallowEqual,
-  );
-
-  const currentSql = queryEditor.sql ?? '';
-  const [sql, setSql] = useState(currentSql);
-  const theme = useTheme();
-
-  // The editor changeSelection is called multiple times in a row,
-  // faster than React reconciliation process, so the selected text
-  // needs to be stored out of the state to ensure changes to it
-  // get saved immediately
-  const currentSelectionCache = useRef('');
-
-  useEffect(() => {
-    // Making sure no text is selected from previous mount
-    dispatch(queryEditorSetSelectedText(queryEditor, null));
-  }, []);
-
-  const prevSql = usePrevious(currentSql);
-
-  useEffect(() => {
-    if (currentSql !== prevSql) {
-      setSql(currentSql);
-    }
-  }, [currentSql]);
-
-  const onBlurSql = () => {
-    onBlur(sql);
-  };
-
-  const onAltEnter = () => {
-    onBlur(sql);
-  };
-
-  const onEditorLoad = (editor: any) => {
-    editor.commands.addCommand({
-      name: 'runQuery',
-      bindKey: { win: 'Alt-enter', mac: 'Alt-enter' },
-      exec: () => {
-        onAltEnter();
-      },
-    });
-
-    hotkeys.forEach(keyConfig => {
-      editor.commands.addCommand({
-        name: keyConfig.name,
-        bindKey: { win: keyConfig.key, mac: keyConfig.key },
-        exec: keyConfig.func,
-      });
-    });
-    const marginSize = theme.sizeUnit * 2;
-    editor.renderer.setScrollMargin(marginSize, marginSize, 0, 0);
-    editor.$blockScrolling = Infinity; // eslint-disable-line no-param-reassign
-    editor.selection.on('changeSelection', () => {
-      const selectedText = editor.getSelectedText();
-
-      // Backspace trigger 1 character selection, ignoring
-      if (
-        selectedText !== currentSelectionCache.current &&
-        selectedText.length !== 1
-      ) {
-        dispatch(queryEditorSetSelectedText(queryEditor, selectedText));
-      }
-
-      currentSelectionCache.current = selectedText;
-    });
-
-    editor.selection.on('changeCursor', () => {
-      const cursor = editor.getCursorPosition();
-      onCursorPositionChange(cursor);
-    });
-
-    const { row, column } = cursorPosition;
-    editor.moveCursorToPosition({ row, column });
-    editor.focus();
-    editor.scrollToLine(row, true, true);
-  };
-
-  const onChangeText = (text: string) => {
-    if (text !== sql) {
-      setSql(text);
-      onChange(text);
-    }
-  };
-
-  const { data: annotations } = useAnnotations({
-    dbId: queryEditor.dbId,
-    catalog: queryEditor.catalog,
-    schema: queryEditor.schema,
-    sql: currentSql,
-    templateParams: queryEditor.templateParams,
-  });
-
-  const keywords = useKeywords(
-    {
-      queryEditorId,
-      dbId: queryEditor.dbId,
-      catalog: queryEditor.catalog,
-      schema: queryEditor.schema,
-      tabViewId: queryEditor.tabViewId,
-    },
-    !autocomplete,
-  );
-
-  return (
-    <>
-      <Global
-        styles={css`
-          .ace_text-layer {
-            width: 100% !important;
-          }
-
-          .ace_content,
-          .SqlEditor .sql-container .ace_gutter {
-            background-color: ${theme.colorBgBase} !important;
-          }
-
-          .ace_gutter::after {
-            content: '';
-            position: absolute;
-            top: 0;
-            bottom: 0;
-            right: ${theme.sizeUnit * 2}px;
-            width: 1px;
-            height: 100%;
-            background-color: ${theme.colorBorder};
-          }
-
-          .ace_gutter,
-          .ace_scroller {
-            background-color: ${theme.colorBgBase} !important;
-          }
-
-          .ace_autocomplete {
-            // Use !important because Ace Editor applies extra CSS at the last second
-            // when opening the autocomplete.
-            width: ${theme.sizeUnit * 130}px !important;
-          }
-
-          .ace_completion-highlight {
-            color: ${theme.colorPrimaryText} !important;
-            background-color: ${theme.colorPrimaryBgHover};
-          }
-
-          .ace_tooltip {
-            max-width: ${SQL_EDITOR_LEFTBAR_WIDTH}px;
-          }
-
-          .ace_scroller {
-            background-color: ${theme.colorBgLayout};
-          }
-        `}
-      />
-      <AceEditor
-        keywords={keywords}
-        onLoad={onEditorLoad}
-        onBlur={onBlurSql}
-        height={height}
-        onChange={onChangeText}
-        width="100%"
-        editorProps={{ $blockScrolling: true }}
-        enableLiveAutocompletion={autocomplete}
-        value={sql}
-        annotations={annotations}
-      />
-    </>
-  );
-};
-
-export default AceEditorWrapper;
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx b/superset-frontend/src/SqlLab/components/EditorWrapper/EditorWrapper.test.tsx
similarity index 74%
rename from superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx
rename to superset-frontend/src/SqlLab/components/EditorWrapper/EditorWrapper.test.tsx
index 5b38f047839..3252f716579 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx
+++ b/superset-frontend/src/SqlLab/components/EditorWrapper/EditorWrapper.test.tsx
@@ -21,11 +21,8 @@ import { render, waitFor, createStore } from 'spec/helpers/testing-library';
 import { QueryEditor } from 'src/SqlLab/types';
 import { Store } from 'redux';
 import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import AceEditorWrapper from 'src/SqlLab/components/AceEditorWrapper';
-import {
-  FullSQLEditor,
-  type AsyncAceEditorProps,
-} from '@superset-ui/core/components';
+import EditorWrapper from 'src/SqlLab/components/EditorWrapper';
+import type { editors } from '@apache-superset/core';
 import {
   queryEditorSetCursorPosition,
   queryEditorSetDb,
@@ -43,17 +40,21 @@ jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => () => (
   <div data-test="mock-deprecated-async-select" />
 ));
 
-jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({
-  FullSQLEditor: jest
-    .fn()
-    .mockImplementation((props: AsyncAceEditorProps) => (
-      <div data-test="react-ace">{JSON.stringify(props)}</div>
-    )),
+// Mock EditorHost from the editors module
+const MockEditorHost = jest
+  .fn()
+  .mockImplementation((props: editors.EditorProps) => (
+    <div data-test="editor-host">{JSON.stringify(props)}</div>
+  ));
+
+jest.mock('src/core/editors', () => ({
+  ...jest.requireActual('src/core/editors'),
+  EditorHost: (props: editors.EditorProps) => MockEditorHost(props),
 }));
 
 const setup = (queryEditor: QueryEditor, store?: Store) =>
   render(
-    <AceEditorWrapper
+    <EditorWrapper
       queryEditorId={queryEditor.id}
       height="100px"
       hotkeys={[]}
@@ -69,17 +70,17 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
   );
 
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
-describe('AceEditorWrapper', () => {
+describe('EditorWrapper', () => {
   beforeEach(() => {
-    (FullSQLEditor as any as jest.Mock).mockClear();
+    MockEditorHost.mockClear();
   });
 
-  test('renders ace editor including sql value', async () => {
+  test('renders editor including sql value', async () => {
     const store = createStore(initialState, reducerIndex);
     const { getByTestId } = setup(defaultQueryEditor, store);
-    await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument());
+    await waitFor(() => expect(getByTestId('editor-host')).toBeInTheDocument());
 
-    expect(getByTestId('react-ace')).toHaveTextContent(
+    expect(getByTestId('editor-host')).toHaveTextContent(
       JSON.stringify({ value: defaultQueryEditor.sql }).slice(1, -1),
     );
   });
@@ -101,10 +102,10 @@ describe('AceEditorWrapper', () => {
     );
     const { getByTestId } = setup(defaultQueryEditor, store);
 
-    expect(getByTestId('react-ace')).not.toHaveTextContent(
+    expect(getByTestId('editor-host')).not.toHaveTextContent(
       JSON.stringify({ value: expectedSql }).slice(1, -1),
     );
-    expect(getByTestId('react-ace')).toHaveTextContent(
+    expect(getByTestId('editor-host')).toHaveTextContent(
       JSON.stringify({ value: defaultQueryEditor.sql }).slice(1, -1),
     );
   });
@@ -113,14 +114,14 @@ describe('AceEditorWrapper', () => {
     const store = createStore(initialState, reducerIndex);
     setup(defaultQueryEditor, store);
 
-    expect(FullSQLEditor).toHaveBeenCalled();
-    const renderCount = (FullSQLEditor as any as jest.Mock).mock.calls.length;
+    expect(MockEditorHost).toHaveBeenCalled();
+    const renderCount = MockEditorHost.mock.calls.length;
     const updatedCursorPosition = { row: 1, column: 9 };
     store.dispatch(
       queryEditorSetCursorPosition(defaultQueryEditor, updatedCursorPosition),
     );
-    expect(FullSQLEditor).toHaveBeenCalledTimes(renderCount);
+    expect(MockEditorHost).toHaveBeenCalledTimes(renderCount);
     store.dispatch(queryEditorSetDb(defaultQueryEditor, 2));
-    expect(FullSQLEditor).toHaveBeenCalledTimes(renderCount + 1);
+    expect(MockEditorHost).toHaveBeenCalledTimes(renderCount + 1);
   });
 });
diff --git a/superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx
new file mode 100644
index 00000000000..e6b5ac1afea
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx
@@ -0,0 +1,363 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
+import { shallowEqual, useDispatch, useSelector } from 'react-redux';
+import { usePrevious } from '@superset-ui/core';
+import { css, useTheme } from '@apache-superset/core/ui';
+import { Global } from '@emotion/react';
+import type { editors } from '@apache-superset/core';
+
+import { SQL_EDITOR_LEFTBAR_WIDTH } from 'src/SqlLab/constants';
+import { queryEditorSetSelectedText } from 'src/SqlLab/actions/sqlLab';
+import type { KeyboardShortcut } from 'src/SqlLab/components/KeyboardShortcutButton';
+import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
+import { SqlLabRootState, type CursorPosition } from 'src/SqlLab/types';
+import { EditorHost } from 'src/core/editors';
+import { useAnnotations } from './useAnnotations';
+import { useKeywords } from './useKeywords';
+
+type EditorHandle = editors.EditorHandle;
+type EditorHotkey = editors.EditorHotkey;
+type EditorAnnotation = editors.EditorAnnotation;
+
+type HotKey = {
+  key: KeyboardShortcut;
+  descr?: string;
+  name: string;
+  func: (editor: EditorHandle) => void;
+};
+
+type EditorWrapperProps = {
+  autocomplete: boolean;
+  onBlur: (sql: string) => void;
+  onChange: (sql: string) => void;
+  queryEditorId: string;
+  onCursorPositionChange: (position: CursorPosition) => void;
+  height: string;
+  hotkeys: HotKey[];
+};
+
+/**
+ * Convert legacy HotKey format to EditorHotkey format.
+ */
+const convertHotkeys = (
+  hotkeys: HotKey[],
+  onRunQuery: () => void,
+): EditorHotkey[] => {
+  const result: EditorHotkey[] = [
+    // Add the built-in run query hotkey
+    {
+      name: 'runQuery',
+      key: 'Alt-enter',
+      description: 'Run query',
+      exec: () => onRunQuery(),
+    },
+  ];
+
+  hotkeys.forEach(keyConfig => {
+    result.push({
+      name: keyConfig.name,
+      key: keyConfig.key,
+      description: keyConfig.descr,
+      exec: keyConfig.func,
+    });
+  });
+
+  return result;
+};
+
+/**
+ * Ace annotation format returned from useAnnotations when data is available.
+ */
+type AceAnnotation = {
+  row: number;
+  column: number;
+  text: string | null;
+  type: string;
+};
+
+/**
+ * Type guard to check if an annotation is in Ace format.
+ */
+const isAceAnnotation = (ann: unknown): ann is AceAnnotation =>
+  typeof ann === 'object' &&
+  ann !== null &&
+  'row' in ann &&
+  'column' in ann &&
+  'text' in ann &&
+  'type' in ann;
+
+/**
+ * Convert annotation array to EditorAnnotation format.
+ * Handles the union type returned from useAnnotations.
+ */
+const convertAnnotations = (
+  annotations?: unknown[],
+): EditorAnnotation[] | undefined => {
+  if (!annotations || annotations.length === 0) return undefined;
+  // Check if first item is in Ace format (has row, column, text, type)
+  if (!isAceAnnotation(annotations[0])) return undefined;
+  return (annotations as AceAnnotation[]).map(ann => ({
+    line: ann.row,
+    column: ann.column,
+    message: ann.text ?? '',
+    severity: ann.type as EditorAnnotation['severity'],
+  }));
+};
+
+/**
+ * EditorWrapper component that renders the SQL editor using EditorHost.
+ * Uses the default Ace editor or an extension-provided editor based on
+ * what's registered with the editors API.
+ */
+const EditorWrapper = ({
+  autocomplete,
+  onBlur = () => {},
+  onChange = () => {},
+  queryEditorId,
+  onCursorPositionChange,
+  height,
+  hotkeys,
+}: EditorWrapperProps) => {
+  const dispatch = useDispatch();
+  const queryEditor = useQueryEditor(queryEditorId, [
+    'id',
+    'dbId',
+    'sql',
+    'catalog',
+    'schema',
+    'templateParams',
+    'tabViewId',
+  ]);
+  // Prevent a maximum update depth exceeded error
+  // by skipping access the unsaved query editor state
+  const cursorPosition = useSelector<SqlLabRootState, CursorPosition>(
+    ({ sqlLab: { queryEditors } }) => {
+      const editor = queryEditors.find(({ id }) => id === queryEditorId);
+      return editor?.cursorPosition ?? { row: 0, column: 0 };
+    },
+    shallowEqual,
+  );
+
+  const currentSql = queryEditor.sql ?? '';
+  const [sql, setSql] = useState(currentSql);
+  const theme = useTheme();
+  const editorHandleRef = useRef<EditorHandle | null>(null);
+
+  // The editor changeSelection is called multiple times in a row,
+  // faster than React reconciliation process, so the selected text
+  // needs to be stored out of the state to ensure changes to it
+  // get saved immediately
+  const currentSelectionCache = useRef('');
+
+  useEffect(() => {
+    // Making sure no text is selected from previous mount
+    dispatch(queryEditorSetSelectedText(queryEditor, null));
+  }, []);
+
+  const prevSql = usePrevious(currentSql);
+
+  useEffect(() => {
+    if (currentSql !== prevSql) {
+      setSql(currentSql);
+    }
+  }, [currentSql]);
+
+  const onBlurSql = useCallback(
+    (value: string) => {
+      onBlur(value);
+    },
+    [onBlur],
+  );
+
+  const onAltEnter = useCallback(() => {
+    onBlur(sql);
+  }, [onBlur, sql]);
+
+  const onChangeText = useCallback(
+    (text: string) => {
+      if (text !== sql) {
+        setSql(text);
+        onChange(text);
+      }
+    },
+    [sql, onChange],
+  );
+
+  // Handle cursor position changes
+  const handleCursorPositionChange = useCallback(
+    (pos: { line: number; column: number }) => {
+      onCursorPositionChange({ row: pos.line, column: pos.column });
+    },
+    [onCursorPositionChange],
+  );
+
+  // Handle selection changes
+  const handleSelectionChange = useCallback(
+    (
+      selections: Array<{
+        start: { line: number; column: number };
+        end: { line: number; column: number };
+      }>,
+    ) => {
+      if (editorHandleRef.current && selections.length > 0) {
+        const selectedText = editorHandleRef.current.getSelectedText();
+        if (
+          selectedText !== currentSelectionCache.current &&
+          selectedText.length !== 1
+        ) {
+          dispatch(queryEditorSetSelectedText(queryEditor, selectedText));
+        }
+        currentSelectionCache.current = selectedText;
+      }
+    },
+    [dispatch, queryEditor],
+  );
+
+  // Handle editor ready callback
+  const handleEditorReady = useCallback(
+    (handle: EditorHandle) => {
+      editorHandleRef.current = handle;
+      // Set initial cursor position
+      const { row, column } = cursorPosition;
+      handle.moveCursorToPosition({ line: row, column });
+      handle.focus();
+      handle.scrollToLine(row);
+    },
+    [cursorPosition],
+  );
+
+  const { data: annotations } = useAnnotations({
+    dbId: queryEditor.dbId,
+    catalog: queryEditor.catalog,
+    schema: queryEditor.schema,
+    sql: currentSql,
+    templateParams: queryEditor.templateParams,
+  });
+
+  const keywords = useKeywords(
+    {
+      queryEditorId,
+      dbId: queryEditor.dbId,
+      catalog: queryEditor.catalog,
+      schema: queryEditor.schema,
+      tabViewId: queryEditor.tabViewId,
+    },
+    !autocomplete,
+  );
+
+  // Convert hotkeys and annotations for the editor
+  const editorHotkeys = useMemo(
+    () => convertHotkeys(hotkeys, onAltEnter),
+    [hotkeys, onAltEnter],
+  );
+  const editorAnnotations = useMemo(
+    () => convertAnnotations(annotations),
+    [annotations],
+  );
+
+  // Metadata for the editor (e.g., database context for completions)
+  const metadata = useMemo(
+    () => ({
+      dbId: queryEditor.dbId,
+      catalog: queryEditor.catalog,
+      schema: queryEditor.schema,
+      queryEditorId,
+    }),
+    [queryEditor.dbId, queryEditor.catalog, queryEditor.schema, queryEditorId],
+  );
+
+  // Global styles for the editor
+  const globalStyles = (
+    <Global
+      styles={css`
+        .ace_text-layer {
+          width: 100% !important;
+        }
+
+        .ace_content,
+        .SqlEditor .sql-container .ace_gutter {
+          background-color: ${theme.colorBgBase} !important;
+        }
+
+        .ace_gutter::after {
+          content: '';
+          position: absolute;
+          top: 0;
+          bottom: 0;
+          right: ${theme.sizeUnit * 2}px;
+          width: 1px;
+          height: 100%;
+          background-color: ${theme.colorBorder};
+        }
+
+        .ace_gutter,
+        .ace_scroller {
+          background-color: ${theme.colorBgBase} !important;
+        }
+
+        .ace_autocomplete {
+          // Use !important because Ace Editor applies extra CSS at the last second
+          // when opening the autocomplete.
+          width: ${theme.sizeUnit * 130}px !important;
+        }
+
+        .ace_completion-highlight {
+          color: ${theme.colorPrimaryText} !important;
+          background-color: ${theme.colorPrimaryBgHover};
+        }
+
+        .ace_tooltip {
+          max-width: ${SQL_EDITOR_LEFTBAR_WIDTH}px;
+        }
+
+        .ace_scroller {
+          background-color: ${theme.colorBgLayout};
+        }
+      `}
+    />
+  );
+
+  return (
+    <>
+      {globalStyles}
+      <EditorHost
+        id={queryEditorId}
+        value={sql}
+        onChange={onChangeText}
+        onBlur={onBlurSql}
+        onCursorPositionChange={handleCursorPositionChange}
+        onSelectionChange={handleSelectionChange}
+        onReady={handleEditorReady}
+        language="sql"
+        tabSize={2}
+        lineNumbers
+        annotations={editorAnnotations}
+        hotkeys={editorHotkeys}
+        height={height}
+        width="100%"
+        metadata={metadata}
+        keywords={keywords}
+      />
+    </>
+  );
+};
+
+// Export with the legacy name for backward compatibility
+export default EditorWrapper;
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useAnnotations.test.ts
similarity index 100%
rename from superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts
rename to superset-frontend/src/SqlLab/components/EditorWrapper/useAnnotations.test.ts
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useAnnotations.ts
similarity index 100%
rename from superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts
rename to superset-frontend/src/SqlLab/components/EditorWrapper/useAnnotations.ts
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts
similarity index 100%
rename from superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
rename to superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts
similarity index 100%
rename from superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
rename to superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
index b15bbdadc46..6eab25215f3 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
@@ -206,7 +206,7 @@ describe('SqlEditor', () => {
   });
 
   // Update other similar tests with timeouts
-  test('render an AceEditorWrapper', async () => {
+  test('render an EditorWrapper', async () => {
     const { findByTestId, unmount } = setup(mockedProps, store);
 
     await waitFor(
@@ -217,7 +217,7 @@ describe('SqlEditor', () => {
     unmount();
   }, 15000);
 
-  test('skip rendering an AceEditorWrapper when the current tab is inactive', async () => {
+  test('skip rendering an EditorWrapper when the current tab is inactive', async () => {
     const { queryByTestId } = setup(
       {
         ...mockedProps,
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index da2caaf006b..18262329970 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -28,7 +28,7 @@ import {
   FC,
 } from 'react';
 
-import type AceEditor from 'react-ace';
+import type { editors } from '@apache-superset/core';
 import useEffectEvent from 'src/hooks/useEffectEvent';
 import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 import AutoSizer from 'react-virtualized-auto-sizer';
@@ -111,7 +111,7 @@ import SaveQuery, { QueryPayload } from '../SaveQuery';
 import ScheduleQueryButton from '../ScheduleQueryButton';
 import EstimateQueryCostButton from '../EstimateQueryCostButton';
 import ShareSqlLabQuery from '../ShareSqlLabQuery';
-import AceEditorWrapper from '../AceEditorWrapper';
+import EditorWrapper from '../EditorWrapper';
 import RunQueryActionButton from '../RunQueryActionButton';
 import QueryLimitSelect from '../QueryLimitSelect';
 import KeyboardShortcutButton, {
@@ -429,73 +429,136 @@ const SqlEditor: FC<Props> = ({
   }, [dispatch, queryEditor.sql, startQuery, stopQuery, formatCurrentQuery]);
 
   const hotkeys = useMemo(() => {
-    // Get all hotkeys including ace editor hotkeys
+    // Get all hotkeys including editor hotkeys
     // Get the user's OS
     const userOS = detectOS();
+
+    type EditorHandle = editors.EditorHandle;
+
+    /**
+     * Find the position of a semicolon in the given direction from a starting position.
+     * Returns the position after the semicolon (for backwards) or at the semicolon (for forwards).
+     */
+    const findSemicolon = (
+      lines: string[],
+      fromLine: number,
+      fromColumn: number,
+      backwards: boolean,
+    ): { line: number; column: number } | null => {
+      if (backwards) {
+        // Search backwards: start from current position going up
+        for (let line = fromLine; line >= 0; line -= 1) {
+          const lineText = lines[line];
+          const searchEnd = line === fromLine ? fromColumn : lineText.length;
+          // Search from right to left within the line
+          const idx = lineText.lastIndexOf(';', searchEnd - 1);
+          if (idx !== -1) {
+            // Return position after the semicolon
+            return { line, column: idx + 1 };
+          }
+        }
+      } else {
+        // Search forwards: start from current position going down
+        for (let line = fromLine; line < lines.length; line += 1) {
+          const lineText = lines[line];
+          const searchStart = line === fromLine ? fromColumn + 1 : 0;
+          const idx = lineText.indexOf(';', searchStart);
+          if (idx !== -1) {
+            // Return position at the semicolon (end of statement)
+            return { line, column: idx + 1 };
+          }
+        }
+      }
+      return null;
+    };
+
     const base = [
       ...getHotkeyConfig(),
       {
         name: 'runQuery3',
         key: KeyboardShortcut.CtrlShiftEnter,
         descr: KEY_MAP[KeyboardShortcut.CtrlShiftEnter],
-        func: (editor: AceEditor['editor']) => {
-          if (!editor.getValue().trim()) {
+        func: (editor: EditorHandle) => {
+          const value = editor.getValue();
+          if (!value.trim()) {
             return;
           }
-          const session = editor.getSession();
+
+          const lines = value.split('\n');
           const cursorPosition = editor.getCursorPosition();
-          const totalLine = session.getLength();
-          const currentRow = editor.getFirstVisibleRow();
-          const semicolonEnd = editor.find(';', {
-            backwards: false,
-            skipCurrent: true,
-          });
-          let end;
-          if (semicolonEnd) {
-            ({ end } = semicolonEnd);
-          }
-          if (!end || end.row < cursorPosition.row) {
+          const totalLines = lines.length;
+
+          // Find the end of the statement (next semicolon or end of file)
+          const semicolonEnd = findSemicolon(
+            lines,
+            cursorPosition.line,
+            cursorPosition.column,
+            false,
+          );
+          let end: { line: number; column: number };
+          if (semicolonEnd && semicolonEnd.line >= cursorPosition.line) {
+            end = semicolonEnd;
+          } else {
+            // No semicolon found forward, use end of file
+            const lastLineIndex = totalLines - 1;
             end = {
-              row: totalLine + 1,
-              column: 0,
+              line: lastLineIndex,
+              column: lines[lastLineIndex]?.length ?? 0,
             };
           }
-          const semicolonStart = editor.find(';', {
-            backwards: true,
-            skipCurrent: true,
-          });
-          let start;
+
+          // Find the start of the statement (previous semicolon or start of file)
+          const semicolonStart = findSemicolon(
+            lines,
+            cursorPosition.line,
+            cursorPosition.column,
+            true,
+          );
+          let start: { line: number; column: number } | undefined;
           if (semicolonStart) {
-            start = semicolonStart.end;
+            start = semicolonStart;
           }
-          let currentLine = start?.row;
+
+          // Determine the starting line
+          let currentLine = start?.line;
           if (
-            !currentLine ||
-            currentLine > cursorPosition.row ||
-            (currentLine === cursorPosition.row &&
+            currentLine === undefined ||
+            currentLine > cursorPosition.line ||
+            (currentLine === cursorPosition.line &&
               (start?.column || 0) > cursorPosition.column)
           ) {
             currentLine = 0;
           }
+
+          // Skip empty lines to find actual content
           let content =
-            currentLine === start?.row
-              ? session.getLine(currentLine).slice(start.column).trim()
-              : session.getLine(currentLine).trim();
-          while (!content && currentLine < totalLine) {
+            currentLine === start?.line && start
+              ? lines[currentLine].slice(start.column).trim()
+              : (lines[currentLine]?.trim() ?? '');
+          while (!content && currentLine < totalLines - 1) {
             currentLine += 1;
-            content = session.getLine(currentLine).trim();
+            content = lines[currentLine]?.trim() ?? '';
           }
-          if (currentLine !== start?.row) {
-            start = { row: currentLine, column: 0 };
+
+          // Adjust start if we skipped lines
+          if (start === undefined || currentLine !== start.line) {
+            start = { line: currentLine, column: 0 };
           }
-          editor.selection.setSelectionRange({
-            start: start ?? { row: 0, column: 0 },
+
+          // Set selection and run query
+          editor.setSelection({
+            start: start ?? { line: 0, column: 0 },
             end,
           });
           startQuery();
-          editor.selection.clearSelection();
+
+          // Clear selection and restore cursor position
+          editor.setSelection({
+            start: cursorPosition,
+            end: cursorPosition,
+          });
           editor.moveCursorToPosition(cursorPosition);
-          editor.scrollToRow(currentRow);
+          editor.scrollToLine(cursorPosition.line);
         },
       },
     ];
@@ -504,8 +567,14 @@ const SqlEditor: FC<Props> = ({
         name: 'previousLine',
         key: KeyboardShortcut.CtrlP,
         descr: KEY_MAP[KeyboardShortcut.CtrlP],
-        func: editor => {
-          editor.navigateUp();
+        func: (editor: EditorHandle) => {
+          const pos = editor.getCursorPosition();
+          if (pos.line > 0) {
+            editor.moveCursorToPosition({
+              line: pos.line - 1,
+              column: pos.column,
+            });
+          }
         },
       });
     }
@@ -903,7 +972,7 @@ const SqlEditor: FC<Props> = ({
             <AutoSizer disableWidth>
               {({ height }) =>
                 isActive && (
-                  <AceEditorWrapper
+                  <EditorWrapper
                     autocomplete={autocompleteEnabled}
                     onBlur={onSqlChanged}
                     onChange={onSqlChanged}
diff --git a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
index e5b9f214977..9be2bbae66c 100644
--- a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
+++ b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
@@ -38,8 +38,8 @@ jest.mock('@superset-ui/core/components/Select/Select', () => () => (
 jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => () => (
   <div data-test="mock-async-select" />
 ));
-jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({
-  ConfigEditor: ({ value }: { value: string }) => (
+jest.mock('src/core/editors', () => ({
+  EditorHost: ({ value }: { value: string }) => (
     <div data-test="mock-async-ace-editor">{value}</div>
   ),
 }));
diff --git a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
index 402097d0a77..9b27e45427b 100644
--- a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
@@ -22,15 +22,15 @@ import { styled } from '@apache-superset/core/ui';
 import { debounce } from 'lodash';
 import {
   Badge,
-  ConfigEditor,
   InfoTooltip,
   ModalTrigger,
   Tooltip,
   Constants,
 } from '@superset-ui/core/components';
+import { EditorHost } from 'src/core/editors';
 import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
 
-const StyledConfigEditor = styled(ConfigEditor)`
+const StyledEditorHost = styled(EditorHost)`
   &.ace_editor {
     border: 1px solid ${({ theme }) => theme.colorBorder};
   }
@@ -87,14 +87,12 @@ const TemplateParamsEditor = ({
         </a>{' '}
         {t('syntax.')}
       </StyledParagraph>
-      <StyledConfigEditor
-        mode={language}
-        minLines={25}
-        maxLines={50}
+      <StyledEditorHost
+        id={`template-params-${queryEditorId}`}
+        height="800px"
         onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
+        language={language === 'yaml' ? 'yaml' : 'json'}
         width="100%"
-        editorProps={{ $blockScrolling: true }}
-        enableLiveAutocompletion
         value={code}
       />
     </div>
diff --git a/superset-frontend/src/components/SQLEditorWithValidation/index.tsx b/superset-frontend/src/components/SQLEditorWithValidation/index.tsx
index 61e020d1bc6..de483fd90c3 100644
--- a/superset-frontend/src/components/SQLEditorWithValidation/index.tsx
+++ b/superset-frontend/src/components/SQLEditorWithValidation/index.tsx
@@ -18,34 +18,39 @@
  */
 import { useCallback, useState, useEffect, forwardRef } from 'react';
 import { t } from '@apache-superset/core';
+import type { editors } from '@apache-superset/core';
 import { SupersetClient } from '@superset-ui/core';
 import { styled } from '@apache-superset/core/ui';
-import {
-  SQLEditor,
-  Button,
-  Icons,
-  Tooltip,
-  Flex,
-} from '@superset-ui/core/components';
+import { Button, Icons, Tooltip, Flex } from '@superset-ui/core/components';
+import { EditorHost } from 'src/core/editors';
 import {
   ExpressionType,
   ValidationError,
   ValidationResponse,
 } from '../../types/SqlExpression';
 
+type EditorKeyword = editors.EditorKeyword;
+
 interface SQLEditorWithValidationProps {
-  // SQLEditor props - we'll accept any props that SQLEditor accepts
+  // Editor props
   value: string;
   onChange: (value: string) => void;
   // Validation-specific props
   showValidation?: boolean;
   expressionType?: ExpressionType;
-  datasourceId?: number;
+  datasourceId?: number | string;
   datasourceType?: string;
   clause?: string; // For filters: "WHERE" or "HAVING"
   onValidationComplete?: (isValid: boolean, errors?: ValidationError[]) => void;
-  // Any other props will be passed through to SQLEditor
-  [key: string]: any;
+  // Editor appearance props
+  height?: string;
+  width?: string;
+  /** Whether to show line numbers */
+  lineNumbers?: boolean;
+  /** Whether to enable word wrap */
+  wordWrap?: boolean;
+  /** Keywords for autocomplete */
+  keywords?: EditorKeyword[];
 }
 
 const StyledValidationMessage = styled.div<{
@@ -71,7 +76,10 @@ const StyledValidationMessage = styled.div<{
   }
 `;
 
-const SQLEditorWithValidation = forwardRef<any, SQLEditorWithValidationProps>(
+const SQLEditorWithValidation = forwardRef<
+  editors.EditorHandle,
+  SQLEditorWithValidationProps
+>(
   (
     {
       // Required props
@@ -84,8 +92,12 @@ const SQLEditorWithValidation = forwardRef<any, SQLEditorWithValidationProps>(
       datasourceType,
       clause,
       onValidationComplete,
-      // All other props will be passed through to SQLEditor
-      ...sqlEditorProps
+      // Editor appearance props
+      height,
+      width,
+      lineNumbers,
+      wordWrap,
+      keywords,
     },
     ref,
   ) => {
@@ -187,11 +199,17 @@ const SQLEditorWithValidation = forwardRef<any, SQLEditorWithValidationProps>(
 
     return (
       <Flex vertical gap="middle">
-        <SQLEditor
+        <EditorHost
           ref={ref}
+          id="sql-editor-with-validation"
           value={value}
           onChange={handleChange}
-          {...sqlEditorProps}
+          language="sql"
+          lineNumbers={lineNumbers}
+          wordWrap={wordWrap}
+          height={height}
+          width={width}
+          keywords={keywords}
         />
 
         {showValidation && (
diff --git a/superset-frontend/src/core/editors/AceEditorProvider.tsx b/superset-frontend/src/core/editors/AceEditorProvider.tsx
new file mode 100644
index 00000000000..018be92f2df
--- /dev/null
+++ b/superset-frontend/src/core/editors/AceEditorProvider.tsx
@@ -0,0 +1,325 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview Default Ace Editor provider implementation.
+ *
+ * This module wraps the existing Ace editor components to implement the
+ * standard EditorProps interface, serving as the default editor when no
+ * extension provides a custom implementation.
+ */
+
+import {
+  useRef,
+  useEffect,
+  useCallback,
+  useImperativeHandle,
+  forwardRef,
+  type Ref,
+} from 'react';
+import type AceEditor from 'react-ace';
+import type { editors } from '@apache-superset/core';
+import {
+  FullSQLEditor,
+  JsonEditor,
+  MarkdownEditor,
+  CssEditor,
+  ConfigEditor,
+  type AceCompleterKeyword,
+} from '@superset-ui/core/components';
+import { Disposable } from '../models';
+
+type EditorKeyword = editors.EditorKeyword;
+type EditorProps = editors.EditorProps;
+type EditorHandle = editors.EditorHandle;
+type Position = editors.Position;
+type Range = editors.Range;
+type Selection = editors.Selection;
+type EditorAnnotation = editors.EditorAnnotation;
+type CompletionProvider = editors.CompletionProvider;
+
+/**
+ * Maps EditorLanguage to the corresponding Ace editor component.
+ */
+const getEditorComponent = (language: string) => {
+  switch (language) {
+    case 'sql':
+      return FullSQLEditor;
+    case 'json':
+      return JsonEditor;
+    case 'markdown':
+      return MarkdownEditor;
+    case 'css':
+      return CssEditor;
+    case 'yaml':
+      return ConfigEditor;
+    default:
+      console.warn(
+        `Unknown editor language "${language}", falling back to SQL editor`,
+      );
+      return FullSQLEditor;
+  }
+};
+
+/**
+ * Converts EditorAnnotation to Ace annotation format.
+ */
+const toAceAnnotation = (annotation: EditorAnnotation) => ({
+  row: annotation.line,
+  column: annotation.column ?? 0,
+  text: annotation.message,
+  type: annotation.severity,
+});
+
+/**
+ * Creates an EditorHandle implementation backed by an Ace editor instance.
+ */
+const createAceEditorHandle = (
+  aceEditorRef: React.RefObject<AceEditor>,
+  completionProviders: React.MutableRefObject<Map<string, CompletionProvider>>,
+): EditorHandle => ({
+  focus: () => {
+    aceEditorRef.current?.editor?.focus();
+  },
+
+  getValue: () => aceEditorRef.current?.editor?.getValue() ?? '',
+
+  setValue: (value: string) => {
+    aceEditorRef.current?.editor?.setValue(value, -1);
+  },
+
+  getCursorPosition: (): Position => {
+    const pos = aceEditorRef.current?.editor?.getCursorPosition();
+    return {
+      line: pos?.row ?? 0,
+      column: pos?.column ?? 0,
+    };
+  },
+
+  moveCursorToPosition: (position: Position) => {
+    aceEditorRef.current?.editor?.moveCursorToPosition({
+      row: position.line,
+      column: position.column,
+    });
+  },
+
+  getSelections: (): Selection[] => {
+    const selection = aceEditorRef.current?.editor?.getSelection();
+    if (!selection) return [];
+    const range = selection.getRange();
+    return [
+      {
+        start: { line: range.start.row, column: range.start.column },
+        end: { line: range.end.row, column: range.end.column },
+      },
+    ];
+  },
+
+  setSelection: (range: Range) => {
+    const editor = aceEditorRef.current?.editor;
+    if (editor) {
+      editor.selection.setSelectionRange({
+        start: { row: range.start.line, column: range.start.column },
+        end: { row: range.end.line, column: range.end.column },
+      });
+    }
+  },
+
+  getSelectedText: () => aceEditorRef.current?.editor?.getSelectedText() ?? '',
+
+  insertText: (text: string) => {
+    aceEditorRef.current?.editor?.insert(text);
+  },
+
+  executeCommand: (commandName: string) => {
+    const editor = aceEditorRef.current?.editor;
+    if (editor?.commands) {
+      editor.commands.exec(commandName, editor, {});
+    }
+  },
+
+  scrollToLine: (line: number) => {
+    aceEditorRef.current?.editor?.scrollToLine(line, true, true);
+  },
+
+  setAnnotations: (annotations: EditorAnnotation[]) => {
+    const session = aceEditorRef.current?.editor?.getSession();
+    if (session) {
+      session.setAnnotations(annotations.map(toAceAnnotation));
+    }
+  },
+
+  clearAnnotations: () => {
+    const session = aceEditorRef.current?.editor?.getSession();
+    if (session) {
+      session.clearAnnotations();
+    }
+  },
+
+  registerCompletionProvider: (provider: CompletionProvider): Disposable => {
+    completionProviders.current.set(provider.id, provider);
+    return new Disposable(() => {
+      completionProviders.current.delete(provider.id);
+    });
+  },
+});
+
+/**
+ * Converts generic EditorKeyword to Ace's AceCompleterKeyword format.
+ */
+const toAceKeyword = (keyword: EditorKeyword): AceCompleterKeyword => ({
+  name: keyword.name,
+  value: keyword.value ?? keyword.name,
+  score: keyword.score ?? 0,
+  meta: keyword.meta ?? '',
+});
+
+/**
+ * Default Ace-based editor provider component.
+ * Implements the standard EditorProps interface while wrapping the existing
+ * Ace editor components.
+ */
+const AceEditorProvider = forwardRef<EditorHandle, EditorProps>(
+  (props, ref) => {
+    const {
+      id,
+      value,
+      onChange,
+      onBlur,
+      onCursorPositionChange,
+      onSelectionChange,
+      language,
+      readOnly,
+      tabSize,
+      lineNumbers,
+      wordWrap,
+      annotations,
+      hotkeys,
+      keywords,
+      height = '100%',
+      width = '100%',
+      onReady,
+    } = props;
+
+    const aceEditorRef = useRef<AceEditor>(null);
+    const completionProviders = useRef<Map<string, CompletionProvider>>(
+      new Map(),
+    );
+
+    // Create the handle
+    const handle = createAceEditorHandle(aceEditorRef, completionProviders);
+
+    // Expose handle via ref
+    useImperativeHandle(ref, () => handle, []);
+
+    // Notify when ready
+    useEffect(() => {
+      if (onReady && aceEditorRef.current?.editor) {
+        onReady(handle);
+      }
+    }, [onReady, handle]);
+
+    // Handle editor load
+    const onEditorLoad = useCallback(
+      (editor: AceEditor['editor']) => {
+        // Register hotkeys
+        if (hotkeys) {
+          hotkeys.forEach(hotkey => {
+            editor.commands.addCommand({
+              name: hotkey.name,
+              bindKey: { win: hotkey.key, mac: hotkey.key },
+              exec: () => hotkey.exec(handle),
+            });
+          });
+        }
+
+        // Set up cursor position change listener
+        if (onCursorPositionChange) {
+          editor.selection.on('changeCursor', () => {
+            const cursor = editor.getCursorPosition();
+            onCursorPositionChange({
+              line: cursor.row,
+              column: cursor.column,
+            });
+          });
+        }
+
+        // Set up selection change listener
+        if (onSelectionChange) {
+          editor.selection.on('changeSelection', () => {
+            const range = editor.getSelection().getRange();
+            onSelectionChange([
+              {
+                start: { line: range.start.row, column: range.start.column },
+                end: { line: range.end.row, column: range.end.column },
+              },
+            ]);
+          });
+        }
+
+        // Focus the editor
+        editor.focus();
+      },
+      [hotkeys, onCursorPositionChange, onSelectionChange, handle],
+    );
+
+    // Handle blur
+    const handleBlur = useCallback(() => {
+      if (onBlur) {
+        onBlur(value);
+      }
+    }, [onBlur, value]);
+
+    // Get the appropriate editor component
+    const EditorComponent = getEditorComponent(language);
+
+    // Convert annotations to Ace format
+    const aceAnnotations = annotations?.map(toAceAnnotation);
+
+    // Convert generic keywords to Ace format
+    const aceKeywords = keywords?.map(toAceKeyword);
+
+    return (
+      <EditorComponent
+        ref={aceEditorRef as Ref<never>}
+        name={id}
+        mode={language}
+        value={value}
+        onChange={onChange}
+        onBlur={handleBlur}
+        onLoad={onEditorLoad}
+        height={height}
+        width={width}
+        readOnly={readOnly}
+        tabSize={tabSize}
+        showGutter={lineNumbers !== false}
+        wrapEnabled={wordWrap}
+        annotations={aceAnnotations}
+        keywords={aceKeywords}
+        enableLiveAutocompletion
+        editorProps={{ $blockScrolling: true }}
+        showLoadingForImport
+      />
+    );
+  },
+);
+
+AceEditorProvider.displayName = 'AceEditorProvider';
+
+export default AceEditorProvider;
diff --git a/superset-frontend/src/core/editors/EditorHost.test.tsx b/superset-frontend/src/core/editors/EditorHost.test.tsx
new file mode 100644
index 00000000000..195f5e44d45
--- /dev/null
+++ b/superset-frontend/src/core/editors/EditorHost.test.tsx
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render, screen, cleanup } from 'spec/helpers/testing-library';
+import type { editors } from '@apache-superset/core';
+
+afterEach(() => {
+  cleanup();
+});
+
+type EditorProps = editors.EditorProps;
+
+// Mock the AceEditorProvider to avoid loading the full Ace editor in tests
+jest.mock('./AceEditorProvider', () => ({
+  __esModule: true,
+  default: ({ id, value, language }: EditorProps) => (
+    <div data-test="ace-editor-provider">
+      <span data-test="ace-editor-id">{id}</span>
+      <span data-test="ace-editor-value">{value}</span>
+      <span data-test="ace-editor-language">{language}</span>
+    </div>
+  ),
+}));
+
+// Mock the EditorProviders - return undefined (no extension provider)
+jest.mock('./EditorProviders', () => ({
+  __esModule: true,
+  default: {
+    getInstance: () => ({
+      getProvider: jest.fn().mockReturnValue(undefined),
+      hasProvider: jest.fn().mockReturnValue(false),
+      onDidRegister: jest.fn().mockReturnValue({ dispose: jest.fn() }),
+      onDidUnregister: jest.fn().mockReturnValue({ dispose: jest.fn() }),
+    }),
+  },
+}));
+
+// Import EditorHost after mocks are set up
+import EditorHost from './EditorHost';
+
+const defaultProps: EditorProps = {
+  id: 'test-editor',
+  value: 'SELECT * FROM table',
+  onChange: jest.fn(),
+  language: 'sql',
+};
+
+test('renders default Ace editor when no extension provider is registered', () => {
+  render(<EditorHost {...defaultProps} />);
+
+  expect(screen.getByTestId('ace-editor-provider')).toBeInTheDocument();
+  expect(screen.getByTestId('ace-editor-id')).toHaveTextContent('test-editor');
+  expect(screen.getByTestId('ace-editor-value')).toHaveTextContent(
+    'SELECT * FROM table',
+  );
+  expect(screen.getByTestId('ace-editor-language')).toHaveTextContent('sql');
+});
+
+test('passes id prop to the editor', () => {
+  render(<EditorHost {...defaultProps} id="custom-id" />);
+
+  expect(screen.getByTestId('ace-editor-id')).toHaveTextContent('custom-id');
+});
+
+test('passes value prop to the editor', () => {
+  render(<EditorHost {...defaultProps} value="SELECT 1" />);
+
+  expect(screen.getByTestId('ace-editor-value')).toHaveTextContent('SELECT 1');
+});
+
+test('passes language option to the editor', () => {
+  render(<EditorHost {...defaultProps} language="json" />);
+
+  expect(screen.getByTestId('ace-editor-language')).toHaveTextContent('json');
+});
diff --git a/superset-frontend/src/core/editors/EditorHost.tsx b/superset-frontend/src/core/editors/EditorHost.tsx
new file mode 100644
index 00000000000..a6caa4743b1
--- /dev/null
+++ b/superset-frontend/src/core/editors/EditorHost.tsx
@@ -0,0 +1,128 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview EditorHost component for dynamic editor resolution.
+ *
+ * This component resolves and renders the appropriate editor implementation
+ * based on the language and any registered extension providers. If an extension
+ * has registered an editor for the language, it uses that; otherwise, it falls
+ * back to the default Ace editor.
+ */
+
+import { useState, useEffect, forwardRef } from 'react';
+import type { editors, contributions } from '@apache-superset/core';
+import { useTheme } from '@apache-superset/core/ui';
+import EditorProviders from './EditorProviders';
+import AceEditorProvider from './AceEditorProvider';
+
+type EditorLanguage = contributions.EditorLanguage;
+type EditorProps = editors.EditorProps;
+type EditorHandle = editors.EditorHandle;
+
+/**
+ * Props for EditorHost component.
+ * Uses the generic EditorProps interface that all editor implementations support.
+ */
+export type EditorHostProps = EditorProps;
+
+/**
+ * Hook to track editor provider changes.
+ * Returns the provider for the specified language and re-renders when it changes.
+ */
+const useEditorProvider = (language: EditorLanguage) => {
+  const manager = EditorProviders.getInstance();
+  const [provider, setProvider] = useState(() => manager.getProvider(language));
+
+  useEffect(() => {
+    // Helper to safely update provider state, always fetching latest from manager
+    const updateProvider = () => {
+      setProvider(prev => {
+        const current = manager.getProvider(language);
+        return current !== prev ? current : prev;
+      });
+    };
+
+    // Subscribe to provider changes
+    const registerDisposable = manager.onDidRegister(event => {
+      if (event.provider.contribution.languages.includes(language)) {
+        updateProvider();
+      }
+    });
+
+    const unregisterDisposable = manager.onDidUnregister(event => {
+      if (event.contribution.languages.includes(language)) {
+        updateProvider();
+      }
+    });
+
+    // Check for provider on mount (in case it was registered before this component mounted)
+    updateProvider();
+
+    return () => {
+      registerDisposable.dispose();
+      unregisterDisposable.dispose();
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [language, manager]);
+
+  return provider;
+};
+
+/**
+ * EditorHost component that dynamically resolves and renders the appropriate editor.
+ *
+ * This component serves as the main entry point for rendering editors in Superset.
+ * It checks if an extension has registered a custom editor for the requested language
+ * and uses that if available; otherwise, it falls back to the default Ace editor.
+ *
+ * @example
+ * ```tsx
+ * <EditorHost
+ *   id="sql-editor-1"
+ *   value={sql}
+ *   onChange={setSql}
+ *   language="sql"
+ *   height="400px"
+ * />
+ * ```
+ */
+const EditorHost = forwardRef<EditorHandle, EditorHostProps>((props, ref) => {
+  const { language } = props;
+  const theme = useTheme();
+  const provider = useEditorProvider(language);
+
+  // Merge theme into props
+  const propsWithTheme = { ...props, theme };
+
+  // Use extension-provided editor if available
+  if (provider) {
+    const EditorComponent = provider.component;
+    return <EditorComponent ref={ref} {...propsWithTheme} />;
+  }
+
+  // Fall back to default Ace editor
+  return <AceEditorProvider ref={ref} {...propsWithTheme} />;
+});
+
+EditorHost.displayName = 'EditorHost';
+
+export default EditorHost;
+
+export { EditorHost };
diff --git a/superset-frontend/src/core/editors/EditorProviders.test.ts b/superset-frontend/src/core/editors/EditorProviders.test.ts
new file mode 100644
index 00000000000..74abb6088cf
--- /dev/null
+++ b/superset-frontend/src/core/editors/EditorProviders.test.ts
@@ -0,0 +1,339 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { editors, contributions } from '@apache-superset/core';
+import EditorProviders from './EditorProviders';
+
+type EditorLanguage = contributions.EditorLanguage;
+type EditorContribution = editors.EditorContribution;
+type EditorComponent = editors.EditorComponent;
+
+/**
+ * Creates a mock editor contribution for testing.
+ */
+function createMockEditorContribution(
+  overrides: Partial<EditorContribution> = {},
+): EditorContribution {
+  return {
+    id: 'test.mock-editor',
+    name: 'Mock Editor',
+    languages: ['sql'] as EditorLanguage[],
+    description: 'A mock editor for testing',
+    ...overrides,
+  };
+}
+
+/**
+ * Creates a mock editor component for testing.
+ */
+function createMockEditorComponent(): EditorComponent {
+  return jest.fn(() => null) as unknown as EditorComponent;
+}
+
+beforeEach(() => {
+  // Reset the singleton instance before each test
+  const manager = EditorProviders.getInstance();
+  manager.reset();
+});
+
+test('creates singleton instance', () => {
+  const manager1 = EditorProviders.getInstance();
+  const manager2 = EditorProviders.getInstance();
+
+  expect(manager1).toBe(manager2);
+  expect(manager1).toBeInstanceOf(EditorProviders);
+});
+
+test('registers and retrieves a provider', () => {
+  const manager = EditorProviders.getInstance();
+  const contribution = createMockEditorContribution();
+  const component = createMockEditorComponent();
+
+  manager.registerProvider(contribution, component);
+
+  const provider = manager.getProvider('sql');
+  expect(provider).toBeDefined();
+  expect(provider?.contribution).toEqual(contribution);
+  expect(provider?.component).toBe(component);
+});
+
+test('hasProvider returns true when provider is registered', () => {
+  const manager = EditorProviders.getInstance();
+  const contribution = createMockEditorContribution();
+  const component = createMockEditorComponent();
+
+  expect(manager.hasProvider('sql')).toBe(false);
+
+  manager.registerProvider(contribution, component);
+
+  expect(manager.hasProvider('sql')).toBe(true);
+});
+
+test('hasProvider returns false for unregistered languages', () => {
+  const manager = EditorProviders.getInstance();
+  const contribution = createMockEditorContribution({
+    languages: ['sql'],
+  });
+  const component = createMockEditorComponent();
+
+  manager.registerProvider(contribution, component);
+
+  expect(manager.hasProvider('sql')).toBe(true);
+  expect(manager.hasProvider('json')).toBe(false);
+  expect(manager.hasProvider('markdown')).toBe(false);
+});
+
+test('returns undefined for unregistered language', () => {
+  const manager = EditorProviders.getInstance();
+
+  const provider = manager.getProvider('sql');
+  expect(provider).toBeUndefined();
+});
+
+test('getAllProviders returns all registered providers', () => {
+  const manager = EditorProviders.getInstance();
+
+  expect(manager.getAllProviders()).toHaveLength(0);
+
+  const contribution1 = createMockEditorContribution({
+    id: 'editor-1',
+    languages: ['sql'],
+  });
+  const contribution2 = createMockEditorContribution({
+    id: 'editor-2',
+    languages: ['json'],
+  });
+
+  manager.registerProvider(contribution1, createMockEditorComponent());
+  manager.registerProvider(contribution2, createMockEditorComponent());
+
+  const providers = manager.getAllProviders();
+  expect(providers).toHaveLength(2);
+});
+
+test('unregisters provider when disposable is disposed', () => {
+  const manager = EditorProviders.getInstance();
+  const contribution = createMockEditorContribution();
+  const component = createMockEditorComponent();
+
+  const disposable = manager.registerProvider(contribution, component);
+
+  expect(manager.hasProvider('sql')).toBe(true);
+
+  disposable.dispose();
+
+  expect(manager.hasProvider('sql')).toBe(false);
+  expect(manager.getProvider('sql')).toBeUndefined();
+});
+
+test('supports multiple languages per provider', () => {
+  const manager = EditorProviders.getInstance();
+  const contribution = createMockEditorContribution({
+    languages: ['sql', 'json', 'yaml'],
+  });
+  const component = createMockEditorComponent();
+
+  manager.registerProvider(contribution, component);
+
+  expect(manager.hasProvider('sql')).toBe(true);
+  expect(manager.hasProvider('json')).toBe(true);
+  expect(manager.hasProvider('yaml')).toBe(true);
+  expect(manager.hasProvider('markdown')).toBe(false);
+
+  // All should return the same provider
+  const sqlProvider = manager.getProvider('sql');
+  const jsonProvider = manager.getProvider('json');
+  const yamlProvider = manager.getProvider('yaml');
+
+  expect(sqlProvider).toBe(jsonProvider);
+  expect(jsonProvider).toBe(yamlProvider);
+});
+
+test('warns when registering duplicate provider id', () => {
+  const manager = EditorProviders.getInstance();
+  const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
+
+  const contribution = createMockEditorContribution({
+    id: 'duplicate-editor',
+  });
+
+  manager.registerProvider(contribution, createMockEditorComponent());
+
+  // Try to register with same ID
+  const disposable = manager.registerProvider(
+    { ...contribution },
+    createMockEditorComponent(),
+  );
+
+  expect(consoleWarnSpy).toHaveBeenCalledWith(
+    'Editor provider with id "duplicate-editor" is already registered.',
+  );
+
+  // Disposing the duplicate should be a no-op
+  disposable.dispose();
+
+  // Original provider should still be registered
+  expect(manager.hasProvider('sql')).toBe(true);
+
+  consoleWarnSpy.mockRestore();
+});
+
+test('fires onDidRegister event when provider is registered', () => {
+  const manager = EditorProviders.getInstance();
+  const listener = jest.fn();
+
+  manager.onDidRegister(listener);
+
+  const contribution = createMockEditorContribution();
+  const component = createMockEditorComponent();
+
+  manager.registerProvider(contribution, component);
+
+  expect(listener).toHaveBeenCalledTimes(1);
+  expect(listener).toHaveBeenCalledWith({
+    provider: {
+      contribution,
+      component,
+    },
+  });
+});
+
+test('fires onDidUnregister event when provider is unregistered', () => {
+  const manager = EditorProviders.getInstance();
+  const listener = jest.fn();
+
+  manager.onDidUnregister(listener);
+
+  const contribution = createMockEditorContribution();
+  const disposable = manager.registerProvider(
+    contribution,
+    createMockEditorComponent(),
+  );
+
+  disposable.dispose();
+
+  expect(listener).toHaveBeenCalledTimes(1);
+  expect(listener).toHaveBeenCalledWith({
+    contribution,
+  });
+});
+
+test('event listeners can be disposed', () => {
+  const manager = EditorProviders.getInstance();
+  const listener = jest.fn();
+
+  const listenerDisposable = manager.onDidRegister(listener);
+
+  // Register first provider - listener should be called
+  manager.registerProvider(
+    createMockEditorContribution({ id: 'editor-1' }),
+    createMockEditorComponent(),
+  );
+
+  expect(listener).toHaveBeenCalledTimes(1);
+
+  // Dispose the listener
+  listenerDisposable.dispose();
+
+  // Register second provider - listener should not be called
+  manager.registerProvider(
+    createMockEditorContribution({ id: 'editor-2', languages: ['json'] }),
+    createMockEditorComponent(),
+  );
+
+  expect(listener).toHaveBeenCalledTimes(1); // Still only 1 call
+});
+
+test('handles errors in event listeners gracefully', () => {
+  const manager = EditorProviders.getInstance();
+  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
+
+  const errorListener = jest.fn(() => {
+    throw new Error('Listener error');
+  });
+  const successListener = jest.fn();
+
+  manager.onDidRegister(errorListener);
+  manager.onDidRegister(successListener);
+
+  manager.registerProvider(
+    createMockEditorContribution(),
+    createMockEditorComponent(),
+  );
+
+  // Both listeners should have been called
+  expect(errorListener).toHaveBeenCalledTimes(1);
+  expect(successListener).toHaveBeenCalledTimes(1);
+
+  // Error should have been logged
+  expect(consoleErrorSpy).toHaveBeenCalledWith(
+    'Error in event listener:',
+    expect.any(Error),
+  );
+
+  consoleErrorSpy.mockRestore();
+});
+
+test('reset clears all providers and language mappings', () => {
+  const manager = EditorProviders.getInstance();
+
+  manager.registerProvider(
+    createMockEditorContribution({ id: 'editor-1', languages: ['sql'] }),
+    createMockEditorComponent(),
+  );
+  manager.registerProvider(
+    createMockEditorContribution({ id: 'editor-2', languages: ['json'] }),
+    createMockEditorComponent(),
+  );
+
+  expect(manager.getAllProviders()).toHaveLength(2);
+  expect(manager.hasProvider('sql')).toBe(true);
+  expect(manager.hasProvider('json')).toBe(true);
+
+  manager.reset();
+
+  expect(manager.getAllProviders()).toHaveLength(0);
+  expect(manager.hasProvider('sql')).toBe(false);
+  expect(manager.hasProvider('json')).toBe(false);
+});
+
+test('later registration replaces language mapping', () => {
+  const manager = EditorProviders.getInstance();
+
+  const contribution1 = createMockEditorContribution({
+    id: 'editor-1',
+    name: 'Editor 1',
+    languages: ['sql'],
+  });
+  const contribution2 = createMockEditorContribution({
+    id: 'editor-2',
+    name: 'Editor 2',
+    languages: ['sql'],
+  });
+
+  manager.registerProvider(contribution1, createMockEditorComponent());
+  manager.registerProvider(contribution2, createMockEditorComponent());
+
+  // The second registration should replace the first for the 'sql' language
+  const provider = manager.getProvider('sql');
+  expect(provider?.contribution.id).toBe('editor-2');
+  expect(provider?.contribution.name).toBe('Editor 2');
+
+  // But both providers should exist
+  expect(manager.getAllProviders()).toHaveLength(2);
+});
diff --git a/superset-frontend/src/core/editors/EditorProviders.ts b/superset-frontend/src/core/editors/EditorProviders.ts
new file mode 100644
index 00000000000..e9390fd99c1
--- /dev/null
+++ b/superset-frontend/src/core/editors/EditorProviders.ts
@@ -0,0 +1,243 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { editors, contributions } from '@apache-superset/core';
+import { Disposable } from '../models';
+
+type EditorLanguage = contributions.EditorLanguage;
+type EditorProvider = editors.EditorProvider;
+type EditorContribution = editors.EditorContribution;
+type EditorComponent = editors.EditorComponent;
+type EditorProviderRegisteredEvent = editors.EditorProviderRegisteredEvent;
+type EditorProviderUnregisteredEvent = editors.EditorProviderUnregisteredEvent;
+
+/**
+ * Listener function type for events.
+ */
+type Listener<T> = (e: T) => void;
+
+/**
+ * Simple event emitter for editor provider lifecycle events.
+ */
+class EventEmitter<T> {
+  private listeners: Set<Listener<T>> = new Set();
+
+  /**
+   * Subscribe to this event.
+   * @param listener The listener function to call when the event is fired.
+   * @returns A Disposable to unsubscribe from the event.
+   */
+  subscribe(listener: Listener<T>): Disposable {
+    this.listeners.add(listener);
+    return new Disposable(() => {
+      this.listeners.delete(listener);
+    });
+  }
+
+  /**
+   * Fire the event with the given data.
+   * @param data The event data to pass to listeners.
+   */
+  fire(data: T): void {
+    this.listeners.forEach(listener => {
+      try {
+        listener(data);
+      } catch (error) {
+        // eslint-disable-next-line no-console
+        console.error('Error in event listener:', error);
+      }
+    });
+  }
+}
+
+/**
+ * Singleton manager for editor providers.
+ * Handles registration, resolution, and lifecycle of custom editor implementations.
+ */
+class EditorProviders {
+  private static instance: EditorProviders;
+
+  /**
+   * Map of provider ID to EditorProvider.
+   */
+  private providers: Map<string, EditorProvider> = new Map();
+
+  /**
+   * Map of language to provider ID for quick lookups.
+   */
+  private languageToProvider: Map<EditorLanguage, string> = new Map();
+
+  /**
+   * Event emitter for provider registration events.
+   */
+  private registerEmitter = new EventEmitter<EditorProviderRegisteredEvent>();
+
+  /**
+   * Event emitter for provider unregistration events.
+   */
+  private unregisterEmitter =
+    new EventEmitter<EditorProviderUnregisteredEvent>();
+
+  // eslint-disable-next-line no-useless-constructor
+  private constructor() {
+    // Private constructor for singleton pattern
+  }
+
+  /**
+   * Get the singleton instance of EditorProviders.
+   * @returns The singleton instance.
+   */
+  public static getInstance(): EditorProviders {
+    if (!EditorProviders.instance) {
+      EditorProviders.instance = new EditorProviders();
+    }
+    return EditorProviders.instance;
+  }
+
+  /**
+   * Register an editor provider.
+   * When registered, the provider replaces the default editor for its supported languages.
+   *
+   * @param contribution The editor contribution metadata.
+   * @param component The React component implementing the editor.
+   * @returns A Disposable to unregister the provider.
+   */
+  public registerProvider(
+    contribution: EditorContribution,
+    component: EditorComponent,
+  ): Disposable {
+    const { id, languages } = contribution;
+
+    // Check if provider with this ID already exists
+    if (this.providers.has(id)) {
+      // eslint-disable-next-line no-console
+      console.warn(`Editor provider with id "${id}" is already registered.`);
+      return new Disposable(() => {});
+    }
+
+    const provider: EditorProvider = {
+      contribution,
+      component,
+    };
+
+    // Register the provider
+    this.providers.set(id, provider);
+
+    // Map languages to this provider
+    languages.forEach(language => {
+      this.languageToProvider.set(language, id);
+    });
+
+    // Fire registration event
+    this.registerEmitter.fire({ provider });
+
+    // Return disposable for cleanup
+    return new Disposable(() => {
+      this.unregisterProvider(id);
+    });
+  }
+
+  /**
+   * Unregister an editor provider by ID.
+   * @param id The provider ID to unregister.
+   */
+  private unregisterProvider(id: string): void {
+    const provider = this.providers.get(id);
+    if (!provider) {
+      return;
+    }
+
+    const { contribution } = provider;
+
+    // Remove language mappings for this provider
+    contribution.languages.forEach(language => {
+      if (this.languageToProvider.get(language) === id) {
+        this.languageToProvider.delete(language);
+      }
+    });
+
+    // Remove the provider
+    this.providers.delete(id);
+
+    // Fire unregistration event
+    this.unregisterEmitter.fire({ contribution });
+  }
+
+  /**
+   * Get the editor provider for a specific language.
+   * @param language The language to get a provider for.
+   * @returns The provider or undefined if none is registered.
+   */
+  public getProvider(language: EditorLanguage): EditorProvider | undefined {
+    const providerId = this.languageToProvider.get(language);
+    if (!providerId) {
+      return undefined;
+    }
+    return this.providers.get(providerId);
+  }
+
+  /**
+   * Check if a provider is registered for a language.
+   * @param language The language to check.
+   * @returns True if a provider is registered.
+   */
+  public hasProvider(language: EditorLanguage): boolean {
+    return this.languageToProvider.has(language);
+  }
+
+  /**
+   * Get all registered providers.
+   * @returns Array of all registered providers.
+   */
+  public getAllProviders(): EditorProvider[] {
+    return Array.from(this.providers.values());
+  }
+
+  /**
+   * Subscribe to provider registration events.
+   * @param listener The listener function.
+   * @returns A Disposable to unsubscribe.
+   */
+  public onDidRegister(
+    listener: Listener<EditorProviderRegisteredEvent>,
+  ): Disposable {
+    return this.registerEmitter.subscribe(listener);
+  }
+
+  /**
+   * Subscribe to provider unregistration events.
+   * @param listener The listener function.
+   * @returns A Disposable to unsubscribe.
+   */
+  public onDidUnregister(
+    listener: Listener<EditorProviderUnregisteredEvent>,
+  ): Disposable {
+    return this.unregisterEmitter.subscribe(listener);
+  }
+
+  /**
+   * Reset the manager state (for testing purposes).
+   */
+  public reset(): void {
+    this.providers.clear();
+    this.languageToProvider.clear();
+  }
+}
+
+export default EditorProviders;
diff --git a/superset-frontend/src/core/editors/index.ts b/superset-frontend/src/core/editors/index.ts
new file mode 100644
index 00000000000..26e6e63e811
--- /dev/null
+++ b/superset-frontend/src/core/editors/index.ts
@@ -0,0 +1,130 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview Implementation of the editors API for Superset.
+ *
+ * This module provides the runtime implementation of the editor registration
+ * and resolution functions declared in the API types.
+ */
+
+import type { contributions } from '@apache-superset/core';
+import { editors as editorsApi } from '@apache-superset/core';
+import { Disposable } from '../models';
+import EditorProviders from './EditorProviders';
+
+type EditorLanguage = contributions.EditorLanguage;
+type EditorProvider = editorsApi.EditorProvider;
+type EditorContribution = editorsApi.EditorContribution;
+type EditorComponent = editorsApi.EditorComponent;
+type EditorProviderRegisteredEvent = editorsApi.EditorProviderRegisteredEvent;
+type EditorProviderUnregisteredEvent =
+  editorsApi.EditorProviderUnregisteredEvent;
+
+/**
+ * Register an editor provider for specific languages.
+ * When an extension registers an editor, it replaces the default for those languages.
+ *
+ * @param contribution The editor contribution metadata from extension.json
+ * @param component The React component implementing EditorProps
+ * @returns A Disposable to unregister the provider
+ */
+export const registerEditorProvider = (
+  contribution: EditorContribution,
+  component: EditorComponent,
+): Disposable => {
+  const manager = EditorProviders.getInstance();
+  return manager.registerProvider(contribution, component);
+};
+
+/**
+ * Get the editor provider for a specific language.
+ * Returns the extension's editor if registered, otherwise undefined.
+ *
+ * @param language The language to get an editor for
+ * @returns The editor provider or undefined if no extension provides one
+ */
+export const getEditorProvider = (
+  language: EditorLanguage,
+): EditorProvider | undefined => {
+  const manager = EditorProviders.getInstance();
+  return manager.getProvider(language);
+};
+
+/**
+ * Check if an extension has registered an editor for a language.
+ *
+ * @param language The language to check
+ * @returns True if an extension provides an editor for this language
+ */
+export const hasEditorProvider = (language: EditorLanguage): boolean => {
+  const manager = EditorProviders.getInstance();
+  return manager.hasProvider(language);
+};
+
+/**
+ * Get all registered editor providers.
+ *
+ * @returns Array of all registered editor providers
+ */
+export const getAllEditorProviders = (): EditorProvider[] => {
+  const manager = EditorProviders.getInstance();
+  return manager.getAllProviders();
+};
+
+/**
+ * Event fired when an editor provider is registered.
+ * Subscribe to this event to react when extensions register new editors.
+ */
+export const onDidRegisterEditorProvider = (
+  listener: (e: EditorProviderRegisteredEvent) => void,
+): Disposable => {
+  const manager = EditorProviders.getInstance();
+  return manager.onDidRegister(listener);
+};
+
+/**
+ * Event fired when an editor provider is unregistered.
+ * Subscribe to this event to react when extensions unregister editors.
+ */
+export const onDidUnregisterEditorProvider = (
+  listener: (e: EditorProviderUnregisteredEvent) => void,
+): Disposable => {
+  const manager = EditorProviders.getInstance();
+  return manager.onDidUnregister(listener);
+};
+
+/**
+ * Editors API object for use in the extension system.
+ */
+export const editors: typeof editorsApi = {
+  registerEditorProvider,
+  getEditorProvider,
+  hasEditorProvider,
+  getAllEditorProviders,
+  onDidRegisterEditorProvider,
+  onDidUnregisterEditorProvider,
+};
+
+export { EditorProviders };
+
+// Component exports
+export { default as EditorHost } from './EditorHost';
+export type { EditorHostProps } from './EditorHost';
+export { default as AceEditorProvider } from './AceEditorProvider';
diff --git a/superset-frontend/src/core/index.ts b/superset-frontend/src/core/index.ts
index 3e4d8b06f30..463beffcfda 100644
--- a/superset-frontend/src/core/index.ts
+++ b/superset-frontend/src/core/index.ts
@@ -40,6 +40,7 @@ export const core: typeof coreType = {
 
 export * from './authentication';
 export * from './commands';
+export * from './editors';
 export * from './extensions';
 export * from './models';
 export * from './sqlLab';
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index b3a458e76e5..4b4ec463f23 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -23,7 +23,6 @@ import {
   Form,
   Collapse,
   CollapseLabelInModal,
-  JsonEditor,
 } from '@superset-ui/core/components';
 import { useJsonValidation } from '@superset-ui/core/components/AsyncAceEditor';
 import { type TagType } from 'src/components';
@@ -450,8 +449,6 @@ const PropertiesModal = ({
           );
         });
     }
-
-    JsonEditor.preload();
   }, [
     currentDashboardInfo,
     fetchDashboardDetails,
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/sections/AdvancedSection.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/sections/AdvancedSection.tsx
index da1f1d990fb..9717a2de9ba 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/sections/AdvancedSection.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/sections/AdvancedSection.tsx
@@ -18,11 +18,32 @@
  */
 import { t } from '@apache-superset/core';
 import { styled } from '@apache-superset/core/ui';
-import { JsonEditor } from '@superset-ui/core/components';
+import type { editors } from '@apache-superset/core';
+import { EditorHost } from 'src/core/editors';
 import { ModalFormField } from 'src/components/Modal';
 import { ValidationObject } from 'src/components/Modal/useModalValidation';
 
-const StyledJsonEditor = styled(JsonEditor)`
+type EditorAnnotation = editors.EditorAnnotation;
+
+/**
+ * Convert Ace annotation format to EditorAnnotation format.
+ */
+const toEditorAnnotations = (
+  aceAnnotations: Array<{
+    type: string;
+    row: number;
+    column: number;
+    text: string;
+  }>,
+): EditorAnnotation[] =>
+  aceAnnotations.map(ann => ({
+    severity: ann.type as EditorAnnotation['severity'],
+    line: ann.row,
+    column: ann.column,
+    message: ann.text,
+  }));
+
+const StyledEditorHost = styled(EditorHost)`
   /* Border is already applied by AceEditor itself */
 `;
 
@@ -54,17 +75,17 @@ const AdvancedSection = ({
     }
     bottomSpacing={false}
   >
-    <StyledJsonEditor
+    <StyledEditorHost
+      id="dashboard-json-metadata"
       data-test="dashboard-metadata-editor"
-      showLoadingForImport
-      name="json_metadata"
       value={jsonMetadata}
       onChange={onJsonMetadataChange}
+      language="json"
       tabSize={2}
+      wordWrap
       width="100%"
       height="200px"
-      wrapEnabled
-      annotations={jsonAnnotations}
+      annotations={toEditorAnnotations(jsonAnnotations)}
     />
   </ModalFormField>
 );
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
index 3ed4247d6ac..1aa5e4b5412 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/sections/StylingSection.tsx
@@ -24,12 +24,13 @@ import {
   FeatureFlag,
 } from '@superset-ui/core';
 import { styled, Alert } from '@apache-superset/core/ui';
-import { CssEditor, Select, Switch } from '@superset-ui/core/components';
+import { Select, Switch } from '@superset-ui/core/components';
+import { EditorHost } from 'src/core/editors';
 import rison from 'rison';
 import ColorSchemeSelect from 'src/dashboard/components/ColorSchemeSelect';
 import { ModalFormField } from 'src/components/Modal';
 
-const StyledCssEditor = styled(CssEditor)`
+const StyledEditorHost = styled(EditorHost)`
   border-radius: ${({ theme }) => theme.borderRadius}px;
   border: 1px solid ${({ theme }) => theme.colorBorder};
 `;
@@ -254,14 +255,14 @@ const StylingSection = ({
         )}
         bottomSpacing={false}
       >
-        <StyledCssEditor
+        <StyledEditorHost
+          id="dashboard-css-editor"
           data-test="dashboard-css-editor"
           onChange={onCustomCssChange}
           value={customCss}
+          language="css"
           width="100%"
-          minLines={10}
-          maxLines={50}
-          editorProps={{ $blockScrolling: true }}
+          height="160px"
         />
       </ModalFormField>
     </>
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.jsx b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.jsx
index 3bded7c8021..6b451a3faaf 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.jsx
@@ -22,7 +22,8 @@ import { connect } from 'react-redux';
 import cx from 'classnames';
 
 import { t, css, styled } from '@apache-superset/core/ui';
-import { SafeMarkdown, MarkdownEditor } from '@superset-ui/core/components';
+import { SafeMarkdown } from '@superset-ui/core/components';
+import { EditorHost } from 'src/core/editors';
 import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
 
 import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
@@ -192,11 +193,10 @@ class Markdown extends PureComponent {
       (prevProps.component.meta.width !== this.props.component.meta.width ||
         prevProps.columnWidth !== this.props.columnWidth)
     ) {
-      this.state.editor.resize(true);
-    }
-    // pre-load AceEditor when entering edit mode
-    if (this.props.editMode) {
-      MarkdownEditor.preload();
+      // Handle both Ace editor (resize method) and EditorHandle (no resize needed)
+      if (typeof this.state.editor.resize === 'function') {
+        this.state.editor.resize(true);
+      }
     }
   }
 
@@ -211,7 +211,12 @@ class Markdown extends PureComponent {
   }
 
   setEditor(editor) {
-    editor.getSession().setUseWrapMode(true);
+    // EditorHandle or Ace editor instance
+    // For Ace: editor.getSession().setUseWrapMode(true)
+    // For EditorHandle: wrapEnabled is handled via options
+    if (editor?.getSession) {
+      editor.getSession().setUseWrapMode(true);
+    }
     this.setState({
       editor,
     });
@@ -282,20 +287,27 @@ class Markdown extends PureComponent {
 
   renderEditMode() {
     return (
-      <MarkdownEditor
+      <EditorHost
+        id={`markdown-editor-${this.props.id}`}
         onChange={this.handleMarkdownChange}
         width="100%"
         height="100%"
-        showGutter={false}
-        editorProps={{ $blockScrolling: true }}
         value={
           // this allows "select all => delete" to give an empty editor
           typeof this.state.markdownSource === 'string'
             ? this.state.markdownSource
             : MARKDOWN_PLACE_HOLDER
         }
+        language="markdown"
         readOnly={false}
-        onLoad={this.setEditor}
+        lineNumbers={false}
+        wordWrap
+        onReady={handle => {
+          // The handle provides access to the underlying editor for resize
+          if (handle && typeof handle.focus === 'function') {
+            this.setEditor(handle);
+          }
+        }}
         data-test="editor"
       />
     );
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.test.tsx
index 14fc956d88f..74183fcf239 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.test.tsx
@@ -30,6 +30,25 @@ import { mockStore } from 'spec/fixtures/mockStore';
 import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout';
 import MarkdownConnected from './Markdown';
 
+jest.mock('src/core/editors', () => ({
+  EditorHost: ({
+    value,
+    onChange,
+    onBlur,
+  }: {
+    value: string;
+    onChange?: (v: string) => void;
+    onBlur?: (v: string) => void;
+  }) => (
+    <textarea
+      role="textbox"
+      defaultValue={value}
+      onChange={e => onChange?.(e.target.value)}
+      onBlur={e => onBlur?.(e.target.value)}
+    />
+  ),
+}));
+
 interface MarkdownProps {
   id: string;
   parentId: string;
@@ -165,7 +184,7 @@ test('should call updateComponents when switching from edit to preview with chan
   const updateComponents = jest.fn();
   const mockCode = 'new markdown!';
 
-  const { container } = await setup({
+  await setup({
     editMode: true,
     updateComponents,
     component: {
@@ -185,8 +204,8 @@ test('should call updateComponents when switching from edit to preview with chan
     // Wait for editor to be fully mounted
     await new Promise(resolve => setTimeout(resolve, 50));
 
-    // Find the actual textarea/input element
-    const editor = container.querySelector('.ace_text-input');
+    // Find the actual textarea element
+    const editor = screen.getByRole('textbox');
 
     if (editor) {
       // Simulate direct input
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx
index 51fbab696ef..5eade7c64e0 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx
@@ -579,15 +579,11 @@ const ColumnSelectPopover = ({
                     ''
                   }
                   ref={sqlEditorRef}
-                  showLoadingForImport
                   onChange={onSqlExpressionChange}
                   width="100%"
                   height={`${height - 120}px`}
-                  showGutter={false}
-                  editorProps={{ $blockScrolling: true }}
-                  enableLiveAutocompletion
-                  className="filter-sql-editor"
-                  wrapEnabled
+                  lineNumbers={false}
+                  wordWrap
                   keywords={keywords.map((k: any) =>
                     typeof k === 'string' ? k : k.value || k.name || k,
                   )}
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx
index 21c588a3306..3664e16c5bd 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx
@@ -32,7 +32,6 @@ import {
   screen,
   within,
 } from 'spec/helpers/testing-library';
-import type { AsyncAceEditorProps } from '@superset-ui/core/components';
 import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
 import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
 import { Operators } from 'src/explore/constants';
@@ -46,10 +45,9 @@ import { Datasource } from '../../../types';
 import { DndItemType } from '../../DndItemType';
 import DatasourcePanelDragOption from '../../DatasourcePanel/DatasourcePanelDragOption';
 
-jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({
-  ...jest.requireActual('@superset-ui/core/components/AsyncAceEditor'),
-  SQLEditor: (props: AsyncAceEditorProps) => (
-    <div data-test="react-ace">{props.value}</div>
+jest.mock('src/core/editors', () => ({
+  EditorHost: ({ value }: { value: string }) => (
+    <div data-test="react-ace">{value}</div>
   ),
 }));
 
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx
index 4323520b8ee..420937b9703 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx
@@ -489,23 +489,13 @@ test('title changes on custom SQL text change', async () => {
     'ant-tabs-tab-active',
   );
 
-  const container = screen.getByTestId('adhoc-metric-edit-tabs');
-  await waitFor(() => {
-    const textArea = container.getElementsByClassName(
-      'ace_text-input',
-    ) as HTMLCollectionOf<HTMLTextAreaElement>;
-    expect(textArea.length).toBe(1);
-    expect(textArea[0].value).toBe('');
-  });
+  // Wait for the editor to render after tab switch
+  const textArea = (await screen.findByRole('textbox')) as HTMLTextAreaElement;
 
   expect(screen.getByTestId('AdhocMetricEditTitle#trigger')).toHaveTextContent(
     'metric_a',
   );
 
-  const textArea = container.getElementsByClassName(
-    'ace_text-input',
-  )[0] as HTMLTextAreaElement;
-
   // Changing the ACE editor via pasting, since the component
   // handles the textarea value internally, and changing it doesn't
   // trigger the onChange
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.tsx
index 3857ec1d658..2a29116e118 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.tsx
@@ -23,7 +23,6 @@ import {
   selectOption,
   userEvent,
 } from 'spec/helpers/testing-library';
-import { IAceEditorProps } from 'react-ace';
 import AdhocFilter from '../AdhocFilter';
 import { Clauses, ExpressionTypes } from '../types';
 import AdhocFilterEditPopoverSqlTabContent from '.';
@@ -35,12 +34,15 @@ afterEach(async () => {
   await new Promise(resolve => setTimeout(resolve, 0));
 });
 
-jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({
-  SQLEditor: ({ value, onChange }: IAceEditorProps) => (
-    <textarea
-      defaultValue={value}
-      onChange={value => onChange?.(value.target.value)}
-    />
+jest.mock('src/core/editors', () => ({
+  EditorHost: ({
+    value,
+    onChange,
+  }: {
+    value: string;
+    onChange: (v: string) => void;
+  }) => (
+    <textarea defaultValue={value} onChange={e => onChange?.(e.target.value)} />
   ),
 }));
 
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/index.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/index.tsx
index 01c027f6e33..37d8fbfa270 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/index.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/index.tsx
@@ -132,12 +132,9 @@ export default function AdhocFilterEditPopoverSqlTabContent({
           height={`${height - 130}px`}
           onChange={onSqlExpressionChange}
           width="100%"
-          showGutter={false}
+          lineNumbers={false}
           value={adhocFilter.sqlExpression || adhocFilter.translateToSql()}
-          editorProps={{ $blockScrolling: true }}
-          enableLiveAutocompletion
-          className="filter-sql-editor"
-          wrapEnabled
+          wordWrap
           showValidation
           expressionType={
             adhocFilter.clause === 'HAVING'
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx
index 21f303a0a4f..a43a623c5ed 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx
@@ -21,6 +21,7 @@ import { PureComponent, createRef } from 'react';
 import PropTypes from 'prop-types';
 import { isDefined, ensureIsArray, DatasourceType } from '@superset-ui/core';
 import { t } from '@apache-superset/core';
+import type { editors } from '@apache-superset/core';
 import { styled } from '@apache-superset/core/ui';
 import Tabs from '@superset-ui/core/components/Tabs';
 import {
@@ -147,7 +148,7 @@ export default class AdhocMetricEditPopover extends PureComponent<
   // "Saved" is a default tab unless there are no saved metrics for dataset
   defaultActiveTabKey = this.getDefaultTab();
 
-  aceEditorRef: RefObject<HTMLDivElement>;
+  aceEditorRef: RefObject<editors.EditorHandle>;
 
   dragStartX = 0;
 
@@ -560,21 +561,17 @@ export default class AdhocMetricEditPopover extends PureComponent<
               children: (
                 <SQLEditorWithValidation
                   data-test="sql-editor"
-                  showLoadingForImport
                   ref={this.aceEditorRef}
                   keywords={keywords}
                   height={`${this.state.height - 120}px`}
                   onChange={this.onSqlExpressionChange}
                   width="100%"
-                  showGutter={false}
+                  lineNumbers={false}
                   value={
                     adhocMetric.sqlExpression ||
                     adhocMetric.translateToSql({ transformCountDistinct: true })
                   }
-                  editorProps={{ $blockScrolling: true }}
-                  enableLiveAutocompletion
-                  className="filter-sql-editor"
-                  wrapEnabled
+                  wordWrap
                   showValidation
                   expressionType="metric"
                   datasourceId={datasource?.id}
diff --git a/superset-frontend/src/extensions/ExtensionsManager.ts b/superset-frontend/src/extensions/ExtensionsManager.ts
index 87ed3c4fa76..55fb1b5eadc 100644
--- a/superset-frontend/src/extensions/ExtensionsManager.ts
+++ b/superset-frontend/src/extensions/ExtensionsManager.ts
@@ -21,19 +21,26 @@ import { logging } from '@apache-superset/core';
 import type { contributions, core } from '@apache-superset/core';
 import { ExtensionContext } from '../core/models';
 
+type MenuContribution = contributions.MenuContribution;
+type ViewContribution = contributions.ViewContribution;
+type CommandContribution = contributions.CommandContribution;
+type EditorContribution = contributions.EditorContribution;
+type Extension = core.Extension;
+
 class ExtensionsManager {
   private static instance: ExtensionsManager;
 
-  private extensionIndex: Map<string, core.Extension> = new Map();
+  private extensionIndex: Map<string, Extension> = new Map();
 
   private contextIndex: Map<string, ExtensionContext> = new Map();
 
   private extensionContributions: Map<
     string,
     {
-      menus?: Record<string, contributions.MenuContribution>;
-      views?: Record<string, contributions.ViewContribution[]>;
-      commands?: contributions.CommandContribution[];
+      menus?: Record<string, MenuContribution>;
+      views?: Record<string, ViewContribution[]>;
+      commands?: CommandContribution[];
+      editors?: EditorContribution[];
     }
   > = new Map();
 
@@ -61,7 +68,7 @@ class ExtensionsManager {
     const response = await SupersetClient.get({
       endpoint: '/api/v1/extensions/',
     });
-    const extensions: core.Extension[] = response.json.result;
+    const extensions: Extension[] = response.json.result;
     await Promise.all(
       extensions.map(async extension => {
         await this.initializeExtension(extension);
@@ -74,7 +81,7 @@ class ExtensionsManager {
    * If the extension has a remote entry, it will load the module.
    * @param extension The extension to initialize.
    */
-  public async initializeExtension(extension: core.Extension) {
+  public async initializeExtension(extension: Extension) {
     try {
       let loadedExtension = extension;
       if (extension.remoteEntry) {
@@ -94,7 +101,7 @@ class ExtensionsManager {
    * Enables an extension by its instance.
    * @param extension The extension to enable.
    */
-  private enableExtension(extension: core.Extension): void {
+  private enableExtension(extension: Extension): void {
     const { id } = extension;
     if (extension && typeof extension.activate === 'function') {
       // If already enabled, do nothing
@@ -114,7 +121,7 @@ class ExtensionsManager {
    * @param extension The extension to load.
    * @returns The loaded extension with activate and deactivate methods.
    */
-  private async loadModule(extension: core.Extension): Promise<core.Extension> {
+  private async loadModule(extension: Extension): Promise<Extension> {
     const { remoteEntry, id, exposedModules } = extension;
 
     // Load the remote entry script
@@ -171,7 +178,7 @@ class ExtensionsManager {
    * @param context The context to pass to the activate method.
    */
   public activateExtension(
-    extension: core.Extension,
+    extension: Extension,
     context: ExtensionContext,
   ): void {
     if (extension.activate) {
@@ -213,12 +220,13 @@ class ExtensionsManager {
    * Indexes contributions from an extension for quick retrieval.
    * @param extension The extension to index.
    */
-  private indexContributions(extension: core.Extension): void {
+  private indexContributions(extension: Extension): void {
     const { contributions, id } = extension;
     this.extensionContributions.set(id, {
       menus: contributions.menus,
       views: contributions.views,
       commands: contributions.commands,
+      editors: contributions.editors,
     });
   }
 
@@ -227,10 +235,8 @@ class ExtensionsManager {
    * @param key The key of the menu contributions.
    * @returns The menu contributions matching the key, or undefined if not found.
    */
-  public getMenuContributions(
-    key: string,
-  ): contributions.MenuContribution | undefined {
-    const merged: contributions.MenuContribution = {
+  public getMenuContributions(key: string): MenuContribution | undefined {
+    const merged: MenuContribution = {
       context: [],
       primary: [],
       secondary: [],
@@ -258,10 +264,8 @@ class ExtensionsManager {
    * @param key The key of the view contributions.
    * @returns An array of view contributions matching the key, or undefined if not found.
    */
-  public getViewContributions(
-    key: string,
-  ): contributions.ViewContribution[] | undefined {
-    let result: contributions.ViewContribution[] = [];
+  public getViewContributions(key: string): ViewContribution[] | undefined {
+    let result: ViewContribution[] = [];
     for (const ext of this.extensionContributions.values()) {
       if (ext.views && ext.views[key]) {
         result = result.concat(ext.views[key]);
@@ -274,8 +278,8 @@ class ExtensionsManager {
    * Retrieves all command contributions.
    * @returns An array of all command contributions.
    */
-  public getCommandContributions(): contributions.CommandContribution[] {
-    const result: contributions.CommandContribution[] = [];
+  public getCommandContributions(): CommandContribution[] {
+    const result: CommandContribution[] = [];
     for (const ext of this.extensionContributions.values()) {
       if (ext.commands) {
         result.push(...ext.commands);
@@ -289,9 +293,7 @@ class ExtensionsManager {
    * @param key The key of the command contribution.
    * @returns The command contribution matching the key, or undefined if not found.
    */
-  public getCommandContribution(
-    key: string,
-  ): contributions.CommandContribution | undefined {
+  public getCommandContribution(key: string): CommandContribution | undefined {
     for (const ext of this.extensionContributions.values()) {
       if (ext.commands) {
         const found = ext.commands.find(cmd => cmd.command === key);
@@ -305,7 +307,7 @@ class ExtensionsManager {
    * Retrieves all extensions.
    * @returns An array of all registered extensions.
    */
-  public getExtensions(): core.Extension[] {
+  public getExtensions(): Extension[] {
     return Array.from(this.extensionIndex.values());
   }
 
@@ -314,9 +316,23 @@ class ExtensionsManager {
    * @param id The id of the extension.
    * @returns The extension matching the id, or undefined if not found.
    */
-  public getExtension(id: string): core.Extension | undefined {
+  public getExtension(id: string): Extension | undefined {
     return this.extensionIndex.get(id);
   }
+
+  /**
+   * Retrieves all editor contributions from all extensions.
+   * @returns An array of all editor contributions.
+   */
+  public getEditorContributions(): EditorContribution[] {
+    const result: EditorContribution[] = [];
+    for (const ext of this.extensionContributions.values()) {
+      if (ext.editors) {
+        result.push(...ext.editors);
+      }
+    }
+    return result;
+  }
 }
 
 export default ExtensionsManager;
diff --git a/superset-frontend/src/extensions/ExtensionsStartup.tsx b/superset-frontend/src/extensions/ExtensionsStartup.tsx
index 51710953519..b6f03eb5164 100644
--- a/superset-frontend/src/extensions/ExtensionsStartup.tsx
+++ b/superset-frontend/src/extensions/ExtensionsStartup.tsx
@@ -20,7 +20,14 @@ import { useEffect, useState } from 'react';
 // eslint-disable-next-line no-restricted-syntax
 import * as supersetCore from '@apache-superset/core';
 import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
-import { authentication, core, commands, extensions, sqlLab } from 'src/core';
+import {
+  authentication,
+  core,
+  commands,
+  editors,
+  extensions,
+  sqlLab,
+} from 'src/core';
 import { useSelector } from 'react-redux';
 import { RootState } from 'src/views/store';
 import { useExtensionsContext } from './ExtensionsContext';
@@ -32,6 +39,7 @@ declare global {
       authentication: typeof authentication;
       core: typeof core;
       commands: typeof commands;
+      editors: typeof editors;
       extensions: typeof extensions;
       sqlLab: typeof sqlLab;
     };
@@ -61,6 +69,7 @@ const ExtensionsStartup = () => {
       authentication,
       core,
       commands,
+      editors,
       extensions,
       sqlLab,
     };
diff --git a/superset-frontend/src/features/annotations/AnnotationModal.tsx b/superset-frontend/src/features/annotations/AnnotationModal.tsx
index 13f86bde002..3e5ac70de48 100644
--- a/superset-frontend/src/features/annotations/AnnotationModal.tsx
+++ b/superset-frontend/src/features/annotations/AnnotationModal.tsx
@@ -23,18 +23,34 @@ import { styled } from '@apache-superset/core/ui';
 import { useSingleViewResource } from 'src/views/CRUD/hooks';
 import { extendedDayjs } from '@superset-ui/core/utils/dates';
 import withToasts from 'src/components/MessageToasts/withToasts';
-import {
-  Input,
-  JsonEditor,
-  Modal,
-  RangePicker,
-} from '@superset-ui/core/components';
+import { Input, Modal, RangePicker } from '@superset-ui/core/components';
 import { useJsonValidation } from '@superset-ui/core/components/AsyncAceEditor';
-
+import type { editors } from '@apache-superset/core';
+import { EditorHost } from 'src/core/editors';
 import { OnlyKeyWithType } from 'src/utils/types';
 import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
 import { AnnotationObject } from './types';
 
+type EditorAnnotation = editors.EditorAnnotation;
+
+/**
+ * Convert Ace annotation format to EditorAnnotation format.
+ */
+const toEditorAnnotations = (
+  aceAnnotations: Array<{
+    type: string;
+    row: number;
+    column: number;
+    text: string;
+  }>,
+): EditorAnnotation[] =>
+  aceAnnotations.map(ann => ({
+    severity: ann.type as EditorAnnotation['severity'],
+    line: ann.row,
+    column: ann.column,
+    message: ann.text,
+  }));
+
 interface AnnotationModalProps {
   addDangerToast: (msg: string) => void;
   addSuccessToast: (msg: string) => void;
@@ -50,7 +66,7 @@ const StyledAnnotationTitle = styled.div`
     ${({ theme }) => theme.sizeUnit * 4}px auto;
 `;
 
-const StyledJsonEditor = styled(JsonEditor)`
+const StyledEditorHost = styled(EditorHost)`
   /* Border is already applied by AceEditor itself */
 `;
 
@@ -329,16 +345,18 @@ const AnnotationModal: FunctionComponent<AnnotationModalProps> = ({
       </AnnotationContainer>
       <AnnotationContainer>
         <div className="control-label">{t('JSON metadata')}</div>
-        <StyledJsonEditor
+        <StyledEditorHost
+          id="annotation-json-metadata"
           onChange={onJsonChange}
           value={
             currentAnnotation?.json_metadata
               ? currentAnnotation.json_metadata
               : ''
           }
+          language="json"
           width="100%"
           height="120px"
-          annotations={jsonAnnotations}
+          annotations={toEditorAnnotations(jsonAnnotations)}
         />
       </AnnotationContainer>
     </Modal>
diff --git a/superset-frontend/src/features/cssTemplates/CssTemplateModal.tsx b/superset-frontend/src/features/cssTemplates/CssTemplateModal.tsx
index 46dba2b8524..45fe8498ec9 100644
--- a/superset-frontend/src/features/cssTemplates/CssTemplateModal.tsx
+++ b/superset-frontend/src/features/cssTemplates/CssTemplateModal.tsx
@@ -22,7 +22,8 @@ import { css, styled } from '@apache-superset/core/ui';
 import { useSingleViewResource } from 'src/views/CRUD/hooks';
 import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
 import withToasts from 'src/components/MessageToasts/withToasts';
-import { Input, CssEditor, Modal } from '@superset-ui/core/components';
+import { Input, Modal } from '@superset-ui/core/components';
+import { EditorHost } from 'src/core/editors';
 import { Typography } from '@superset-ui/core/components/Typography';
 import { OnlyKeyWithType } from 'src/utils/types';
 import { TemplateObject } from './types';
@@ -46,7 +47,7 @@ const StyledCssTemplateTitle = styled.div(
   `,
 );
 
-const StyledCssEditor = styled(CssEditor)`
+const StyledEditorHost = styled(EditorHost)`
   ${({ theme }) => css`
     border-radius: ${theme.borderRadius}px;
     border: 1px solid ${theme.colorPrimaryBg};
@@ -262,9 +263,11 @@ const CssTemplateModal: FunctionComponent<CssTemplateModalProps> = ({
           {t('css')}
           <span className="required">*</span>
         </div>
-        <StyledCssEditor
+        <StyledEditorHost
+          id="css-template-editor"
           onChange={onCssChange}
-          value={currentCssTemplate?.css}
+          value={currentCssTemplate?.css ?? ''}
+          language="css"
           width="100%"
         />
       </TemplateContainer>
diff --git a/superset-frontend/src/features/themes/ThemeModal.tsx b/superset-frontend/src/features/themes/ThemeModal.tsx
index b56eef55b76..534e1aa1425 100644
--- a/superset-frontend/src/features/themes/ThemeModal.tsx
+++ b/superset-frontend/src/features/themes/ThemeModal.tsx
@@ -39,17 +39,37 @@ import {
   Button,
   Form,
   Input,
-  JsonEditor,
   Modal,
   Space,
   Tooltip,
 } from '@superset-ui/core/components';
 import { useJsonValidation } from '@superset-ui/core/components/AsyncAceEditor';
+import type { editors } from '@apache-superset/core';
+import { EditorHost } from 'src/core/editors';
 import { Typography } from '@superset-ui/core/components/Typography';
-
 import { OnlyKeyWithType } from 'src/utils/types';
 import { ThemeObject } from './types';
 
+type EditorAnnotation = editors.EditorAnnotation;
+
+/**
+ * Convert Ace annotation format to EditorAnnotation format.
+ */
+const toEditorAnnotations = (
+  aceAnnotations: Array<{
+    type: string;
+    row: number;
+    column: number;
+    text: string;
+  }>,
+): EditorAnnotation[] =>
+  aceAnnotations.map(ann => ({
+    severity: ann.type as EditorAnnotation['severity'],
+    line: ann.row,
+    column: ann.column,
+    message: ann.text,
+  }));
+
 interface ThemeModalProps {
   addDangerToast: (msg: string) => void;
   addSuccessToast?: (msg: string) => void;
@@ -66,7 +86,7 @@ type ThemeStringKeys = keyof Pick<
   OnlyKeyWithType<ThemeObject, string>
 >;
 
-const StyledJsonEditor = styled.div`
+const StyledEditorWrapper = styled.div`
   ${({ theme }) => css`
     .ace_editor {
       border-radius: ${theme.borderRadius}px;
@@ -474,22 +494,21 @@ const ThemeModal: FunctionComponent<ThemeModalProps> = ({
                 </span>
               }
             />
-            <StyledJsonEditor>
-              <JsonEditor
-                showLoadingForImport
-                name="json_data"
+            <StyledEditorWrapper>
+              <EditorHost
+                id="theme-json-editor"
                 value={currentTheme?.json_data || ''}
                 onChange={onJsonDataChange}
+                language="json"
                 tabSize={2}
+                readOnly={isReadOnly}
+                wordWrap
+                lineNumbers
                 width="100%"
                 height="250px"
-                wrapEnabled
-                readOnly={isReadOnly}
-                showGutter
-                showPrintMargin={false}
-                annotations={jsonAnnotations}
+                annotations={toEditorAnnotations(jsonAnnotations)}
               />
-            </StyledJsonEditor>
+            </StyledEditorWrapper>
             {canDevelopThemes && (
               <div className="apply-button-container">
                 <Tooltip