mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
Feat/sql explorer (#174)
* chore: reworking plugin internals into modules * feat: settings module is now working, added debug module, enabled new api module * chore: restoring external API * feat: refactoring project to use proper inversion of control * feat: plugins can now be loaded in any order * chore: final file migration, all typescript is in modules now * chore: fixing dependencies of cellParser * feat: csv and json views are only registered when they are not colliding with other plugins * feat: fixed library behaviour on canvas * feat: added ability to hide columns from csv files * feat: adding global tables support (wip) * feat: global tables full implementation * feat: sqlite databases can now be previewed in explorer * feat: improved explorer view * feat: highlighting code in the copy code modal * chore: fixing typechecks * chore: adding missing changeset
This commit is contained in:
parent
2bfd2060d7
commit
531d486d06
35 changed files with 1034 additions and 114 deletions
5
.changeset/dirty-sites-study.md
Normal file
5
.changeset/dirty-sites-study.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"sqlseal": minor
|
||||
---
|
||||
|
||||
Added SQLSeal Explorer that makes it easy to work on new queries
|
||||
5
.changeset/wicked-lights-burn.md
Normal file
5
.changeset/wicked-lights-burn.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"sqlseal": minor
|
||||
---
|
||||
|
||||
sqlite databases can now be previewed using explorer view
|
||||
5
.changeset/witty-laws-end.md
Normal file
5
.changeset/witty-laws-end.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"sqlseal": patch
|
||||
---
|
||||
|
||||
highlighting code in the copy modal
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -26,11 +26,12 @@ export class CodeblockProcessor extends MarkdownRenderChild {
|
|||
private source: string,
|
||||
private ctx: MarkdownPostProcessorContext,
|
||||
private rendererRegistry: RendererRegistry,
|
||||
private db: SqlSealDatabase,
|
||||
private db: Pick<SqlSealDatabase, 'select' | 'explain'>,
|
||||
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) {
|
||||
|
|
|
|||
134
src/modules/explorer/Editor.ts
Normal file
134
src/modules/explorer/Editor.ts
Normal file
|
|
@ -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<CodeblockProcessor>;
|
||||
|
||||
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)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/modules/explorer/EditorMenuBar.ts
Normal file
57
src/modules/explorer/EditorMenuBar.ts
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
95
src/modules/explorer/FileDatabaseExplorerView.ts
Normal file
95
src/modules/explorer/FileDatabaseExplorerView.ts
Normal file
|
|
@ -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<void> {
|
||||
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'
|
||||
}
|
||||
|
||||
}
|
||||
80
src/modules/explorer/InitFactory.ts
Normal file
80
src/modules/explorer/InitFactory.ts
Normal file
|
|
@ -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<ExplorerModule, "factory">()([
|
||||
"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')
|
||||
|
||||
})
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
22
src/modules/explorer/activateView.ts
Normal file
22
src/modules/explorer/activateView.ts
Normal file
|
|
@ -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);
|
||||
};
|
||||
32
src/modules/explorer/database/databaseManager.ts
Normal file
32
src/modules/explorer/database/databaseManager.ts
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
52
src/modules/explorer/database/memoryDatabase.ts
Normal file
52
src/modules/explorer/database/memoryDatabase.ts
Normal file
|
|
@ -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<T = ParamsObject>(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<T = ParamsObject>(query: string, params: BindParams = null) {
|
||||
return this.query<T>(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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
83
src/modules/explorer/explorer/ExplorerView.ts
Normal file
83
src/modules/explorer/explorer/ExplorerView.ts
Normal file
|
|
@ -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<SqlSealDatabase, 'select' | 'explain'>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
81
src/modules/explorer/explorer/style.scss
Normal file
81
src/modules/explorer/explorer/style.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
31
src/modules/explorer/module.ts
Normal file
31
src/modules/explorer/module.ts
Normal file
|
|
@ -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;
|
||||
14
src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts
Normal file
14
src/modules/explorer/schemaVisualiser/SchemaVisualiser.ts
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
30
src/modules/explorer/schemaVisualiser/TableVisualiser.ts
Normal file
30
src/modules/explorer/schemaVisualiser/TableVisualiser.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
29
src/modules/explorer/schemaVisualiser/schemaVisualiser.scss
Normal file
29
src/modules/explorer/schemaVisualiser/schemaVisualiser.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
|
|
@ -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<GlobalTablesModule, 'factory'>()(
|
||||
['plugin', 'app', 'sync']
|
||||
))
|
||||
@(makeInjector<GlobalTablesModule, "factory">()(["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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.addCommand({
|
||||
id: 'sqlseal-command-global-tables',
|
||||
name: 'Open global tables configuration',
|
||||
icon: 'logo-sqlseal',
|
||||
callback: () => activateView(app, GLOBAL_TABLES_VIEW_TYPE)
|
||||
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<SettingsModule>()(["plugin", "settingsTab", "app", "settings"]))
|
||||
@(makeInjector<SettingsModule>()(["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];
|
||||
|
|
|
|||
|
|
@ -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!');
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Range<Decoration>> = [];
|
||||
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<Range<Decoration>> {
|
||||
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<Range<Decoration>> = [];
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,18 +6,13 @@ import { RendererRegistry } from "../editor/renderer/rendererRegistry";
|
|||
import { SQLSealViewPlugin } from "./editorExtension/syntaxHighlight";
|
||||
|
||||
@(makeInjector<SyntaxHighlightModule, 'factory'>()([
|
||||
'app', 'rendererRegistry', 'plugin'
|
||||
'plugin', 'viewPluginGenerator'
|
||||
]))
|
||||
export class SyntaxHighlightInit {
|
||||
make(app: App, rendererRegistry: RendererRegistry, plugin: Plugin) {
|
||||
make(plugin: Plugin, viewPluginGenerator: () => ViewPlugin<any>) {
|
||||
return () => {
|
||||
// FIXME: settings here.
|
||||
plugin.registerEditorExtension([
|
||||
ViewPlugin.define(
|
||||
(view: EditorView) => new SQLSealViewPlugin(view, app, rendererRegistry),
|
||||
{ decorations: v => v.decorations }
|
||||
)
|
||||
]);
|
||||
plugin.registerEditorExtension([viewPluginGenerator()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
22
src/modules/syntaxHighlight/viewPluginGenerator.ts
Normal file
22
src/modules/syntaxHighlight/viewPluginGenerator.ts
Normal file
|
|
@ -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<ViewPluginGenerator['make']>
|
||||
|
||||
@(makeInjector<SyntaxHighlightModule, 'factory'>()([
|
||||
'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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,3 +9,5 @@
|
|||
@use 'canvas';
|
||||
@use 'autocomplete';
|
||||
@use 'global-tables';
|
||||
|
||||
@use '../modules/explorer/explorer/style.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;
|
||||
}
|
||||
2
styles.css
Normal file
2
styles.css
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue