Skip to content

feat: editor settings, remove readonly from single query run, smarter grid column widths#584

Open
emrberk wants to merge 3 commits into
mainfrom
feat/editor-settings
Open

feat: editor settings, remove readonly from single query run, smarter grid column widths#584
emrberk wants to merge 3 commits into
mainfrom
feat/editor-settings

Conversation

@emrberk

@emrberk emrberk commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an Editor settings modal and reworks how the result grid sizes its columns, while letting you keep editing the SQL editor without interruption during a running query.

Changes

  • Editor settings modal (editor tabs menu → Editor settings):
    • Run with selection: when on, editor run actions execute your highlighted text; when off, they always run the statement under the cursor. Shared query links still run their selected fragment regardless of this setting.
    • Maximum column width: cap how wide result columns can auto-size (or leave empty for fully automatic). You can still drag to resize any column.
  • Keep editing while a query runs: the editor is no longer locked during execution. You can type, add lines above a running query, or start a new run; the run indicator follows the query as it moves, and the result still lands on the right statement (or is cleanly dropped if you rewrite it mid-flight).
  • Smarter result-grid column widths: columns are measured from the actual rendered font, wide columns share the available space more sensibly instead of hard-capping, and widths re-settle once web fonts finish loading. Long array values are truncated to fit with an ellipsis.

Notes

  • New settings persist in local storage (editor.runWithSelection, grid.maxColumnWidth); no schema/migration changes.
  • Both settings apply to the main Result grid and to Notebook result cells.

Testing

  • Unit tests added for width sampling/clamping, the run-with-selection gating, inflight-query offset tracking, the notification-key reducer, and settings parsing/validation.
  • E2E coverage added for the settings modal, run-with-selection behavior, editing while a query runs, and column-width allocation.
  • yarn typecheck, yarn lint, yarn test:unit, and yarn build all pass.

@emrberk

emrberk commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Code Review — staged diff (level 3, full mission-critical pass)

Reviewed at level 3 — all 13 review agents, per-finding verification. Scope of the change: editing the editor while a query runs (Monaco inflight-offset tracking), a "Run with selection" setting, a max-column-width setting, canvas-based result-grid column measurement, and a notification-key collision reducer.

Quality gates: yarn typecheck ✅ · yarn lint ✅ · yarn build ✅ · yarn test:unit ✅ (1305 tests). No quality-gate failures.

The core high-risk machinery — supersede/runSeq guarding, inflight offset math, the notification-key collision reducer, the width memo chain, and every changed cross-context callsite — was code-traced and is correct. No data-loss or data-misrepresentation bug survived verification. The remaining findings are a few minor UX, accessibility, and performance items.

Issues

