diff --git a/superset-frontend/packages/superset-core/src/editors/index.ts b/superset-frontend/packages/superset-core/src/editors/index.ts index a1ed3ed9653..0fea7c7fe71 100644 --- a/superset-frontend/packages/superset-core/src/editors/index.ts +++ b/superset-frontend/packages/superset-core/src/editors/index.ts @@ -369,6 +369,28 @@ export interface EditorProps { theme?: SupersetTheme; } +/** + * A single text change expressed as an offset-based replacement. + */ +export interface ContentChange { + /** Character offset in the document where the replaced range starts */ + rangeOffset: number; + /** Length in characters of the replaced range (0 for pure insertions) */ + rangeLength: number; + /** Text inserted at rangeOffset (empty string for pure deletions) */ + text: string; +} + +/** + * Payload delivered to `onDidChangeContent` listeners. + */ +export interface ContentChangeEvent { + /** Returns the full current content of the editor */ + getValue(): string; + /** The individual changes that occurred in this event */ + changes: ReadonlyArray; +} + /** * Imperative API for controlling the editor programmatically. * @@ -492,6 +514,27 @@ export interface EditorHandle { * - CodeMirror: editor.requestMeasure() */ resize(): void; + + /** + * Subscribe to content changes in the editor. + * + * The listener receives a {@link ContentChangeEvent} with: + * - `getValue()` — lazy accessor for the full content (call only when needed + * to avoid unnecessary O(n) string allocation on every keystroke) + * - `changes` — the individual edits that occurred, as offset-based replacements + * + * @param listener Called with a ContentChangeEvent on every change + * @param thisArgs Optional `this` context for the listener + * @returns A Disposable that unsubscribes the listener when disposed + * + * @example + * const disposable = editor.onDidChangeContent(e => { + * setStatements(parseStatements(e.getValue())); + * }); + * // Later, to unsubscribe: + * disposable.dispose(); + */ + onDidChangeContent: Event; } /** diff --git a/superset-frontend/packages/superset-core/src/sqlLab/index.ts b/superset-frontend/packages/superset-core/src/sqlLab/index.ts index cae3bc2d4e5..844c25f9ea6 100644 --- a/superset-frontend/packages/superset-core/src/sqlLab/index.ts +++ b/superset-frontend/packages/superset-core/src/sqlLab/index.ts @@ -252,6 +252,22 @@ export interface QueryResult { */ export declare const getActivePanel: () => Panel; +/** + * Switches the active panel in the SQL Lab south pane. + * Built-in panel IDs are 'Results' and 'History'. + * Pinned table panels use the table's ID as their panel ID. + * + * @param panelId The ID of the panel to activate + * @returns Promise that resolves when the panel is activated + * + * @example + * ```typescript + * // Focus the Results panel after running a query + * await setActivePanel('Results'); + * ``` + */ +export declare function setActivePanel(panelId: string): Promise; + /** * Gets the currently active tab in SQL Lab. * diff --git a/superset-frontend/src/core/editors/AceEditorProvider.tsx b/superset-frontend/src/core/editors/AceEditorProvider.tsx index abdf76d14f1..3deb345a217 100644 --- a/superset-frontend/src/core/editors/AceEditorProvider.tsx +++ b/superset-frontend/src/core/editors/AceEditorProvider.tsx @@ -55,6 +55,8 @@ type Range = editors.Range; type Selection = editors.Selection; type EditorAnnotation = editors.EditorAnnotation; type CompletionProvider = editors.CompletionProvider; +type ContentChange = editors.ContentChange; +type ContentChangeEvent = editors.ContentChangeEvent; /** * Maps EditorLanguage to the corresponding Ace editor component. @@ -117,10 +119,14 @@ const createAceEditorHandle = ( }, moveCursorToPosition: (position: Position) => { - aceEditorRef.current?.editor?.moveCursorToPosition({ - row: position.line, - column: position.column, - }); + const editor = aceEditorRef.current?.editor; + if (editor) { + editor.clearSelection(); + editor.moveCursorToPosition({ + row: position.line, + column: position.column, + }); + } }, getSelections: (): Selection[] => { @@ -186,6 +192,33 @@ const createAceEditorHandle = ( resize: () => { aceEditorRef.current?.editor?.resize(); }, + + onDidChangeContent: (listener, thisArgs?) => { + const editor = aceEditorRef.current?.editor; + if (!editor) return new Disposable(() => {}); + const bound = (thisArgs ? listener.bind(thisArgs) : listener) as ( + e: ContentChangeEvent, + ) => void; + const handler = (delta: { + action: 'insert' | 'remove'; + start: { row: number; column: number }; + lines: string[]; + }) => { + const rangeOffset = editor.session.doc.positionToIndex(delta.start); + const changeText = delta.lines.join( + editor.session.doc.getNewLineCharacter(), + ); + const change: ContentChange = + delta.action === 'insert' + ? { rangeOffset, rangeLength: 0, text: changeText } + : { rangeOffset, rangeLength: changeText.length, text: '' }; + bound({ getValue: () => editor.getValue(), changes: [change] }); + }; + editor.session.on('change', handler); + return new Disposable(() => { + editor.session.off('change', handler); + }); + }, }); /** diff --git a/superset-frontend/src/core/sqlLab/index.ts b/superset-frontend/src/core/sqlLab/index.ts index 8e16a41613a..b14be7efd07 100644 --- a/superset-frontend/src/core/sqlLab/index.ts +++ b/superset-frontend/src/core/sqlLab/index.ts @@ -694,6 +694,12 @@ const setSchema: typeof sqlLabApi.setSchema = async (schema: string | null) => { store.dispatch(queryEditorSetSchema(queryEditor ?? null, schema)); }; +const setActivePanel: typeof sqlLabApi.setActivePanel = async ( + panelId: string, +) => { + store.dispatch({ type: SET_ACTIVE_SOUTHPANE_TAB, tabId: panelId }); +}; + export const sqlLab: typeof sqlLabApi = { CTASMethod, getActivePanel, @@ -719,6 +725,7 @@ export const sqlLab: typeof sqlLabApi = { setDatabase, setCatalog, setSchema, + setActivePanel, }; // Export all models