diff --git a/.changeset/dirty-sites-study.md b/.changeset/dirty-sites-study.md new file mode 100644 index 0000000..df58571 --- /dev/null +++ b/.changeset/dirty-sites-study.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +Added SQLSeal Explorer that makes it easy to work on new queries diff --git a/.changeset/wicked-lights-burn.md b/.changeset/wicked-lights-burn.md new file mode 100644 index 0000000..951eb06 --- /dev/null +++ b/.changeset/wicked-lights-burn.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +sqlite databases can now be previewed using explorer view diff --git a/.changeset/witty-laws-end.md b/.changeset/witty-laws-end.md new file mode 100644 index 0000000..effa067 --- /dev/null +++ b/.changeset/witty-laws-end.md @@ -0,0 +1,5 @@ +--- +"sqlseal": patch +--- + +highlighting code in the copy modal diff --git a/src/modules/database/worker/database.ts b/src/modules/database/worker/database.ts index 2475588..dd7af47 100644 --- a/src/modules/database/worker/database.ts +++ b/src/modules/database/worker/database.ts @@ -13,7 +13,7 @@ import { ColumnDefinition } from "../../../utils/types"; import { sanitise } from "../../../utils/sanitiseColumn"; -function toObjectArray(stmt: Statement) { +export function toObjectArray(stmt: Statement) { const ret = [] while (stmt.step()) { ret.push(stmt.getAsObject()) diff --git a/src/modules/editor/codeblockHandler/CodeblockProcessor.ts b/src/modules/editor/codeblockHandler/CodeblockProcessor.ts index c1368ac..8fe47d3 100644 --- a/src/modules/editor/codeblockHandler/CodeblockProcessor.ts +++ b/src/modules/editor/codeblockHandler/CodeblockProcessor.ts @@ -26,11 +26,12 @@ export class CodeblockProcessor extends MarkdownRenderChild { private source: string, private ctx: MarkdownPostProcessorContext, private rendererRegistry: RendererRegistry, - private db: SqlSealDatabase, + private db: Pick, private cellParser: ModernCellParser, private settings: Settings, private app: App, private sync: Sync, + private tq: typeof transformQuery = transformQuery ) { super(el); @@ -118,7 +119,8 @@ export class CodeblockProcessor extends MarkdownRenderChild { const registeredTablesForContext = await this.sync.getTablesMappingForContext(this.sourceKey); - const res = transformQuery(this.query, registeredTablesForContext); + // Transforming Query + const res = this.tq(this.query, registeredTablesForContext); const transformedQuery = res.sql; if (this.flags.refresh) { diff --git a/src/modules/explorer/Editor.ts b/src/modules/explorer/Editor.ts new file mode 100644 index 0000000..c8bfb40 --- /dev/null +++ b/src/modules/explorer/Editor.ts @@ -0,0 +1,134 @@ +import { EditorState } from "@codemirror/state"; +import { CodeblockProcessor } from "../editor/codeblockHandler/CodeblockProcessor"; +import { EditorView, keymap } from "@codemirror/view"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { EditorMenuBar } from "./EditorMenuBar"; +import { MemoryDatabase } from "./database/memoryDatabase"; +import { SchemaVisualiser } from "./schemaVisualiser/SchemaVisualiser"; +import { activateView } from "./activateView"; +import { App } from "obsidian"; +import { GLOBAL_TABLES_VIEW_TYPE } from "../globalTables/GlobalTablesView"; +import { GridApi } from "ag-grid-community"; + +type CodeblockProcessorWrapper = ( + el: HTMLElement, + source: string, +) => Promise; + +const DEFAULT_QUERY = "SELECT *\nFROM files\nLIMIT 10"; + +export class Editor { + constructor( + private codeblockProcessorGenerator: CodeblockProcessorWrapper, + private viewPluginGenerator: ViewPluginGeneratorType, + private app: App, + private query: string = DEFAULT_QUERY, + private db: MemoryDatabase | null = null + ) {} + + codeblockElement: HTMLElement | null = null; + render(el: HTMLElement) { + el.empty(); + const menuBar = new EditorMenuBar(!!this.db); + const c = el.createDiv({ cls: "sqlseal-explorer-container" }); + menuBar.render(c); + const grid = c.createDiv({ cls: "sqlseal-explorer-grid-container" }); + const codeSidebar = grid.createDiv({ cls: "sqlseal-explorer-code" }); + codeSidebar.classList.add("cm-sqlseal-explorer"); + // codeSidebar.textContent = "CODE" + const rightPane = grid.createDiv({ cls: 'sqlseal-explorer-right-pane' }) + const contentSidebar = rightPane.createDiv({ cls: "sqlseal-explorer-render" }); + const structure = rightPane.createDiv({ cls: 'sqlseal-explorer-structure' }) + structure.hide() + + this.codeblockElement = contentSidebar; + + this.createEditor(codeSidebar); + this.createCodeblockProcessor(this.codeblockElement, this.query); + + if (this.db) { + // rendering structure + const schema = this.db.getSchema() + const vis = new SchemaVisualiser(schema) + vis.show(structure) + } else { + } + + const events = menuBar.events; + events.on("play", () => { + this.play() + }); + + events.on('structure', (b) => { + if (structure.checkVisibility({ checkVisibilityCSS: true })) { + // structure visible + structure.hide() + contentSidebar.show(); + (b as any).setIcon('database') + + } else { + // structure invisible + structure.show() + contentSidebar.hide(); + (b as any).setIcon('table') + } + }) + + events.on('globals', () => { + activateView(this.app, GLOBAL_TABLES_VIEW_TYPE) + }) + } + + createCodeblockProcessor(el: HTMLElement, source: string) { + return this.codeblockProcessorGenerator(el, source); + } + + editor: EditorView; + createEditor(el: HTMLElement) { + const state = EditorState.create({ + doc: this.query, + extensions: [ + // this.createCustomLanguage(), + // this.createChangeListener(), + this.createKeyBindings(), + this.viewPluginGenerator(true), + EditorView.theme({ + "&": { height: "100%" }, + ".cm-scroller": { fontFamily: "monospace" }, + ".cm-content": { + caretColor: "var(--color-base-100)", + }, + }), + ], + }); + + this.editor = new EditorView({ + state, + parent: el, + }); + } + + createKeyBindings() { + return keymap.of([ + { + key: "Mod-r", // Save shortcut example + run: () => { + this.play() + return true + }, + }, + ]); + } + + async play() { + this.query = this.editor.state.doc.toString(); + if (this.codeblockElement) { + const processor = await this.createCodeblockProcessor(this.codeblockElement, this.query); + // const renderer = processor.renderer + // if ('communicator' in renderer && 'gridApi' in (renderer as any)['communicator']) { + // const api: GridApi = (renderer.communicator as any).gridApi + // api.setGridOption('paginationAutoPageSize', true) + // } + } + } +} diff --git a/src/modules/explorer/EditorMenuBar.ts b/src/modules/explorer/EditorMenuBar.ts new file mode 100644 index 0000000..f9e0ef1 --- /dev/null +++ b/src/modules/explorer/EditorMenuBar.ts @@ -0,0 +1,57 @@ +import { args, BusBuilder } from "@hypersphere/omnibus" +import { ButtonComponent } from "obsidian" + +export class EditorMenuBar { + bus = new BusBuilder() + .register('play', args<[]>()) + .register('structure', args<[ButtonComponent]>()) + .register('globals', args<[]>()) + .build() + constructor(private fileDatabasePreview: boolean = false) { + + } + + strucureButton: ButtonComponent + render(el: HTMLElement) { + const container = el.createDiv({ cls: 'sqlseal-menubar-container' }) + new ButtonComponent(container) + .setIcon('play') + .setClass('sqlseal-menubar-button') + .setTooltip('Run (CMD+R)', { + placement: 'bottom', + delay: 1 + }) + .onClick(() => this.bus.trigger('play')) + + if (this.fileDatabasePreview) { + const b = new ButtonComponent(container) + .setIcon('database') + .setClass('sqlseal-menubar-button') + .setTooltip('Structure', { + placement: 'bottom', + delay: 1 + }) + .onClick(() => { + const but = b + this.bus.trigger('structure', but as any) + }) + } + + container.createDiv({ cls: 'separator' }) + + if (!this.fileDatabasePreview) { + // global button + const b = new ButtonComponent(container) + .setIcon('bolt') + .setClass('sqlseal-menubar-button') + .setTooltip('Global Tables', { delay: 1, placement: 'bottom' }) + .onClick(() => { + this.bus.trigger('globals') + }) + } + } + + get events() { + return this.bus.getRegistrator() + } +} \ No newline at end of file diff --git a/src/modules/explorer/FileDatabaseExplorerView.ts b/src/modules/explorer/FileDatabaseExplorerView.ts new file mode 100644 index 0000000..6510783 --- /dev/null +++ b/src/modules/explorer/FileDatabaseExplorerView.ts @@ -0,0 +1,95 @@ +import { FileView, IconName, MarkdownPostProcessorContext, Menu, TextFileView, TFile, WorkspaceLeaf } from "obsidian"; +import { MemoryDatabase } from "./database/memoryDatabase"; +import { DatabaseManager } from "./database/databaseManager"; +import { TableInfo } from "./schemaVisualiser/TableVisualiser"; +import { SchemaVisualiser } from "./schemaVisualiser/SchemaVisualiser"; +import { ExplorerView } from "./explorer/ExplorerView"; +import { Editor } from "./Editor"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { CodeblockProcessor } from "../editor/codeblockHandler/CodeblockProcessor"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; +import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { Settings } from "../settings/Settings"; +import { Sync } from "../sync/sync/sync"; + +export const FILE_DATABASE_VIEW = 'sqlseal-sqlite-file-view' + +const INITIAL_QUERY = "SELECT name\nFROM sqlite_master\nWHERE type='table'" + +export class FileDatabaseExplorerView extends FileView { + constructor( + leaf: WorkspaceLeaf, + private manager: DatabaseManager, + private viewPluginGenerator: ViewPluginGeneratorType, + private rendererRegistry: RendererRegistry, + private cellParser: ModernCellParser, + private settings: Settings, + private sync: Sync, + + ) { + super(leaf) + } + getViewType(): string { + return FILE_DATABASE_VIEW + } + getDisplayText(): string { + return this.file?.basename || 'Database' + } + + async onOpen() { + } + + onPaneMenu(menu: Menu, source: "more-options" | "tab-header" | string): void { + menu.addItem(i => i.setTitle('test')) + } + + db: MemoryDatabase + async onLoadFile(file: TFile): Promise { + const db = await this.manager.getDatabaseConnection(file) + await db.connect() + this.db = db + + // GETTING ALL TABLES + this.schema = db.getSchema() + this.render() + } + + schema: TableInfo[] + render() { + const codeblockProcessorGenerator = async (el: HTMLElement, source: string) => { + const ctx: MarkdownPostProcessorContext = { + docId: "", + sourcePath: "", + frontmatter: {}, + } as any; + + const processor = new CodeblockProcessor( + el, + source, + ctx, + this.rendererRegistry, + this.db as any, // FIXME + this.cellParser, + this.settings, + this.app, + this.sync, + ); + await processor.onload(); + await processor.render(); + return processor + } + + const editor = new Editor(codeblockProcessorGenerator, this.viewPluginGenerator,this.app, INITIAL_QUERY, this.db) + + editor.render(this.contentEl) + + // const vis = new SchemaVisualiser(this.schema) + // vis.show(container) + + } + + getIcon(): IconName { + return 'database' + } + +} \ No newline at end of file diff --git a/src/modules/explorer/InitFactory.ts b/src/modules/explorer/InitFactory.ts new file mode 100644 index 0000000..d75dcb8 --- /dev/null +++ b/src/modules/explorer/InitFactory.ts @@ -0,0 +1,80 @@ +import { makeInjector } from "@hypersphere/dity"; +import { ExplorerModule } from "./module"; +import { addIcon, App, Plugin, WorkspaceLeaf } from "obsidian"; +import { SqlSealDatabase } from "../database/database"; +import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; +import { Sync } from "../sync/sync/sync"; +import { Settings } from "../settings/Settings"; +import { ExplorerView } from "./explorer/ExplorerView"; +import { ViewPlugin } from "@codemirror/view"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { FILE_DATABASE_VIEW, FileDatabaseExplorerView } from "./FileDatabaseExplorerView"; +import { DatabaseManager } from "./database/databaseManager"; +import { activateView } from "./activateView"; + +// @ts-ignore: Handled by esbuild +import SQLSealIcon from "./sqlseal-bw.svg"; + +@(makeInjector()([ + "plugin", + "app", + "db", + "cellParser", + "rendererRegistry", + "sync", + "settings", + "viewPluginGenerator", + "dbManager" +])) +export class InitFactory { + make( + plugin: Plugin, + app: App, + db: SqlSealDatabase, + cellParser: ModernCellParser, + rendererRegistry: RendererRegistry, + sync: Sync, + settings: Settings, + viewPluginGenerator: ViewPluginGeneratorType, + dbManager: DatabaseManager + ) { + + return () => { + plugin.registerView( + "sqlseal-explorer-view", + (leaf) => + new ExplorerView( + leaf, + rendererRegistry, + db, + cellParser, + settings, + sync, + viewPluginGenerator + ), + ); + addIcon("logo-sqlseal", SQLSealIcon); + plugin.addRibbonIcon("logo-sqlseal", "SQLSeal Explorer", () => + activateView(plugin.app, "sqlseal-explorer-view"), + ); + + + // Register for extenion + plugin.registerView(FILE_DATABASE_VIEW, (leaf) => { + return new FileDatabaseExplorerView(leaf, dbManager, viewPluginGenerator, rendererRegistry, cellParser, settings, sync) + }) + + plugin.registerExtensions(['sqlite'], FILE_DATABASE_VIEW) + + plugin.addCommand({ + id: 'sqlseal-command-explorer', + name: 'Open SQLSeal Explorer', + icon: 'logo-sqlseal', + callback: () => activateView(app, 'sqlseal-explorer-view') + + }) + + }; + } +} diff --git a/src/modules/explorer/activateView.ts b/src/modules/explorer/activateView.ts new file mode 100644 index 0000000..374ad00 --- /dev/null +++ b/src/modules/explorer/activateView.ts @@ -0,0 +1,22 @@ +import { App, WorkspaceLeaf } from "obsidian"; + +export const activateView = async (app: App, name: string) => { + const { workspace } = app; + + let leaf: WorkspaceLeaf | null = null; + const leaves = workspace.getLeavesOfType(name); + + if (leaves.length > 0) { + // A leaf with our view already exists, use that + leaf = leaves[0]; + } else { + leaf = workspace.getLeaf("tab"); + if (!leaf) { + return; + } + await leaf.setViewState({ type: name, active: true }); + } + + // "Reveal" the leaf in case it is in a collapsed sidebar + workspace.revealLeaf(leaf); +}; diff --git a/src/modules/explorer/database/databaseManager.ts b/src/modules/explorer/database/databaseManager.ts new file mode 100644 index 0000000..7fe03d5 --- /dev/null +++ b/src/modules/explorer/database/databaseManager.ts @@ -0,0 +1,32 @@ +import { TFile } from "obsidian"; +import { MemoryDatabase } from "./memoryDatabase"; +import wasmBinary from '../../../../node_modules/@jlongster/sql.js/dist/sql-wasm.wasm' +import initSqlJs from '@jlongster/sql.js'; + +export class DatabaseManager { + constructor() {} + + private sql: initSqlJs.SqlJsStatic | null = null + + private async getConnection() { + if (this.sql) { + return this.sql + } + const SQL = await initSqlJs({ + wasmBinary: wasmBinary, + }); + this.sql = SQL + return SQL + } + + async getDatabaseConnection(file: TFile) { + // FIXME: connecting to database + const connection = await this.getConnection() + + const db = new MemoryDatabase(connection, file); + await db.connect() + return db + } + + getGlobalDatabaseConnection() {} +} diff --git a/src/modules/explorer/database/memoryDatabase.ts b/src/modules/explorer/database/memoryDatabase.ts new file mode 100644 index 0000000..dcfc6ae --- /dev/null +++ b/src/modules/explorer/database/memoryDatabase.ts @@ -0,0 +1,52 @@ +import { TFile } from "obsidian"; +import { BindParams, Database, ParamsObject } from "sql.js"; +import { toObjectArray } from "../../database/worker/database"; +import { TableInfo } from "../schemaVisualiser/TableVisualiser"; + +export class MemoryDatabase { + private db: Database + constructor(private sql: initSqlJs.SqlJsStatic, private file: TFile) { + + } + + async connect() { + const binary = await this.file.vault.readBinary(this.file) + this.db = new this.sql.Database(Buffer.from(binary)) + } + + query(query: string, params: BindParams = null): { data: T[], columns: keyof T } { + const stmt = this.db.prepare(query, params) + const data = toObjectArray(stmt) + const columns = stmt.getColumnNames() + stmt.free() + + return { data: data, columns } as any + } + + select(query: string, params: BindParams = null) { + return this.query(query, params) + } + + explain() { + return {} + } + + allTables() { + return this.query<{name: string}>(`select name from sqlite_master where type='table'`) + } + + getColumns(tableName: string) { + return this.query<{ name: string, type: string }>('select name, type from pragma_table_info(@tableName)', { '@tableName': tableName }) + } + + getSchema(): TableInfo[] { + const tables = this.allTables().data + return tables.map(t => { + const columns = this.getColumns(t.name) + return { + name: t.name, + columns: columns.data + } + }) + } +} \ No newline at end of file diff --git a/src/modules/explorer/explorer/ExplorerView.ts b/src/modules/explorer/explorer/ExplorerView.ts new file mode 100644 index 0000000..fe0907d --- /dev/null +++ b/src/modules/explorer/explorer/ExplorerView.ts @@ -0,0 +1,83 @@ +import { EditorState } from "@codemirror/state"; +import { EditorView, keymap, ViewPlugin } from "@codemirror/view"; +import { + ItemView, + MarkdownPostProcessorContext, + WorkspaceLeaf, +} from "obsidian"; +import { CodeblockProcessor } from "../../editor/codeblockHandler/CodeblockProcessor"; +import { SqlSealDatabase } from "../../database/database"; +import { RendererRegistry } from "../../editor/renderer/rendererRegistry"; +import { CellParser } from "../../../../types-package/dist/src/cellParser"; +import { Settings } from "../../settings/Settings"; +import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser"; +import { Sync } from "../../sync/sync/sync"; +import { Language } from "@codemirror/language"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; +import { Editor } from "../Editor"; +import { GridApi } from "ag-grid-community"; + +export class ExplorerView extends ItemView { + constructor( + leaf: WorkspaceLeaf, + private rendererRegistry: RendererRegistry, + private db: Pick, + private cellParser: ModernCellParser, + private settings: Settings, + private sync: Sync, + private viewPluginGenerator: ViewPluginGeneratorType + ) { + super(leaf); + } + private editor: EditorView; + getViewType() { + return "sqlseal-explorer-view"; + } + getDisplayText() { + return "SQLSeal Explorer"; + } + + getIcon() { + return 'logo-sqlseal' + } + async onOpen() { + const content = this.contentEl; + + + const codeblockProcessorGenerator = async (el: HTMLElement, source: string) => { + const ctx: MarkdownPostProcessorContext = { + docId: "", + sourcePath: "", + frontmatter: {}, + } as any; + + const processor = new CodeblockProcessor( + el, + source, + ctx, + this.rendererRegistry, + this.db, + this.cellParser, + this.settings, + this.app, + this.sync, + ); + await processor.onload(); + + + // Resizing + const renderer = processor.renderer + if ('communicator' in renderer && 'gridApi' in (renderer as any)['communicator']) { + const api: GridApi = (renderer.communicator as any).gridApi + api.setGridOption('paginationAutoPageSize', true) + } + + await processor.render(); + return processor + } + + const editor = new Editor(codeblockProcessorGenerator, this.viewPluginGenerator, this.app) + + editor.render(content) + } +} diff --git a/src/modules/explorer/explorer/style.scss b/src/modules/explorer/explorer/style.scss new file mode 100644 index 0000000..efd8c69 --- /dev/null +++ b/src/modules/explorer/explorer/style.scss @@ -0,0 +1,81 @@ +@use '../schemaVisualiser/schemaVisualiser.scss'; + +.sqlseal-explorer-container { + height: 100%; + width: 100%; + max-height: 100%; + display: grid; + grid-template-rows: auto 1fr; +} + +.sqlseal-explorer-grid-container { + display: grid; + grid-template-columns: 50% 50%; + gap: 1em; + height: 100%; + width: 100%; + padding: 1em; +} + +.sqlseal-explorer-code { + // background: orange; +} + +.sqlseal-explorer-render { + // background: red; + overflow: scroll; +} + +.sqlseal-menubar-container { + padding: 0.2em 0; + display: flex; +} + +.sqlseal-menubar-container .separator { + flex: 1; +} + +.sqlseal-menubar-button { + border: 0; + --input-shadow: none; +} + +.sqlseal-explorer-right-pane { + display: grid; + container-type: inline-size; + height: 100%; +// place-content: center; +// align-content: stretch; +// grid-template-rows: 100%; +// width: 100%; +// height: 100%; + & > * { +// width: 100%; +// height: 100%; + grid-area: 1 / 1; + height: 100%; + } +} + +.sqlseal-explorer-right-pane { + + & .ag-root-wrapper { + height: 100%; + } + + & .ag-root-wrapper-body { + flex: 1; + } + + & .ag-paging-page-size { + display: none; + } + + & .ag-paging-row-summary-panel { + display: none; + } + + & .ag-paging-panel { + justify-content: center; + } +} diff --git a/src/modules/explorer/module.ts b/src/modules/explorer/module.ts new file mode 100644 index 0000000..81cd238 --- /dev/null +++ b/src/modules/explorer/module.ts @@ -0,0 +1,31 @@ +import { asClass, asFactory, buildContainer } from "@hypersphere/dity"; +import { InitFactory } from "./InitFactory"; +import { App, Plugin } from "obsidian"; +import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { SqlSealDatabase } from "../database/database"; +import { Settings } from "../settings/Settings"; +import { Sync } from "../sync/sync/sync"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; +import { ViewPlugin } from "@codemirror/view"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { DatabaseManager } from "./database/databaseManager"; + +export const explorer = buildContainer((c) => + c + .register({ + init: asFactory(InitFactory), + dbManager: asClass(DatabaseManager) + }) + .externals<{ + app: App, + cellParser: ModernCellParser, + db: SqlSealDatabase, + settings: Settings, + sync: Sync + rendererRegistry: RendererRegistry, + plugin: Plugin, + viewPluginGenerator: ViewPluginGeneratorType + }>(), +); + +export type ExplorerModule = typeof explorer; diff --git a/src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts b/src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts new file mode 100644 index 0000000..94c1186 --- /dev/null +++ b/src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts @@ -0,0 +1,14 @@ +import { TableInfo, TableVisualiser } from "./TableVisualiser" + +export class SchemaVisualiser { + constructor(private info: TableInfo[]) { } + + show(container: HTMLElement) { + container.empty() + + const c = container.createDiv({ cls: 'sqlseal-tv-container' }) + this.info + .map(i => new TableVisualiser(i)) + .forEach(v => v.show(c)) + } +} \ No newline at end of file diff --git a/src/modules/explorer/schemaVisualiser/TableVisualiser.ts b/src/modules/explorer/schemaVisualiser/TableVisualiser.ts new file mode 100644 index 0000000..902f0f3 --- /dev/null +++ b/src/modules/explorer/schemaVisualiser/TableVisualiser.ts @@ -0,0 +1,30 @@ +export interface ColumnInfo { + name: string + type: string +} +export interface TableInfo { + name: string + columns: ColumnInfo[] +} + +export class TableVisualiser { + constructor(private info: TableInfo) { + + } + + show(el: HTMLElement) { + const cont = el.createDiv({ cls: 'sqlseal-tv-table' }) + cont.createDiv({ cls: 'sqlseal-tv-table-name', text: this.info.name }) + const columns = cont.createEl('ul', { cls: 'sqlseal-tv-columns' }) + this.info.columns.forEach(info => { + columns.appendChild(this.createColumn(info)) + }) + } + + createColumn(info: ColumnInfo) { + const row = createEl('li') + row.createEl('span', { text: info.name, cls: 'sqlseal-tv-column-name' }) + row.createEl('span', { text: info.type, cls: 'sqlseal-tv-column-class' }) + return row + } +} \ No newline at end of file diff --git a/src/modules/explorer/schemaVisualiser/schemaVisualiser.scss b/src/modules/explorer/schemaVisualiser/schemaVisualiser.scss new file mode 100644 index 0000000..62ebbf3 --- /dev/null +++ b/src/modules/explorer/schemaVisualiser/schemaVisualiser.scss @@ -0,0 +1,29 @@ +.sqlseal-tv-container { + gap: 1em; +} + +.sqlseal-tv-table { + display: inline-block; + border: 1px solid black; + margin-right: 1em; + margin-bottom: 1em; +} + +.sqlseal-tv-table-name { + padding: 0.5em 1em; + background: #DDD; + border-bottom: 2px solid black; +} + +.sqlseal-tv-columns { + list-style: none; + margin: 0; + padding: 0.5em 1em; +} + +.sqlseal-tv-columns li { + display: flex; + flex-direction: row; + gap: 1em; + justify-content: space-between; +} \ No newline at end of file diff --git a/src/modules/globalTables/sqlseal-bw.svg b/src/modules/explorer/sqlseal-bw.svg similarity index 100% rename from src/modules/globalTables/sqlseal-bw.svg rename to src/modules/explorer/sqlseal-bw.svg diff --git a/src/modules/globalTables/GlobalTablesViewRegister.ts b/src/modules/globalTables/GlobalTablesViewRegister.ts index 5176067..ace5382 100644 --- a/src/modules/globalTables/GlobalTablesViewRegister.ts +++ b/src/modules/globalTables/GlobalTablesViewRegister.ts @@ -1,46 +1,27 @@ import { makeInjector } from "@hypersphere/dity"; import { GlobalTablesModule } from "./module"; -import { addIcon, App, Plugin, WorkspaceLeaf } from "obsidian"; +import { App, Plugin, WorkspaceLeaf } from "obsidian"; import { GLOBAL_TABLES_VIEW_TYPE, GlobalTablesView } from "./GlobalTablesView"; -// @ts-ignore: Handled by esbuild -import SQLSealIcon from './sqlseal-bw.svg' import { Sync } from "../sync/sync/sync"; +import { activateView } from "../explorer/activateView"; - -@(makeInjector()( - ['plugin', 'app', 'sync'] -)) +@(makeInjector()(["plugin", "app", "sync"])) export class GlobalTablesViewRegister { - make(plugin: Plugin, app: App, sync: Sync) { + make(plugin: Plugin, app: App, sync: Sync) { - const activateView = async () => { - const { workspace } = plugin.app; + return () => { + plugin.registerView( + GLOBAL_TABLES_VIEW_TYPE, + (leaf) => new GlobalTablesView(leaf, app.vault, sync), + ); - let leaf: WorkspaceLeaf | null = null; - const leaves = workspace.getLeavesOfType(GLOBAL_TABLES_VIEW_TYPE); - - if (leaves.length > 0) { - // A leaf with our view already exists, use that - leaf = leaves[0]; - } else { - leaf = workspace.getLeaf('tab') - if (!leaf) { return } - await leaf.setViewState({ type: GLOBAL_TABLES_VIEW_TYPE, active: true }); - } - - // "Reveal" the leaf in case it is in a collapsed sidebar - workspace.revealLeaf(leaf); - } - - return () => { - plugin.registerView(GLOBAL_TABLES_VIEW_TYPE, leaf => new GlobalTablesView(leaf, app.vault, sync)) - - - // SQLSeal Icon - addIcon('logo-sqlseal', SQLSealIcon) - - plugin.addRibbonIcon('logo-sqlseal', 'SQLSeal Global Tables', activateView) - - } - } -} \ No newline at end of file + plugin.addCommand({ + id: 'sqlseal-command-global-tables', + name: 'Open global tables configuration', + icon: 'logo-sqlseal', + callback: () => activateView(app, GLOBAL_TABLES_VIEW_TYPE) + + }) + }; + } +} diff --git a/src/modules/main/init.ts b/src/modules/main/init.ts index 8cfa8af..4aceb24 100644 --- a/src/modules/main/init.ts +++ b/src/modules/main/init.ts @@ -12,7 +12,8 @@ type InitFn = () => void 'sync.init', 'debug.init', 'api.init', - 'globalTables.init' + 'globalTables.init', + 'explorer.init' ])) export class Init { async make( @@ -23,7 +24,8 @@ export class Init { syncInit: InitFn, debugInit: InitFn, apiInit: InitFn, - globalTablesInit: InitFn + globalTablesInit: InitFn, + explorerInit: InitFn ) { return () => { settingsInit() @@ -34,6 +36,7 @@ export class Init { // debugInit() apiInit() globalTablesInit() + explorerInit() } } } \ No newline at end of file diff --git a/src/modules/main/module.ts b/src/modules/main/module.ts index 7126739..e4e0ab8 100644 --- a/src/modules/main/module.ts +++ b/src/modules/main/module.ts @@ -11,6 +11,7 @@ import { contextMenu } from '../contextMenu/module' import { debugModule } from '../debug/module' import { apiModule } from '../api/module' import { globalTables } from '../globalTables/module' +import { explorer } from '../explorer/module' const obsidian = buildContainer(c => c .externals<{ @@ -32,7 +33,8 @@ export const mainModule = buildContainer(c => c contextMenu, debug: debugModule, api: apiModule, - globalTables + globalTables, + explorer }) .register({ init: asFactory(Init) @@ -59,6 +61,7 @@ export const mainModule = buildContainer(c => c 'settings.app': 'obsidian.app', 'settings.plugin': 'obsidian.plugin', 'settings.cellParser': 'syntaxHighlight.cellParser', + 'settings.viewPluginGenerator': 'syntaxHighlight.viewPluginGenerator' }) .resolve({ 'syntaxHighlight.app': 'obsidian.app', @@ -84,6 +87,16 @@ export const mainModule = buildContainer(c => c 'globalTables.app': 'obsidian.app', 'globalTables.sync': 'sync.syncBus' }) + .resolve({ + 'explorer.app': 'obsidian.app', + 'explorer.cellParser': 'syntaxHighlight.cellParser', + 'explorer.db': 'db.db', + 'explorer.settings': 'settings.settings', + 'explorer.plugin': 'obsidian.plugin', + 'explorer.rendererRegistry': 'editor.rendererRegistry', + 'explorer.sync': 'sync.syncBus', + 'explorer.viewPluginGenerator': 'syntaxHighlight.viewPluginGenerator' + }) ) export type MainModule = typeof mainModule diff --git a/src/modules/settings/SQLSealSettingsTab.ts b/src/modules/settings/SQLSealSettingsTab.ts index 6cfee73..432f903 100644 --- a/src/modules/settings/SQLSealSettingsTab.ts +++ b/src/modules/settings/SQLSealSettingsTab.ts @@ -2,8 +2,6 @@ import { makeInjector } from '@hypersphere/dity'; import { App, PluginSettingTab, Setting, Plugin } from 'obsidian'; import { SettingsModule } from './module'; import { Settings } from './Settings'; -import { SettingsCSVControls } from './settingsTabSection/SettingsCSVControls'; -import { SettingsJsonControls } from './settingsTabSection/SettingsJsonControls'; import { SettingsControls } from './settingsTabSection/SettingsControls'; export interface SQLSealSettings { diff --git a/src/modules/settings/init.ts b/src/modules/settings/init.ts index d961177..7d98168 100644 --- a/src/modules/settings/init.ts +++ b/src/modules/settings/init.ts @@ -5,16 +5,18 @@ import { SQLSealSettingsTab } from "./SQLSealSettingsTab"; import { Settings } from "./Settings"; import { SettingsCSVControls } from "./settingsTabSection/SettingsCSVControls"; import { SettingsJsonControls } from "./settingsTabSection/SettingsJsonControls"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; -@(makeInjector()(["plugin", "settingsTab", "app", "settings"])) +@(makeInjector()(["plugin", "settingsTab", "app", "settings", "viewPluginGenerator"])) export class SettingsInit { async make( plugin: Plugin, settingsTab: SQLSealSettingsTab, app: App, settings: Settings, + viewPluginGenerator: ViewPluginGeneratorType ) { - const csvControl = new SettingsCSVControls(settings, app, plugin); + const csvControl = new SettingsCSVControls(settings, app, plugin, viewPluginGenerator); const jsonControl = new SettingsJsonControls(settings, app, plugin); const controls = [csvControl, jsonControl]; diff --git a/src/modules/settings/modal/showCodeSample.ts b/src/modules/settings/modal/showCodeSample.ts index d51e1c5..4daf87f 100644 --- a/src/modules/settings/modal/showCodeSample.ts +++ b/src/modules/settings/modal/showCodeSample.ts @@ -1,8 +1,18 @@ import { App, Modal, Notice, Setting, TFile } from "obsidian"; import { sanitise } from "../../../utils/sanitiseColumn"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; + +const query = (tableName: string, path: string) => `\`\`\`sqlseal +TABLE ${tableName} = file(${path}) + +SELECT * FROM ${tableName} +LIMIT 100 +\`\`\`` export class CodeSampleModal extends Modal { - constructor(app: App, private file: TFile) { + constructor(app: App, private file: TFile, private viewPluginGenerator: ViewPluginGeneratorType) { super(app); } @@ -10,29 +20,37 @@ export class CodeSampleModal extends Modal { const {contentEl} = this; contentEl.createEl('h2', {text: 'SQLSeal Code'}); - // Add container for code - const textArea = contentEl.createEl('textarea', { - cls: 'sql-seal-modal-code', - attr: { - rows: '10' - } - }); + contentEl.classList.add('sqlseal-modal-copycode') + // Setup actual editor here const tableName = sanitise(this.file.basename) - - textArea.setText(`\`\`\`sqlseal -TABLE ${tableName} = file(${this.file.path}) + const q = query(tableName, this.file.path) -SELECT * FROM ${tableName} -LIMIT 100 -\`\`\``); + const state = EditorState.create({ + doc: q, + extensions: [ + this.viewPluginGenerator(false), + EditorView.theme({ + "&": { height: "100%" }, + ".cm-scroller": { fontFamily: "monospace" }, + ".cm-content": { + caretColor: "var(--color-base-100)", + }, + }), + ] + }) + + new EditorView({ + state, + parent: contentEl.createDiv({ cls: 'cm-sqlseal-overlay' }), + }); // Add copy button new Setting(contentEl) .addButton(button => button .setButtonText('Copy to Clipboard') .onClick(async () => { - await navigator.clipboard.writeText(textArea.getText()); + await navigator.clipboard.writeText(q); new Notice('Copied to clipboard!'); })); } diff --git a/src/modules/settings/module.ts b/src/modules/settings/module.ts index 3980c52..16a90c7 100644 --- a/src/modules/settings/module.ts +++ b/src/modules/settings/module.ts @@ -4,12 +4,14 @@ import { SettingsFactory } from "./settingsFactory"; import { SQLSealSettingsTab } from "./SQLSealSettingsTab"; import { SettingsInit } from "./init"; import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; export const settingsModule = buildContainer(c => c .externals<{ 'plugin': Plugin, 'app': App, - 'cellParser': ModernCellParser + 'cellParser': ModernCellParser, + 'viewPluginGenerator': ViewPluginGeneratorType }>() .register({ 'settings': asFactory(SettingsFactory), diff --git a/src/modules/settings/settingsTabSection/SettingsCSVControls.ts b/src/modules/settings/settingsTabSection/SettingsCSVControls.ts index 96dbe83..9549e65 100644 --- a/src/modules/settings/settingsTabSection/SettingsCSVControls.ts +++ b/src/modules/settings/settingsTabSection/SettingsCSVControls.ts @@ -5,10 +5,15 @@ import { } from "../utils/viewInspector"; import { SettingsControls } from "./SettingsControls"; import { CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE, CSVView } from "../view/CSVView"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; export class SettingsCSVControls extends SettingsControls { private registeredView: string | null = null; + constructor(settings: Settings, app: App, plugin: Plugin, private viewPluginGenerator: ViewPluginGeneratorType) { + super(settings, app, plugin) + } + register() { if (this.settings.get("enableViewer")) { const view = checkTypeViewAvaiability(this.app, CSV_VIEW_EXTENSIONS[0]); @@ -19,7 +24,7 @@ export class SettingsCSVControls extends SettingsControls { this.plugin.registerView( CSV_VIEW_TYPE, - (leaf) => new CSVView(leaf, this.settings), + (leaf) => new CSVView(leaf, this.settings, this.viewPluginGenerator), ); this.plugin.registerExtensions(CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE); } diff --git a/src/modules/settings/view/CSVView.ts b/src/modules/settings/view/CSVView.ts index 2cb02e2..64e0f92 100644 --- a/src/modules/settings/view/CSVView.ts +++ b/src/modules/settings/view/CSVView.ts @@ -14,6 +14,7 @@ import { DeleteConfirmationModal } from "../modal/deleteConfirmationModal"; import { CSVColumnContextMenu } from "../menu/csvColumnContextMenu"; import { AgColumn, Column } from "ag-grid-community"; import { CSVViewMenuBar } from "./CSVViewMenuBar"; +import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator"; const delay = (n: number) => new Promise((resolve) => setTimeout(resolve, n)); @@ -28,6 +29,7 @@ export class CSVView extends TextFileView { constructor( leaf: WorkspaceLeaf, private readonly settings: Settings, + private readonly viewPluginGenerator: ViewPluginGeneratorType ) { super(leaf); } @@ -334,7 +336,7 @@ export class CSVView extends TextFileView { if (!this.file) { return; } - const modal = new CodeSampleModal(this.app, this.file); + const modal = new CodeSampleModal(this.app, this.file, this.viewPluginGenerator); modal.open(); }) diff --git a/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts index b6c23f4..a03b6b0 100644 --- a/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts +++ b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts @@ -17,6 +17,11 @@ import { FilePathWidget } from './widgets/FilePathWidget'; import { RendererRegistry } from '../../editor/renderer/rendererRegistry'; import { SQLSealLangDefinition } from '../../editor/parser'; +interface CodeBlockMatch { + startIndex: number, + content: string +} + const markDecorations = { blockFlag: Decoration.mark({ class: 'cm-sqlseal-block-flag' }), blockQuery: Decoration.mark({ class: 'cm-sqlseal-block-query' }), @@ -39,7 +44,7 @@ export class SQLSealViewPlugin implements PluginValue { private readonly app: App; private readonly renderers: RendererRegistry; - constructor(view: EditorView, app: App, renderers: RendererRegistry) { + constructor(view: EditorView, app: App, renderers: RendererRegistry, private allIsCode: boolean) { this.app = app; this.renderers = renderers; this.decorations = this.buildDecorations(view); @@ -69,59 +74,134 @@ export class SQLSealViewPlugin implements PluginValue { return results } - private buildDecorations(view: EditorView): DecorationSet { - const builder: Array> = []; + private getCodeBlocks(view: EditorView): CodeBlockMatch[] { const text = view.state.doc.toString(); + if (this.allIsCode) { + return [{ + startIndex: 0, + content: text + }] + } + + // Parsing const codeBlockRegex = /```(sqlseal)\n([\s\S]*?)```/g; let match; - + let results: CodeBlockMatch[] = [] while ((match = codeBlockRegex.exec(text)) !== null) { const blockStart = match.index; const langTagEnd = blockStart + match[1].length + 3; const sqlContent = match[2]; const contentStart = langTagEnd + 1; + results.push({ + content: sqlContent, + startIndex: contentStart + }) + } + return results + } + decorateFilename(dec: Decorator, { content, startIndex }: CodeBlockMatch) { + let hasQuotes = false; + // Get the actual filename text from the document + let filePath = content.slice(dec.start, dec.end) - const decorations = this.parseWithGrammar(sqlContent); + // Remove leading & trailing quotes, if captured. + if (filePath.startsWith('"')) { + filePath = filePath.substring(1, filePath.length - 1) + hasQuotes = true; + } - if (decorations) { - decorations.forEach(dec => { - if (dec.type === 'filename') { - let hasQuotes = false; - // Get the actual filename text from the document - let filePath = view.state.doc.sliceString( - contentStart + dec.start, - contentStart + dec.end - ); + // Create widget decoration for the filename + const widget = new FilePathWidget(filePath, this.app); + return Decoration.replace({ + widget, + inclusive: true + }).range( + startIndex + dec.start + Number(hasQuotes), + startIndex + dec.end - Number(hasQuotes) + ) + } - // Remove leading & trailing quotes, if captured. - if (filePath.startsWith('"')) { - filePath = filePath.substring(1, filePath.length - 1) - hasQuotes = true; - } - - // Create widget decoration for the filename - const widget = new FilePathWidget(filePath, this.app); - builder.push(Decoration.replace({ - widget, - inclusive: true - }).range( - contentStart + dec.start + Number(hasQuotes), - contentStart + dec.end - Number(hasQuotes) - )); - } else { - const decoration = markDecorations[dec.type as keyof typeof markDecorations]; + privateDecorateCodeblock(codeblockMatch: CodeBlockMatch): Array> { + const { content, startIndex } = codeblockMatch + const decorations = this.parseWithGrammar(content); + return (decorations || []).flatMap(dec => { + switch (dec.type) { + case 'filename': + return this.decorateFilename(dec, codeblockMatch) + default: + const decoration = markDecorations[dec.type as keyof typeof markDecorations]; if (decoration) { - builder.push(decoration.range( - contentStart + dec.start, - contentStart + dec.end - )); + return decoration.range( + startIndex + dec.start, + startIndex + dec.end + ) + } else { + return [] } } }); - } - } + } - return Decoration.set(builder, true); + private buildDecorations(view: EditorView): DecorationSet { + const builder: Array> = []; + // const text = view.state.doc.toString(); + // const codeBlockRegex = /```(sqlseal)\n([\s\S]*?)```/g; + // let match; + + const results = this.getCodeBlocks(view) + const decorators = results.flatMap(r => this.privateDecorateCodeblock(r)) + + return Decoration.set(decorators, true); + + + // while ((match = codeBlockRegex.exec(text)) !== null) { + // const blockStart = match.index; + // const langTagEnd = blockStart + match[1].length + 3; + // const sqlContent = match[2]; + // const contentStart = langTagEnd + 1; + + + // const decorations = this.parseWithGrammar(sqlContent) + + // if (decorations) { + // decorations.forEach(dec => { + // if (dec.type === 'filename') { + // let hasQuotes = false; + // // Get the actual filename text from the document + // let filePath = view.state.doc.sliceString( + // contentStart + dec.start, + // contentStart + dec.end + // ); + + // // Remove leading & trailing quotes, if captured. + // if (filePath.startsWith('"')) { + // filePath = filePath.substring(1, filePath.length - 1) + // hasQuotes = true; + // } + + // // Create widget decoration for the filename + // const widget = new FilePathWidget(filePath, this.app); + // builder.push(Decoration.replace({ + // widget, + // inclusive: true + // }).range( + // contentStart + dec.start + Number(hasQuotes), + // contentStart + dec.end - Number(hasQuotes) + // )); + // } else { + // const decoration = markDecorations[dec.type as keyof typeof markDecorations]; + // if (decoration) { + // builder.push(decoration.range( + // contentStart + dec.start, + // contentStart + dec.end + // )); + // } + // } + // }); + // } + // } + + // return Decoration.set(builder, true); } } \ No newline at end of file diff --git a/src/modules/syntaxHighlight/init.ts b/src/modules/syntaxHighlight/init.ts index a85ed11..aa84e32 100644 --- a/src/modules/syntaxHighlight/init.ts +++ b/src/modules/syntaxHighlight/init.ts @@ -6,18 +6,13 @@ import { RendererRegistry } from "../editor/renderer/rendererRegistry"; import { SQLSealViewPlugin } from "./editorExtension/syntaxHighlight"; @(makeInjector()([ - 'app', 'rendererRegistry', 'plugin' + 'plugin', 'viewPluginGenerator' ])) export class SyntaxHighlightInit { - make(app: App, rendererRegistry: RendererRegistry, plugin: Plugin) { + make(plugin: Plugin, viewPluginGenerator: () => ViewPlugin) { return () => { // FIXME: settings here. - plugin.registerEditorExtension([ - ViewPlugin.define( - (view: EditorView) => new SQLSealViewPlugin(view, app, rendererRegistry), - { decorations: v => v.decorations } - ) - ]); + plugin.registerEditorExtension([viewPluginGenerator()]); } } } \ No newline at end of file diff --git a/src/modules/syntaxHighlight/module.ts b/src/modules/syntaxHighlight/module.ts index 6fad2f5..2e3ef05 100644 --- a/src/modules/syntaxHighlight/module.ts +++ b/src/modules/syntaxHighlight/module.ts @@ -4,6 +4,7 @@ import { App, Plugin } from "obsidian"; import { RendererRegistry } from "../editor/renderer/rendererRegistry"; import { CellParserFactory } from "./cellParser/factory"; import { SqlSealDatabase } from "../database/database"; +import { ViewPluginGenerator } from "./viewPluginGenerator"; export const syntaxHighlight = buildContainer((c) => c @@ -16,8 +17,9 @@ export const syntaxHighlight = buildContainer((c) => .register({ init: asFactory(SyntaxHighlightInit), cellParser: asFactory(CellParserFactory), + viewPluginGenerator: asFactory(ViewPluginGenerator) }) - .exports("init", "cellParser"), + .exports("init", "cellParser", "viewPluginGenerator"), ); export type SyntaxHighlightModule = typeof syntaxHighlight; diff --git a/src/modules/syntaxHighlight/viewPluginGenerator.ts b/src/modules/syntaxHighlight/viewPluginGenerator.ts new file mode 100644 index 0000000..35b1193 --- /dev/null +++ b/src/modules/syntaxHighlight/viewPluginGenerator.ts @@ -0,0 +1,22 @@ +import { makeInjector } from "@hypersphere/dity"; +import { SyntaxHighlightModule } from "./module"; +import { EditorView, ViewPlugin } from "@codemirror/view"; +import { SQLSealViewPlugin } from "./editorExtension/syntaxHighlight"; +import { App } from "obsidian"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; + +export type ViewPluginGeneratorType = ReturnType + +@(makeInjector()([ + 'app', 'rendererRegistry' +])) +export class ViewPluginGenerator { + make(app: App, rendererRegistry: RendererRegistry) { + return (allIsCode: boolean = false) => { + return ViewPlugin.define( + (view: EditorView) => new SQLSealViewPlugin(view, app, rendererRegistry, allIsCode), + { decorations: v => v.decorations } + ) + } + } +} \ No newline at end of file diff --git a/src/styles/main.scss b/src/styles/main.scss index 3f47e7d..357ab1e 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -9,3 +9,5 @@ @use 'canvas'; @use 'autocomplete'; @use 'global-tables'; + +@use '../modules/explorer/explorer/style.scss'; \ No newline at end of file diff --git a/src/styles/syntaxHighlight.scss b/src/styles/syntaxHighlight.scss index af42d9a..9951b9b 100644 --- a/src/styles/syntaxHighlight.scss +++ b/src/styles/syntaxHighlight.scss @@ -97,7 +97,7 @@ color: #6ff77a; } -:is(.cm-sqlseal-block-query, .cm-sqlseal-block-view, .cm-sqlseal-block-flag, .cm-sqlseal-block-table)::before { +:is(.cm-sqlseal-block-query, .cm-sqlseal-block-view, .cm-sqlseal-block-flag, .cm-sqlseal-block-table, .cm-sqlseal-explorer)::before { background-color: var(--color); width: 5px; top: -3px; @@ -111,3 +111,39 @@ .cm-indent + :is(.cm-sqlseal-block-query, .cm-sqlseal-block-view, .cm-sqlseal-block-flag, .cm-sqlseal-block-table)::before { display: none; } + +.cm-sqlseal-explorer .cm-line { + position: relative; +} + +.cm-sqlseal-overlay { + border: 1px solid #AAA; + padding: 1em; + border-radius: 5px; +} + +.cm-sqlseal-overlay .cm-line { + position: relative; +} + +.cm-sqlseal-overlay .cm-editor { + outline: none; +} + +.sqlseal-modal-copycode .setting-item { + border: 0; +} + +.cm-sqlseal-explorer :is(.cm-sqlseal-block-query, .cm-sqlseal-block-view, .cm-sqlseal-block-flag, .cm-sqlseal-block-table)::before { + left: -3px; +} + +.cm-sqlseal-explorer .cm-focused { + outline: none; +} + +.cm-sqlseal-explorer .cm-editor { + border-left-width: 0; + background: var(--color-base-20); + padding: 8px; +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..4134ea6 --- /dev/null +++ b/styles.css @@ -0,0 +1,2 @@ +.sqlseal-table-container{overflow-y:scroll}.sqlseal-table-container table{width:100%;border-collapse:collapse}.sqlseal-grid-wrapper{height:auto;position:relative}.block-language-sqlseal{overflow-y:auto}.sqlseal-error{padding:1em;background:#b80f0f;color:#fff}.sqlseal-notice{padding:1em;background:#1f3f98;color:#fff}.sqlseal-grid-error-message{box-sizing:border-box;width:80%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;background:#b80f0f;font-size:.8em;color:#fff;padding:.5em 2em}.sqlseal-grid-error-message-overlay{content:"";position:absolute;inset:0;background:#fffc;z-index:5}.sqlseal-grid-error-message-overlay.hidden{display:none}.edit-block-button{z-index:1000}.sqlseal-markdown-table{font-family:monospace;white-space:pre-wrap}.sqlseal-markdown-table.no-wrap{display:inline-block;overflow-x:scroll;width:fit-content}.sqlseal-parse-error{background:#b80f0f;color:#fff;padding:.2em;border-radius:2px}.sqlseal-notice-error{color:#f26464}.sqlseal-notice-error:before{content:"";width:16px;height:16px;display:inline-block;margin-right:8px;transform:translateY(3px);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ff0000' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E")}.sql-seal-csv-editor{display:flex;flex-direction:column;height:100%;gap:var(--size-4-4)}.sql-seal-csv-viewer{flex:1;display:flex;flex-direction:column}.sqlseal-grid-wrapper{height:100%;flex:1;display:flex;flex-direction:column}.ag-theme-quartz{flex:1}.sql-seal-csv-viewer-buttons{display:flex;gap:var(--size-4-4)}.sql-seal-modal-code{width:100%;font-family:monospace;padding:8px}.sqlseal-inline-result{display:inline-block;padding:0 4px;margin:0 2px;background-color:var(--background-secondary);border-radius:4px;font-size:.9em;cursor:pointer}.sqlseal-inline-result .error{color:var(--text-error);font-style:italic}.sqlseal-list-element-single .sqlseal-column-name{display:none}.sqlseal-list-element-single .sqlseal-column-name:after{content:": "}.sqlseal-list.show-column-names .sqlseal-list-element-single .sqlseal-column-name{display:inline}.sqlseal-element-inline-list{display:inline-block}.cm-sqlseal-block-query{--color: #297f30}.cm-sqlseal-block-view{--color: #c834dd}.cm-sqlseal-block-flag{--color: #d08306;font-weight:700;color:var(--color)}.cm-sqlseal-block-table{--color: #4985af}.cm-sqlseal-identifier{color:#2d7b33}.cm-sqlseal-function,.cm-sqlseal-parameter{color:#2c0e9b}.cm-sqlseal-comment{color:#7a7a7a;font-style:italic}.cm-sqlseal-keyword{color:var(--color);font-weight:700}.cm-sqlseal-literal{color:#03f}.cm-sqlseal-error{color:#eb2f2f;font-style:italic}.cm-sqlseal-template-keyword{color:#e3ab53}.theme-dark .cm-sqlseal-template-keyword{color:#dd8e10}.theme-dark .cm-sqlseal-block-error{color:red}.theme-dark .cm-sqlseal-block-query{--color: #5eff6a}.theme-dark .cm-sqlseal-comment{color:#898787}.theme-dark .cm-sqlseal-parameter{color:#95ed22}.theme-dark .cm-sqlseal-literal{color:#ff0}.theme-dark .cm-sqlseal-block-view{--color: #e945ff}.theme-dark .cm-sqlseal-block-flag{--color: #e3ab53}.theme-dark .cm-sqlseal-block-table{--color: #56b5fa}.theme-dark .cm-sqlseal-function{color:#c4b8ef}.theme-dark .cm-sqlseal-identifier{color:#6ff77a}:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table,.cm-sqlseal-explorer):before{background-color:var(--color);width:5px;top:-3px;bottom:-3px;left:0;content:"";display:block;position:absolute}.cm-indent+:is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{display:none}.cm-sqlseal-explorer .cm-line{position:relative}.cm-sqlseal-overlay{border:1px solid #AAA;padding:1em;border-radius:5px}.cm-sqlseal-overlay .cm-line{position:relative}.cm-sqlseal-overlay .cm-editor{outline:none}.sqlseal-modal-copycode .setting-item{border:0}.cm-sqlseal-explorer :is(.cm-sqlseal-block-query,.cm-sqlseal-block-view,.cm-sqlseal-block-flag,.cm-sqlseal-block-table):before{left:-3px}.cm-sqlseal-explorer .cm-focused{outline:none}.cm-sqlseal-explorer .cm-editor{border-left-width:0;background:var(--color-base-20);padding:8px}.markdown-source-view.mod-cm6.is-readable-line-width .cm-contentContainer.cm-contentContainer>.cm-content>:has(>.block-language-sqlseal table.dataview){width:var(--container-dataview-table-width);max-width:var(--container-table-max-width)}.ag-checkbox-input{opacity:1!important}.sqlseal-hidden-column{opacity:.5}.sqlseal-settings-warn{background:#ffa600;padding:16px;border:2px solid rgb(134,87,0);border-radius:5px;margin:.5em 2em}.sqlseal-autocomplete-container{position:relative;width:100%}.sqlseal-autocomplete-container input[type=text]{width:100%}.sqlseal-autocomplete-dropdown{position:absolute;top:100%;left:0;margin-top:.5em;right:0;text-align:left;background:#fff;max-height:200px;overflow-y:scroll;border-radius:5px;box-shadow:0 3px 5px #ccc}.modal-overflow{overflow:visible}.sqlseal-dropdown-el{padding:4px 16px 4px 8px;border-top:1px solid #EEE;overflow:hidden}.sqlseal-dropdown-el:first-child{border-top:0}.sqlseal-dropdown-el:hover{background:#a8c4ef}.sqlseal-dropdown-el-name{font-weight:700;font-size:.9em;text-overflow:ellipsis;max-width:100%;overflow:hidden;text-wrap:nowrap}.sqlseal-dropdown-el-path{opacity:.9;font-size:.7em;text-overflow:ellipsis;max-width:100%;overflow:hidden;text-wrap:nowrap}.sqlseal-global-tables-container{height:100%;display:flex;flex-direction:column;gap:1em}.sqlseal-tv-container{gap:1em}.sqlseal-tv-table{display:inline-block;border:1px solid black;margin-right:1em;margin-bottom:1em}.sqlseal-tv-table-name{padding:.5em 1em;background:#ddd;border-bottom:2px solid black}.sqlseal-tv-columns{list-style:none;margin:0;padding:.5em 1em}.sqlseal-tv-columns li{display:flex;flex-direction:row;gap:1em;justify-content:space-between}.sqlseal-explorer-container{height:100%;width:100%;max-height:100%;display:grid;grid-template-rows:auto 1fr}.sqlseal-explorer-grid-container{display:grid;grid-template-columns:50% 50%;gap:1em;height:100%;width:100%;padding:1em}.sqlseal-explorer-render{overflow:scroll}.sqlseal-menubar-container{padding:.2em 0;display:flex}.sqlseal-menubar-container .separator{flex:1}.sqlseal-menubar-button{border:0;--input-shadow: none}.sqlseal-explorer-right-pane{display:grid;container-type:inline-size;height:100%}.sqlseal-explorer-right-pane>*{grid-area:1/1;height:100%}.sqlseal-explorer-right-pane .ag-root-wrapper{height:100%}.sqlseal-explorer-right-pane .ag-root-wrapper-body{flex:1}.sqlseal-explorer-right-pane .ag-paging-page-size,.sqlseal-explorer-right-pane .ag-paging-row-summary-panel{display:none}.sqlseal-explorer-right-pane .ag-paging-panel{justify-content:center} +/*# sourceMappingURL=styles.css.map */