# Issue Category Severity Location Description Repro Suggested fix
1 Focus may not return to trigger on modal close Accessibility & UX Minor (NEEDS VERIFICATION) in-diff (src/components/EditorSettingsModal/index.tsx:229, src/scenes/Editor/Monaco/tabs.tsx:672) The modal opens from a DropdownMenu.Item. On close, Radix FocusScope restores focus to the element that was active when the dialog mounted — potentially the now-unmounted dropdown item — so focus can fall to <body> instead of the tabs "…" button, stranding keyboard/AT users. Could not reproduce live (the running dev instance did not reflect the new menu item), so this is static-analysis only; the common case often works. Keyboard: open tabs "…" → Editor settings → Escape; inspect document.activeElement. If confirmed live, handle onCloseAutoFocus on Dialog.Content and refocus the tabs trigger from the tabs.tsx callsite.
2 Per-cell O(L) upper-bound scan in sampling Performance & rendering Minor in-diff (src/components/ResultGrid/inlineGridUtils.ts:269) sampleColumnWidths runs an unconditional O(L) cellWidthUpperBound scan (up to MAX_MEASURE_CHARS=2000 chars) on every sampled cell, even when the running width already dominates. Not a complexity regression (both old and new are O(C·R·L) because formatting is O(L)), but a constant-factor cost on wide string columns (up to 1000 sampled rows × long cells). A cheap length × maxAsciiAdvance O(1) upper bound could gate the scan. Wide string_agg/varchar result, 1000 sampled rows. Add an O(1) length-based upper-bound early-continue before the scan (len * maxAdvance * margin + chrome ≤ width); a lower-bound gate would under-measure.
3 Toggle draft not saved when width is invalid Accessibility & UX Minor in-diff (src/components/EditorSettingsModal/index.tsx:124) handleSave early-returns on an invalid max-column-width before persisting runWithSelection. If the user flips the switch and enters an invalid width, Save only surfaces the width error; the toggle is not persisted that click. Mitigated: the modal stays open and the switch draft is retained, so fixing the width and re-saving persists both — nothing is permanently lost. Defensible atomic-validation, flagged for UX transparency. Flip the switch, type 10 in the width field, click Save. Optional: persist the valid setting independently, or make the coupling explicit.
4 Array cell renders full (clipped) at extreme min width Performance & rendering Minor in-diff (src/components/ResultGrid/inlineGridUtils.ts:195) When the column is so narrow that ≤3 array content chars fit, fitArrayToWidth returns the full array string (CSS-clips it) instead of a ...] ellipsis. Reachable via max-column-width = 60 on an ARRAY column. Pre-existing design (old code's maxContentLength > 3 guard did the same); data still available via copy. Cosmetic only. Set max column width to 60, run a query with a long ARRAY column. Optional: return wrapArray("...", dim) instead of full so the truncation intent still reads.

False-positives

  • Font-load double-sample: The useFontsReadyinvalidateMeasuredFont → re-sample is required, not wasteful — the first (fallback-font) measurement is genuinely wrong and must be redone once the webfont loads. Bounded to once per result. Not a bug.
  • columnDefs rebuild / array re-truncate on resize drag: Both bounded (O(C); visible cells only) and only during active resize. Negligible; not worth complicating the code.
  • useContainerWidth initial null → empty grid forever: Requires a ResultGrid mounted in a permanently-zero-width container that never fires a nonzero ResizeObserver entry. Both current callsites mount in laid-out containers; unreachable today. Latent constraint, not a bug.
  • AI-conversation updateNotificationKey refusal breaks linkage: SAFE. meta.queryKey is persisted to IndexedDB before the dispatch (AIConversationProvider:923), and "Fix query" reads executionRefs, not the notification store. The refusal is display-only and only fires when a newer real editor notification legitimately occupies the key — keeping it is correct.
  • Reducer refusal leaves flat/keyed arrays divergent: The refusal is return state — a fully atomic referential no-op; both structures stay in their already-consistent state. Verified by reducers.test.ts:85 (next.toBe(state)).
  • document.fonts unguarded / canvas unavailable / security sinks: All document.fonts access is behind "fonts" in document; canvas-null degrades to the fallback char-width path; only TextMetrics.width (universal) is used. No XSS/injection/secret sink introduced — max-width flows as a numeric prop, and share-link params are pre-existing and target the user's own DB.

Summary

Verdict: Approve — all remaining findings are minor or optional; none blocking.

  • Draft findings: 4 verified / kept; ~15+ candidate concerns dropped as false-positives or by-design after per-finding source verification.
  • In-diff vs out-of-diff: 4 in-diff, 0 out-of-diff. The zero is genuine, not a cross-context underrun: the changed contracts are either new required params/props whose few callers were all updated in the same diff, or the reducer refusal (a graceful improvement at every callsite). The cross-context pass widened greps across tests/stories/dynamic construction and cleared the AI callsite, the Monaco shift callsites, the notebook execution path (correctly has no symmetric rekey need), and all useLocalStorage / Switch / ResultGrid consumers.
  • Regressions/tradeoffs: None found. FixQueryButton replacing its ! assertions with real null guards, and the LocalStorageProvider value memoization, are genuine quality/perf improvements. The edit-while-running machinery, supersede guarding, and offset math are correct and well-unit-tested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant