From 8d59e69da4f7a014118e0d21405bba00eb264e6c Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 28 Aug 2026 12:49:35 +0800 Subject: [PATCH 1/8] Add support for running only selected query in QueryEditor (#49651) ## Context As per PR title - this behaviour currently exists in the SQL Editor so just bringing it over to the Explorer, applies to both QueryTab and QueryCell since they use the same QueryEditor component "Run" button also updates to "Run selected" for clarity when a specific portion of the code editor is selected image ## Summary by CodeRabbit * **New Features** * Run only the selected SQL when text is highlighted in the query editor. * Run the full query when no text is selected. * Updated the run button label and tooltip to reflect the action. * **Bug Fixes** * Improved query execution for empty or collapsed selections. * Corrected selection state when reopening the query editor. * **Tests** * Added coverage for selected-text, full-query, and editor reopening scenarios. --- .../interfaces/Explorer/QueryEditor/index.tsx | 43 ++++-- .../Explorer/__tests__/QueryTab.test.tsx | 124 +++++++++++++++++- .../components/ui/CodeEditor/CodeEditor.tsx | 15 +-- .../ui/CodeEditor/CodeEditor.utils.test.ts | 51 +++++++ .../ui/CodeEditor/CodeEditor.utils.ts | 13 ++ 5 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 apps/studio/components/ui/CodeEditor/CodeEditor.utils.test.ts diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx index ccafb2762fd73..3e46de3d5c57d 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx @@ -2,7 +2,7 @@ import { useMonaco } from '@monaco-editor/react' import { acceptUntrustedSql, untrustedSql, type UntrustedSqlFragment } from '@supabase/pg-meta' import { useFlag } from 'common' import { CodeSquare, Eye, EyeOff, Play } from 'lucide-react' -import type { editor as monacoEditor } from 'monaco-editor' +import type { editor as monacoEditor, Selection } from 'monaco-editor' import { forwardRef, useEffect, @@ -46,6 +46,7 @@ import { useAddDefinitions } from '@/components/interfaces/SQLEditor/useAddDefin import { ResizableAIWidget } from '@/components/ui/AIEditor/ResizableAIWidget' import { getEditorSelectionParts, type EditorSelection } from '@/components/ui/AIEditor/utils' import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' +import { getEditorValueOrSelection } from '@/components/ui/CodeEditor/CodeEditor.utils' import { DiffEditor } from '@/components/ui/DiffEditor' import { type DatabaseSourceParameters, @@ -195,6 +196,7 @@ export const QueryEditor = forwardRef(funct const [promptState, setPromptState] = useState<(EditorSelection & { isOpen: boolean }) | null>( null ) + const [hasSelection, setHasSelection] = useState(false) const dialect = query._tag === 'logs' ? 'clickhouse' : 'postgres' const { requestCompletion, isCompletionLoading } = useQueryEditorAi({ dialect }) @@ -237,13 +239,6 @@ export const QueryEditor = forwardRef(funct const isExecuting = isExecutingSql || isExecutingLogs const isBusy = isLoadingProject || isResolvingDatabase || isExecuting - /** - * The user's run gesture, and therefore the promotion point for this query's SQL. The - * raw text comes straight off the editor, so it is (re)branded untrusted here — the - * editor boundary — and promoted in the same handler. Which pair of helpers applies is - * decided by `query._tag`, the same discriminant that picks the execution endpoint, so - * Postgres SQL cannot reach the analytics wire or vice versa. - */ const handleRunQuery = async ({ rawSql = sql, shouldForce = false, @@ -409,13 +404,17 @@ export const QueryEditor = forwardRef(funct } - tooltip="Run query" + tooltip={hasSelection ? 'Run selected query' : 'Run query'} disabled={ isBusy || pendingProposal !== null || isRunDisabled || sql.trim().length === 0 } - onClick={() => handleRunQuery()} + onClick={() => { + const editorInstance = editorInstanceRef.current + const rawSql = editorInstance ? getEditorValueOrSelection(editorInstance) : sql + handleRunQuery({ rawSql }) + }} > - Run + {hasSelection ? 'Run selected' : 'Run'} @@ -447,7 +446,10 @@ export const QueryEditor = forwardRef(funct placeholderClassName="top-[13px]" className={variant === 'embedded' ? 'h-44' : undefined} actions={{ - runQuery: { enabled: !isRunDisabled, callback: handleRunQuery }, + runQuery: { + enabled: !isRunDisabled, + callback: (rawSql: string) => handleRunQuery({ rawSql }), + }, }} options={{ minimap: { enabled: false }, @@ -458,6 +460,23 @@ export const QueryEditor = forwardRef(funct editor.onDidBlurEditorWidget(() => onSqlCommitRef.current?.(sqlRef.current)) editorInstanceRef.current = editor + const updateHasSelection = (selection: Selection | null | undefined) => { + const noSelection = + !selection || + (selection.startLineNumber === selection.endLineNumber && + selection.startColumn === selection.endColumn) + setHasSelection(!noSelection) + } + + // A remount (e.g. toggling "Show query" off then on) creates a fresh + // editor with no listener history, so `hasSelection` must be read from + // this instance directly rather than left at whatever the previous + // editor instance last reported. + updateHasSelection(editor.getSelection()) + editor.onDidChangeCursorSelection(({ selection }) => + updateHasSelection(selection) + ) + editor.addAction({ id: 'generate-sql', label: 'Generate SQL', diff --git a/apps/studio/components/interfaces/Explorer/__tests__/QueryTab.test.tsx b/apps/studio/components/interfaces/Explorer/__tests__/QueryTab.test.tsx index 70224e3f4ff8b..a7ddb608be280 100644 --- a/apps/studio/components/interfaces/Explorer/__tests__/QueryTab.test.tsx +++ b/apps/studio/components/interfaces/Explorer/__tests__/QueryTab.test.tsx @@ -1,6 +1,7 @@ import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { HttpResponse } from 'msw' +import { useEffect, useRef } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ExplorerQueryTab } from '../ExplorerQueryTab' @@ -14,6 +15,8 @@ import { setupSqlEditorMocks } from '@/tests/lib/sql-editor-test-utils' const testContext = vi.hoisted(() => ({ flags: { otelLegacyLogs: true } as Record, params: { ref: 'default', id: 'query-test' } as { ref?: string; id?: string }, + /** Simulated editor selection — the mocked CodeEditor's fake editor reads this. */ + selectedText: undefined as string | undefined, })) vi.mock('common', async (importOriginal) => { @@ -30,16 +33,51 @@ vi.mock('@/components/ui/CodeEditor/CodeEditor', () => ({ CodeEditor: ({ value, onInputChange, + onMount, }: { value: string onInputChange?: (value: string | undefined) => void - }) => ( -