diff --git a/src/ui/app.js b/src/ui/app.js index 8af225f..30b0883 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -20,7 +20,7 @@ import { generatePKCE, randomState } from '../core/pkce.js'; import * as oauthCfg from '../net/oauth-config.js'; import * as oauth from '../net/oauth.js'; import * as ch from '../net/ch-client.js'; -import { mountEditor, insertAtCursor, insertTopLine, replaceEditor } from './editor.js'; +import { mountEditor, insertAtCursor, replaceEditor } from './editor.js'; import { renderTabs, selectTab, newTab, closeTab, loadIntoNewTab } from './tabs.js'; import { renderSchema } from './schema.js'; import { renderResults } from './results.js'; @@ -317,8 +317,9 @@ export function createApp(env = {}) { } // Fetch the DDL for `target` (e.g. 'db.table' or 'DATABASE db') with - // SHOW CREATE, pretty-print it through formatQuery(), and drop it in as a top - // line. Two round-trips by design; if formatting fails the raw DDL is used. + // SHOW CREATE, pretty-print it through formatQuery(), and drop it into the + // editor (replacing its content — undo restores the prior query). Two + // round-trips by design; if formatting fails the raw DDL is used. async function insertCreate(target) { await ensureConfig(); if (!(await getToken())) { chCtx.onSignedOut(); return; } @@ -331,7 +332,7 @@ export function createApp(env = {}) { const fmt = await ch.queryJson(chCtx, 'SELECT formatQuery(' + sqlString(stmt) + ') AS q FORMAT JSON'); out = (fmt.data && fmt.data[0] && fmt.data[0].q) || stmt; } catch { /* formatting is best-effort — fall back to the raw DDL */ } - insertTopLine(app, out); + replaceEditor(app, out); } catch (e) { flashToast('SHOW CREATE failed: ' + String((e && e.message) || e), { document: doc }); } @@ -539,7 +540,7 @@ export function createApp(env = {}) { insertCreate, openShortcuts: () => openShortcuts(app), insertAtCursor: (text) => insertAtCursor(app, text), - insertTopLine: (text) => insertTopLine(app, text), + replaceEditor: (text) => replaceEditor(app, text), loadColumns, rerenderTabs: () => renderTabs(app), rerenderResults: () => renderResults(app), diff --git a/src/ui/editor.js b/src/ui/editor.js index bf08725..5f5aa38 100644 --- a/src/ui/editor.js +++ b/src/ui/editor.js @@ -114,15 +114,6 @@ export function insertAtCursor(app, text) { applyEdit(ta, text); } -/** Prepend `text` as a new first line (does not replace existing content). */ -export function insertTopLine(app, text) { - const ta = app.dom.editorTextarea; - if (!ta) return; - ta.focus(); - ta.selectionStart = ta.selectionEnd = 0; - applyEdit(ta, text + (ta.value ? '\n' : '')); -} - /** Replace the whole editor content with `text` (undoable). */ export function replaceEditor(app, text) { const ta = app.dom.editorTextarea; diff --git a/src/ui/schema.js b/src/ui/schema.js index 954f783..42552c4 100644 --- a/src/ui/schema.js +++ b/src/ui/schema.js @@ -86,7 +86,7 @@ export function renderSchema(app) { if (state.expandedTables.has(key) && tb.columns == null) app.actions.loadColumns(db.db, tb.name, tb); else renderSchema(app); }, - ondblclick: (e) => { e.stopPropagation(); app.actions.insertTopLine('SELECT * FROM ' + key + ' LIMIT 100'); }, + ondblclick: (e) => { e.stopPropagation(); app.actions.replaceEditor('SELECT * FROM ' + key + ' LIMIT 100'); }, }, ...treeRow(Icon.table(), tb.name, formatRows(tb.total_rows), { expanded: isOpen, iconColor: 'var(--accent)' }), )); diff --git a/tests/helpers/fake-app.js b/tests/helpers/fake-app.js index 2cf1ace..fe1b676 100644 --- a/tests/helpers/fake-app.js +++ b/tests/helpers/fake-app.js @@ -48,7 +48,7 @@ export function makeApp(over = {}) { insertCreate: vi.fn(), openShortcuts: vi.fn(), insertAtCursor: vi.fn(), - insertTopLine: vi.fn(), + replaceEditor: vi.fn(), loadColumns: vi.fn(), rerenderTabs: vi.fn(), rerenderResults: vi.fn(), diff --git a/tests/unit/app.test.js b/tests/unit/app.test.js index 880fe98..7683d75 100644 --- a/tests/unit/app.test.js +++ b/tests/unit/app.test.js @@ -674,6 +674,7 @@ describe('exhaustive controller coverage', () => { app.state.tabs.push({ id: 'tx', name: 'X', sql: '', dirty: false, result: null, savedId: null }); app.actions.selectTab('tx'); app.actions.insertAtCursor('zz'); + app.actions.replaceEditor('SELECT 9'); app.actions.loadIntoNewTab('n', 'SELECT 2'); app.actions.rerenderTabs(); app.actions.rerenderResults(); diff --git a/tests/unit/editor.test.js b/tests/unit/editor.test.js index a489da8..b8c4761 100644 --- a/tests/unit/editor.test.js +++ b/tests/unit/editor.test.js @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { renderHighlightInto, mountEditor, insertAtCursor, insertTopLine, replaceEditor, IDENT_MIME } from '../../src/ui/editor.js'; +import { renderHighlightInto, mountEditor, insertAtCursor, replaceEditor, IDENT_MIME } from '../../src/ui/editor.js'; import { makeApp } from '../helpers/fake-app.js'; describe('renderHighlightInto', () => { @@ -128,32 +128,21 @@ describe('insertAtCursor', () => { }); }); -describe('insertTopLine / replaceEditor', () => { +describe('replaceEditor', () => { function mounted(sql = '') { const app = makeApp(); app.activeTab().sql = sql; mountEditor(app, document.createElement('div')); return { app, ta: app.dom.editorTextarea }; } - it('insertTopLine prepends a new first line above existing content', () => { - const { app, ta } = mounted('SELECT 1'); - insertTopLine(app, 'SHOW CREATE db.t'); - expect(ta.value).toBe('SHOW CREATE db.t\nSELECT 1'); - expect(app.activeTab().sql).toBe('SHOW CREATE db.t\nSELECT 1'); - }); - it('insertTopLine on an empty editor adds no trailing newline', () => { - const { ta, app } = mounted(''); - insertTopLine(app, 'SELECT 1'); - expect(ta.value).toBe('SELECT 1'); - }); - it('replaceEditor swaps the whole content', () => { + it('swaps the whole content', () => { const { ta, app } = mounted('select 1'); replaceEditor(app, 'SELECT\n 1'); expect(ta.value).toBe('SELECT\n 1'); + expect(app.activeTab().sql).toBe('SELECT\n 1'); }); - it('insertTopLine / replaceEditor no-op without a textarea', () => { + it('no-ops without a textarea', () => { const app = makeApp(); - expect(() => insertTopLine(app, 'x')).not.toThrow(); expect(() => replaceEditor(app, 'x')).not.toThrow(); }); }); diff --git a/tests/unit/schema.test.js b/tests/unit/schema.test.js index f573d3c..cb60fb2 100644 --- a/tests/unit/schema.test.js +++ b/tests/unit/schema.test.js @@ -106,12 +106,12 @@ describe('renderSchema tree', () => { click(ordersRow); // collapse expect(app.state.expandedTables.has('db1.orders')).toBe(false); }); - it('double-clicking a table inserts a SELECT * as a top line', () => { + it('double-clicking a table replaces the editor with a SELECT *', () => { const app = withSchema(); renderSchema(app); const ordersRow = rows(app).find((r) => r.querySelector('.label').textContent === 'orders'); dblclick(ordersRow); - expect(app.actions.insertTopLine).toHaveBeenCalledWith('SELECT * FROM db1.orders LIMIT 100'); + expect(app.actions.replaceEditor).toHaveBeenCalledWith('SELECT * FROM db1.orders LIMIT 100'); }); it('shift-clicking a table inserts its formatted DDL without expanding', () => { const app = withSchema();