diff --git a/src/lib/components/Editor/Editor.svelte b/src/lib/components/Editor/Editor.svelte
index d7f7134..9935e43 100644
--- a/src/lib/components/Editor/Editor.svelte
+++ b/src/lib/components/Editor/Editor.svelte
@@ -2,7 +2,7 @@
import type { Table } from '$lib/olap-engine';
import { sql } from '@codemirror/lang-sql';
import { Compartment, EditorState } from '@codemirror/state';
- import { EditorView, keymap, placeholder } from '@codemirror/view';
+ import { EditorView, placeholder } from '@codemirror/view';
import { untrack } from 'svelte';
import './codemirror.css';
import { default_extensions, default_keymaps } from './extensions';
@@ -11,11 +11,10 @@
type Props = {
value: string;
- onExec?: () => unknown;
tables?: Table[];
};
- let { value = $bindable(''), onExec, tables = [] }: Props = $props();
+ let { value = $bindable(''), tables = [] }: Props = $props();
let container: HTMLDivElement;
let editor_view: EditorView;
@@ -37,16 +36,7 @@
value = update.state.doc.toString();
}
}),
- placeholder('Enter a query...'),
- keymap.of([
- {
- key: 'Mod-Enter',
- run: () => {
- onExec?.();
- return true;
- }
- }
- ])
+ placeholder('Type your query...')
]
});
@@ -78,7 +68,6 @@
div {
width: 100%;
height: 100%;
- padding: 7px 2px;
background-color: hsl(0deg 0% 5%);
}
diff --git a/src/lib/components/History.svelte b/src/lib/components/History.svelte
index 0e7d92e..b641b66 100644
--- a/src/lib/components/History.svelte
+++ b/src/lib/components/History.svelte
@@ -41,6 +41,10 @@
e.currentTarget.blur();
}
}}
+ ontouchend={(e) => {
+ onHistoryClick?.(entry);
+ e.currentTarget.blur();
+ }}
>
{entry.content}
{dayjs(entry.timestamp).fromNow()}
diff --git a/src/lib/components/Queries/Queries.svelte b/src/lib/components/Queries/Queries.svelte
index 7ea2694..64816a6 100644
--- a/src/lib/components/Queries/Queries.svelte
+++ b/src/lib/components/Queries/Queries.svelte
@@ -85,6 +85,10 @@
e.currentTarget.blur();
}
}}
+ ontouchend={async (e) => {
+ await onopen?.(query);
+ e.currentTarget.blur();
+ }}
>
{#if editing_id === query.id}
diff --git a/src/lib/components/Tab.svelte b/src/lib/components/Tab.svelte
new file mode 100644
index 0000000..6fa8d02
--- /dev/null
+++ b/src/lib/components/Tab.svelte
@@ -0,0 +1,78 @@
+
+
+
{}}>
+ {label}
+
+ {#if !hide_close}
+
+ {/if}
+
+
+
diff --git a/src/lib/icons/XMark.svelte b/src/lib/icons/XMark.svelte
new file mode 100644
index 0000000..18c0cab
--- /dev/null
+++ b/src/lib/icons/XMark.svelte
@@ -0,0 +1,18 @@
+
+
+
diff --git a/src/lib/migrations/003_create_tabs_table.sql b/src/lib/migrations/003_create_tabs_table.sql
new file mode 100644
index 0000000..35fcf01
--- /dev/null
+++ b/src/lib/migrations/003_create_tabs_table.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS tabs (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ contents TEXT NOT NULL,
+ query_id INTEGER,
+ tab_index INTEGER NOT NULL,
+ active BOOL DEFAULT FALSE UNIQUE
+);
diff --git a/src/lib/migrations/index.ts b/src/lib/migrations/index.ts
index cdbdb19..52aad25 100644
--- a/src/lib/migrations/index.ts
+++ b/src/lib/migrations/index.ts
@@ -2,8 +2,10 @@ import type { Migration } from '@agnosticeng/migrate';
import CREATE_HISTORY_TABLE_SCRIPT from './001_create_history_table.sql?raw';
import CREATE_QUERIES_TABLE_SCRIPT from './002_create_queries_table.sql?raw';
+import CREATE_TABS_TABLE_SCRIPT from './003_create_tabs_table.sql?raw';
export const MIGRATIONS: Migration[] = [
{ name: 'create_history_table', script: CREATE_HISTORY_TABLE_SCRIPT },
- { name: 'create_queries_table', script: CREATE_QUERIES_TABLE_SCRIPT }
+ { name: 'create_queries_table', script: CREATE_QUERIES_TABLE_SCRIPT },
+ { name: 'create_tabs_table', script: CREATE_TABS_TABLE_SCRIPT }
];
diff --git a/src/lib/repositories/tabs.ts b/src/lib/repositories/tabs.ts
new file mode 100644
index 0000000..e5c2a11
--- /dev/null
+++ b/src/lib/repositories/tabs.ts
@@ -0,0 +1,55 @@
+import { db, type Database } from '$lib/database';
+
+export interface Tab {
+ id: string;
+ name: string;
+ contents: string;
+ query_id?: number;
+}
+
+export interface TabRepository {
+ get(): Promise<[tabs: Tab[], activeIndex: number]>;
+ save(tabs: Tab[], activeIndex: number): Promise
;
+}
+
+class SQLiteTabRepository implements TabRepository {
+ constructor(private db: Database) {}
+
+ async get(): Promise<[tabs: Tab[], activeIndex: number]> {
+ const rows = await this.db.exec('select * from tabs order by tab_index');
+ let index = rows.findIndex((r) => r.active);
+ return [rows.map(row_to_tab), Math.max(0, index)];
+ }
+
+ async save(tabs: Tab[], activeIndex: number): Promise {
+ const rows = tabs.map((tab, tab_index) => ({ ...tab, tab_index }));
+
+ await this.db.exec(
+ `DELETE FROM tabs;
+INSERT INTO tabs (id, name, contents, query_id, tab_index, active)
+VALUES ${Array.from({ length: rows.length }).fill('(?,?,?,?,?, ?)').join(',\n')}
+`,
+ rows
+ .map((r) => [
+ r.id,
+ r.name,
+ r.contents,
+ r.query_id ?? null,
+ r.tab_index,
+ r.tab_index === activeIndex || null
+ ])
+ .flat()
+ );
+ }
+}
+
+function row_to_tab(row: Awaited>[number]): Tab {
+ return {
+ id: row.id as string,
+ contents: row.contents as string,
+ name: row.name as string,
+ query_id: row.query_id as number | undefined
+ };
+}
+
+export const tab_repository: TabRepository = new SQLiteTabRepository(db);
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 245541d..079cc25 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -6,36 +6,37 @@
import { SaveQueryModal } from '$lib/components/Queries';
import Result from '$lib/components/Result.svelte';
import SideBar from '$lib/components/SideBar.svelte';
+ import TabComponent from '$lib/components/Tab.svelte';
import { set_app_context } from '$lib/context';
import Bars3 from '$lib/icons/Bars3.svelte';
import Play from '$lib/icons/Play.svelte';
+ import Plus from '$lib/icons/Plus.svelte';
import Save from '$lib/icons/Save.svelte';
import type { Table } from '$lib/olap-engine';
import { engine, type OLAPResponse } from '$lib/olap-engine';
import { history_repository, type HistoryEntry } from '$lib/repositories/history';
import { query_repository, type Query } from '$lib/repositories/queries';
+ import { tab_repository, type Tab } from '$lib/repositories/tabs';
import { SplitPane } from '@rich_harris/svelte-split-pane';
+ import debounce from 'p-debounce';
import type { ComponentProps } from 'svelte';
let response = $state.raw();
- let query = $state('');
let loading = $state(false);
async function handleExec() {
+ const query = currentTab.contents;
if (loading || !query) {
return;
}
loading = true;
- const query_to_execute = query;
- response = await engine.exec(query_to_execute).finally(() => (loading = false));
+ response = await engine.exec(query).finally(() => (loading = false));
const last = await history_repository.getLast();
- if (response && last?.content !== query_to_execute) {
- await addHistoryEntry(query_to_execute);
- }
+ if (response && last?.content !== query) await addHistoryEntry(query);
}
let tables = $state.raw([]);
@@ -64,7 +65,10 @@
}
function handleHistoryClick(entry: HistoryEntry) {
- query = entry.content;
+ if (currentTab.contents) {
+ selectedTabIndex =
+ tabs.push({ id: crypto.randomUUID(), contents: entry.content, name: 'Untitled' }) - 1;
+ } else tabs[selectedTabIndex] = { ...currentTab, contents: entry.content };
if (is_mobile) open_drawer = false;
}
@@ -74,41 +78,72 @@
let save_query_modal = $state>();
- function handleKeyDown(event: KeyboardEvent) {
+ async function handleKeyDown(event: KeyboardEvent) {
if (event.key === 's' && event.metaKey) {
- if (query) {
- event.preventDefault();
- save_query_modal?.show();
- }
+ event.preventDefault();
+ handleSaveQuery();
}
+
+ if (event.key === 'Enter' && event.metaKey) handleExec();
}
async function handleCreateQuery({
name
}: Parameters['onCreate']>>['0']) {
- const q = await query_repository.create(name, query);
+ const q = await query_repository.create(name, currentTab.contents);
queries = queries.concat(q);
+ tabs[selectedTabIndex] = { ...currentTab, name, query_id: q.id };
}
async function handleDeleteQuery(query: Query) {
await query_repository.delete(query.id);
- const index = queries.indexOf(query);
- queries = queries.slice(0, index).concat(queries.slice(index + 1));
+ queries = queries.toSpliced(queries.indexOf(query), 1);
}
- function handleQueryOpen(_query: Query) {
- query = _query.sql;
+ function handleQueryOpen(query: Query) {
+ const index = tabs.findIndex((t) => t.query_id === query.id);
+ if (index === -1) {
+ if (currentTab.contents) {
+ selectedTabIndex =
+ tabs.push({
+ id: crypto.randomUUID(),
+ contents: query.sql,
+ name: query.name,
+ query_id: query.id
+ }) - 1;
+ } else
+ tabs[selectedTabIndex] = {
+ ...currentTab,
+ contents: query.sql,
+ name: query.name,
+ query_id: query.id
+ };
+ } else selectedTabIndex = index;
+
if (is_mobile) open_drawer = false;
}
async function handleQueryRename(query: Query) {
const updated = await query_repository.update(query);
const index = queries.findIndex((query) => query.id === updated.id);
+ if (index !== -1) queries = queries.with(index, updated);
+
+ const tab_index = tabs.findIndex((t) => t.query_id === updated.id);
+ if (tab_index !== -1) {
+ tabs[tab_index] = { ...tabs[tab_index], name: updated.name, contents: updated.sql };
+ }
+ }
+
+ async function handleSaveQuery() {
+ const { contents, query_id: query_id } = currentTab;
+ if (contents && !query_id) {
+ return save_query_modal?.show();
+ }
+
+ const index = queries.findIndex((q) => q.id === query_id);
if (index !== -1) {
- queries = queries
- .slice(0, index)
- .concat(updated)
- .concat(queries.slice(index + 1));
+ const updated = await query_repository.update({ ...queries[index], sql: contents });
+ queries = queries.with(index, updated);
}
}
@@ -122,6 +157,43 @@
$effect(() => {
if (!is_mobile) open_drawer = false;
});
+
+ let tabs = $state([]);
+ $effect(
+ () =>
+ void tab_repository.get().then(([t, active]) => {
+ if (t.length) (tabs = t), (selectedTabIndex = active);
+ else tabs.push({ id: crypto.randomUUID(), contents: '', name: 'Untitled' });
+ })
+ );
+
+ const saveTabs = debounce(
+ (tabs: Tab[], activeIndex: number) => tab_repository.save(tabs, activeIndex),
+ 2_000
+ );
+
+ let selectedTabIndex = $state(0);
+ const currentTab = $derived(tabs[selectedTabIndex]);
+ const canSave = $derived.by(() => {
+ if (!tabs.length) return false;
+ if (currentTab.query_id) {
+ const query = queries.find((q) => q.id === currentTab.query_id);
+ return query?.sql !== currentTab.contents;
+ }
+
+ return !!currentTab.contents;
+ });
+
+ function addNewTab() {
+ selectedTabIndex = tabs.push({ id: crypto.randomUUID(), name: 'Untitled', contents: '' }) - 1;
+ }
+
+ function closeTab(index: number) {
+ tabs.splice(index, 1);
+ selectedTabIndex = Math.max(0, selectedTabIndex - 1);
+ }
+
+ $effect(() => void saveTabs($state.snapshot(tabs), selectedTabIndex).catch(console.error));
@@ -162,24 +234,45 @@
{#snippet a()}
-