Skip to content

Commit 42040e4

Browse files
authored
style(sql): readable editor text and selection (#2535)
* style(sql): use strong token for editor identifiers for readable contrast * style(sql): theme editor text selection with the selection token * fix(sql): keep syntax colours on text selection instead of white * fix(sql): theme editor selection background and text for readable contrast * docs(sql): trim verbose comments in editor
1 parent 2f0436b commit 42040e4

1 file changed

Lines changed: 21 additions & 32 deletions

File tree

frontend/src/components/pages/sql/sql-editor.tsx

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ import { z } from 'zod';
4242

4343
import type { Catalog, TableRef } from './sql-types';
4444

45-
// Imperative handle exposed to the workspace so the catalog tree can open a
46-
// query in a new editor tab (mirrors the prototype's editorRef).
45+
// Lets the workspace open a query in a new editor tab.
4746
export type SqlEditorHandle = {
4847
/** Open `sql` in a new tab named `name` (or "Query N") and focus it. */
4948
setQuery: (sql: string, name?: string) => void;
@@ -98,10 +97,7 @@ type Tab = { id: number; name: string; sql: string };
9897
const DEFAULT_QUERY =
9998
'SELECT vin, make, model, year, price_usd\nFROM default_redpanda_catalog=>cars\nWHERE in_stock = true\nORDER BY price_usd DESC\nLIMIT 100;';
10099

101-
// Tracks the registry `.dark` class on the document root so the editor (whose
102-
// highlight palette is built from theme-invariant color scales, not Tailwind
103-
// classes) switches theme in lockstep with the rest of the surface. Uses
104-
// useSyncExternalStore — no effect — per project style.
100+
// Re-render the editor when the registry `.dark` class toggles.
105101
function subscribeToColorMode(onStoreChange: () => void): () => void {
106102
const observer = new MutationObserver(onStoreChange);
107103
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
@@ -116,10 +112,7 @@ function useIsDarkMode(): boolean {
116112
return useSyncExternalStore(subscribeToColorMode, getIsDarkSnapshot, () => false);
117113
}
118114

119-
// Editor chrome tuned to match the SQL Studio surface: transparent editor and
120-
// gutter so the surrounding `bg-background` container shows through, with
121-
// muted gutter line numbers. CodeMirror themes are plain CSS, so registry
122-
// custom properties can be referenced directly and stay live.
115+
// Transparent editor/gutter so the `bg-background` surface shows through.
123116
function editorChrome(mode: 'light' | 'dark'): Extension {
124117
return EditorView.theme(
125118
{
@@ -130,6 +123,13 @@ function editorChrome(mode: 'light' | 'dark'): Extension {
130123
lineHeight: '21px',
131124
},
132125
'.cm-content': { padding: '12px 0' },
126+
// A global `::selection` rule otherwise whitens selected text; theme the
127+
// selection pair instead. Full focused path matches the base rule's
128+
// specificity so this (later) theme wins.
129+
'& .cm-selectionBackground, &.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
130+
backgroundColor: 'var(--color-selection)',
131+
},
132+
'.cm-content ::selection': { color: 'var(--color-selection-foreground)' },
133133
'.cm-gutters': { backgroundColor: 'transparent', border: 'none', color: 'var(--color-muted-foreground)' },
134134
'.cm-activeLineGutter': { backgroundColor: 'transparent', color: 'var(--color-foreground)' },
135135
'.cm-activeLine': { backgroundColor: 'var(--color-surface-default-hover)' },
@@ -138,9 +138,7 @@ function editorChrome(mode: 'light' | 'dark'): Extension {
138138
);
139139
}
140140

141-
// SQL syntax palette mapped onto the Lezer highlight tags the SQL grammar
142-
// emits, entirely from theme-adaptive semantic tokens so it tracks light/dark
143-
// without per-mode values.
141+
// SQL syntax palette from semantic tokens, so it tracks light/dark.
144142
function sqlHighlight(): Extension {
145143
return syntaxHighlighting(
146144
HighlightStyle.define([
@@ -156,7 +154,7 @@ function sqlHighlight(): Extension {
156154
tag: [tags.operator, tags.punctuation, tags.separator, tags.paren, tags.brace, tags.squareBracket],
157155
color: 'var(--color-muted-foreground)',
158156
},
159-
{ tag: tags.name, color: 'var(--color-foreground)' },
157+
{ tag: tags.name, color: 'var(--color-strong)' },
160158
])
161159
);
162160
}
@@ -264,12 +262,9 @@ function catalogNameCompletionResult(catalogs: Catalog[], from: number, before:
264262
};
265263
}
266264

267-
// Completion source for Redpanda SQL's catalog arrow notation. The generic
268-
// schema completion can't model `catalog=>table`, so this source:
269-
// - offers catalog names (boosted right after FROM/JOIN); applying one
270-
// inserts `catalog=>` and immediately reopens completion for its tables
271-
// - offers the catalog's tables after `catalog=>` — and after a typed
272-
// `catalog.`, rewriting the dot to `=>` so users land on valid syntax
265+
// Completion for Oxla's `catalog=>table` notation (which the generic schema
266+
// completion can't model): catalog names after FROM/JOIN, then the catalog's
267+
// tables, rewriting a typed `catalog.` to `=>`.
273268
function catalogArrowSource(catalogs: Catalog[]): (context: CompletionContext) => CompletionResult | null {
274269
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: CodeMirror completion context handling is branchy by API shape.
275270
return (context) => {
@@ -303,9 +298,8 @@ function catalogArrowSource(catalogs: Catalog[]): (context: CompletionContext) =
303298
};
304299
}
305300

306-
// Reformats the whole document through sql-formatter (dynamically imported to
307-
// keep it out of the initial bundle; postgresql is the closest dialect to
308-
// Oxla) as a single transaction, so undo restores the pre-format text.
301+
// Reformat via sql-formatter (lazy-loaded; postgresql ≈ Oxla) in one
302+
// transaction so undo restores the original.
309303
async function formatDocument(view: EditorView): Promise<void> {
310304
const { format } = await import('sql-formatter');
311305
const current = view.state.doc.toString();
@@ -391,9 +385,7 @@ export const SqlEditor = forwardRef<SqlEditorHandle, SqlEditorProps>(
391385
runText(active.sql);
392386
};
393387

394-
// The Cmd/Ctrl+Enter keymap is part of the extensions array (rebuilt only on
395-
// catalog/theme changes), so it reads fresh render state through this ref
396-
// without an effect hook.
388+
// Keeps the keymap's run callback current without rebuilding the extensions.
397389
runRef.current = doRun;
398390

399391
const runSelection = () => {
@@ -405,8 +397,7 @@ export const SqlEditor = forwardRef<SqlEditorHandle, SqlEditorProps>(
405397
};
406398

407399
const extensions = useMemo(() => {
408-
// No `schema` here — schemaColumnSource adds it back for dotted
409-
// completions only, avoiding bare table names at the top level.
400+
// No `schema` here — schemaColumnSource adds it back for dotted members only.
410401
const sqlSupport = sqlLanguage({ dialect: PostgreSQL, upperCaseKeywords: true });
411402
return [
412403
// Prec.highest so Mod-Enter beats the default keymap's insertBlankLine.
@@ -439,10 +430,8 @@ export const SqlEditor = forwardRef<SqlEditorHandle, SqlEditorProps>(
439430
if (update.selectionSet) {
440431
setHasSel(!update.state.selection.main.empty);
441432
}
442-
// Auto-open the catalog helper the moment the caller finishes typing
443-
// `FROM `/`JOIN ` (a typed space), so `catalog=>` is suggested without
444-
// a manual Ctrl+Space. Guarded to typing events to avoid re-triggering
445-
// on programmatic edits (formatting, tab seeding).
433+
// Auto-open the catalog helper right after a typed `FROM `/`JOIN `
434+
// (typing events only, so formatting/seeding don't re-trigger it).
446435
if (update.docChanged && update.transactions.some((tr) => tr.isUserEvent('input.type'))) {
447436
const pos = update.state.selection.main.head;
448437
const lineText = update.state.doc.lineAt(pos);

0 commit comments

Comments
 (0)