From 3c833abbff34bf49e799a6e256c03b537c00c718 Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Thu, 9 Jan 2025 20:12:39 +0000 Subject: [PATCH] feat: adding inline codeblocks support --- docs/inline-codeblocks.md | 2 + .../inline/InlineCodeHandler.ts | 32 +++++ .../inline/InlineProcessor.ts | 71 ++++++++++ src/editorExtension/inlineCodeBlock.ts | 124 ++++++++++++++++++ src/main.ts | 33 ++++- src/sqlSeal.ts | 16 ++- styles.css | 17 +++ 7 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 docs/inline-codeblocks.md create mode 100644 src/codeblockHandler/inline/InlineCodeHandler.ts create mode 100644 src/codeblockHandler/inline/InlineProcessor.ts create mode 100644 src/editorExtension/inlineCodeBlock.ts diff --git a/docs/inline-codeblocks.md b/docs/inline-codeblocks.md new file mode 100644 index 0000000..f27fe99 --- /dev/null +++ b/docs/inline-codeblocks.md @@ -0,0 +1,2 @@ +# Inline Codeblocks +SQLSeal supports inline codeblocks. This allows you to put data inline in your note as if it's part of the sentence. \ No newline at end of file diff --git a/src/codeblockHandler/inline/InlineCodeHandler.ts b/src/codeblockHandler/inline/InlineCodeHandler.ts new file mode 100644 index 0000000..078ba43 --- /dev/null +++ b/src/codeblockHandler/inline/InlineCodeHandler.ts @@ -0,0 +1,32 @@ +import { App, MarkdownPostProcessorContext } from "obsidian"; +import { InlineProcessor } from "./InlineProcessor"; +import { SqlSealDatabase } from "src/database/database"; +import { Sync } from "src/datamodel/sync"; + +export class SqlSealInlineHandler { + constructor( + private readonly app: App, + private readonly db: SqlSealDatabase, + private sync: Sync + ) { } + + getHandler() { + return async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { + ctx.addChild(this.instantiateProcessor(source, el, ctx.sourcePath)); + }; + } + + instantiateProcessor(source: string, el: HTMLElement, sourcePath: string) { + const query = source.replace(/^S>\s*/, "").trim(); + const processor = new InlineProcessor( + el, + query, + sourcePath, + this.db, + this.app, + this.sync + ); + + return processor + } +} \ No newline at end of file diff --git a/src/codeblockHandler/inline/InlineProcessor.ts b/src/codeblockHandler/inline/InlineProcessor.ts new file mode 100644 index 0000000..8060d30 --- /dev/null +++ b/src/codeblockHandler/inline/InlineProcessor.ts @@ -0,0 +1,71 @@ +import { OmnibusRegistrator } from "@hypersphere/omnibus"; +import { App, MarkdownPostProcessorContext, MarkdownRenderChild } from "obsidian"; +import { SqlSealDatabase } from "src/database/database"; +import { Sync } from "src/datamodel/sync"; +import { RendererRegistry, RenderReturn } from "src/renderer/rendererRegistry"; +import { transformQuery } from "src/sql/transformer"; +import { displayError } from "src/utils/ui"; + +export class InlineProcessor extends MarkdownRenderChild { + private registrator: OmnibusRegistrator; + private renderer: RenderReturn; + + constructor( + private el: HTMLElement, + private query: string, + private sourcePath: string, + private db: SqlSealDatabase, + private app: App, + private sync: Sync + ) { + super(el); + this.registrator = this.sync.getRegistrator(); + } + + async onload() { + try { + await this.render(); + } catch (e) { + displayError(this.el, e.toString()); + } + } + + onunload() { + this.registrator.offAll(); + } + + async render() { + try { + const registeredTablesForContext = await this.sync.getTablesMappingForContext(this.sourcePath); + const transformedQuery = transformQuery(this.query, registeredTablesForContext); + + this.registrator.offAll(); + Object.values(registeredTablesForContext).forEach(v => { + this.registrator.on(`change::${v}`, () => { + this.render(); + }); + }); + + this.registrator.on('file::change::' + this.sourcePath, () => { + setTimeout(() => this.render(), 250); + }); + + const file = this.app.vault.getFileByPath(this.sourcePath); + if (!file) { + return; + } + + const fileCache = this.app.metadataCache.getFileCache(file); + const { data, columns } = await this.db.select( + transformedQuery, + fileCache?.frontmatter ?? {} + ); + + this.el.empty() + this.el.createSpan({ text: data[0][columns[0]] ?? '' }) + + } catch (e) { + displayError(this.el, e.toString()) + } + } +} \ No newline at end of file diff --git a/src/editorExtension/inlineCodeBlock.ts b/src/editorExtension/inlineCodeBlock.ts new file mode 100644 index 0000000..60b7901 --- /dev/null +++ b/src/editorExtension/inlineCodeBlock.ts @@ -0,0 +1,124 @@ +import { EditorView, ViewPlugin, ViewUpdate, DecorationSet, Decoration, WidgetType } from "@codemirror/view"; +import { RangeSetBuilder } from "@codemirror/state"; +import { syntaxTree } from "@codemirror/language"; +import { App } from "obsidian"; +import { SqlSealDatabase } from "../database/database"; +import { RendererRegistry } from "../renderer/rendererRegistry"; +import { Sync } from "../datamodel/sync"; +import { SqlSealInlineHandler } from "src/codeblockHandler/inline/InlineCodeHandler"; +import { InlineProcessor } from "src/codeblockHandler/inline/InlineProcessor"; + +export function createSqlSealEditorExtension( + app: App, + db: SqlSealDatabase, + sync: Sync, +) { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + inlineHandler: SqlSealInlineHandler; + + constructor(view: EditorView) { + this.inlineHandler = new SqlSealInlineHandler(app, db, sync); + this.decorations = this.buildDecorations(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged || update.selectionSet) { + this.decorations = this.buildDecorations(update.view); + } + } + + buildDecorations(view: EditorView) { + const builder = new RangeSetBuilder(); + const tree = syntaxTree(view.state); + + tree.iterate({ + enter: ({ type, from, to }) => { + if (type.name.includes("inline-code")) { + const text = view.state.doc.sliceString(from, to); + if (text.startsWith('S>')) { + // Check if we're currently editing this specific inline code + const isEditing = view.hasFocus && + view.state.selection.ranges.some(range => + range.from >= from && range.to <= to); + + if (!isEditing) { + // Create a replacement decoration + builder.add(from, to, Decoration.replace({ + widget: new SqlSealInlineWidget( + text, + this.inlineHandler, + view.state.doc.lineAt(from).number, + app + ) + })); + } + } + } + } + }); + + return builder.finish(); + } + }, + { + decorations: (v) => v.decorations + } + ); +} + +class SqlSealInlineWidget extends WidgetType { + constructor( + private query: string, + private handler: SqlSealInlineHandler, + private line: number, + private app: App + ) { + super(); + } + + eq(other: SqlSealInlineWidget): boolean { + return ( + other.query === this.query && + other.line === this.line + ); + } + + processor: InlineProcessor + + toDOM(): HTMLElement { + const container = document.createElement('span'); + container.classList.add('sqlseal-inline-result'); + + // Create a new context for the inline query + const ctx = { + sourcePath: this.app.workspace.getActiveFile()?.path ?? '', + addChild: () => {}, + getSectionInfo: () => ({ + lineStart: this.line, + lineEnd: this.line + }) + }; + + // Show the original query on hover + container.setAttribute('aria-label', this.query); + container.classList.add('has-tooltip'); + + this.processor = this.handler.instantiateProcessor(this.query, container, ctx.sourcePath) + + this.processor.load() + return container; + } + + destroy(dom: HTMLElement): void { + if (this.processor) { + this.processor.unload() + } + } + + ignoreEvent(event: Event): boolean { + // Return false to allow events to propagate (important for editing) + return false; + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 67cc3f5..903287c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import { FilesFileSyncTable } from 'src/vaultSync/tables/filesTable'; import { TagsFileSyncTable } from 'src/vaultSync/tables/tagsTable'; import { TasksFileSyncTable } from 'src/vaultSync/tables/tasksTable'; import { CSV_VIEW_TYPE, CSVView } from 'src/view/CSVView'; +import { createSqlSealEditorExtension } from './editorExtension/inlineCodeBlock'; const GLOBAL_KEY = 'sqlSealApi' @@ -43,7 +44,7 @@ export default class SqlSealPlugin extends Plugin { // start syncing when files are loaded this.app.workspace.onLayoutReady(() => { - sqlSeal.db.connect().then(() => { + sqlSeal.db.connect().then(async () => { this.fileSync = new SealFileSync(this.app, this) @@ -51,8 +52,9 @@ export default class SqlSealPlugin extends Plugin { this.fileSync.addTablePlugin(new TagsFileSyncTable(sqlSeal.db, this.app)) this.fileSync.addTablePlugin(new TasksFileSyncTable(sqlSeal.db, this.app)) - this.fileSync.init() + await this.fileSync.init() this.registerMarkdownCodeBlockProcessor("sqlseal", sqlSeal.getHandler()) + this.registerInlineCodeblocks() }) }) @@ -63,6 +65,33 @@ export default class SqlSealPlugin extends Plugin { ); } + private registerInlineCodeblocks() { + + // Extension for Live Preview + const editorExtension = createSqlSealEditorExtension( + this.app, + this.sqlSeal.db, + this.sqlSeal.sync, + ); + this.registerEditorExtension(editorExtension); + + // Extension for Read mode + this.registerMarkdownPostProcessor((el, ctx) => { + const inlineCodeBlocks = el.querySelectorAll('code'); + inlineCodeBlocks.forEach((node: HTMLSpanElement) => { + const text = node.innerText; + if (text.startsWith('S>')) { + const container = createEl('span', { cls: 'sqlseal-inline-result' }); + container.setAttribute('aria-label', text.slice(3)); + container.classList.add('has-tooltip'); + node.replaceWith(container); + this.sqlSeal.getInlineHandler()(text, container, ctx); + } + }); + }); + + } + private addCSVCreatorMenuItem(menu: Menu, file: TAbstractFile) { menu.addItem((item) => { item diff --git a/src/sqlSeal.ts b/src/sqlSeal.ts index a83e3d6..efe41dc 100644 --- a/src/sqlSeal.ts +++ b/src/sqlSeal.ts @@ -4,21 +4,29 @@ import { SqlSealCodeblockHandler } from "./codeblockHandler/SqlSealCodeblockHand import { Logger } from "./utils/logger"; import { RendererRegistry } from "./renderer/rendererRegistry"; import { Sync } from "./datamodel/sync"; +import { SqlSealInlineHandler } from "./codeblockHandler/inline/InlineCodeHandler"; export class SqlSeal { public db: SqlSealDatabase - public codeBlockHandler: SqlSealCodeblockHandler + private codeBlockHandler: SqlSealCodeblockHandler + private inlineCodeBlock: SqlSealInlineHandler + public sync: Sync constructor(private readonly app: App, verbose = false, rendererRegistry: RendererRegistry) { this.db = new SqlSealDatabase(app, verbose) const logger = new Logger(verbose) - const sync = new Sync(this.db, this.app.vault) - sync.init() + this.sync = new Sync(this.db, this.app.vault) + this.sync.init() - this.codeBlockHandler = new SqlSealCodeblockHandler(app, this.db, sync, rendererRegistry) + this.codeBlockHandler = new SqlSealCodeblockHandler(app, this.db, this.sync, rendererRegistry) + this.inlineCodeBlock = new SqlSealInlineHandler(app, this.db, this.sync) } getHandler() { return this.codeBlockHandler.getHandler() } + + getInlineHandler() { + return this.inlineCodeBlock.getHandler() + } } diff --git a/styles.css b/styles.css index 053fc9d..c8802c3 100644 --- a/styles.css +++ b/styles.css @@ -104,4 +104,21 @@ width: 100%; font-family: monospace; padding: 8px; +} + + +/* INLINE QUERIES */ +.sqlseal-inline-result { + display: inline-block; + padding: 0 4px; + margin: 0 2px; + background-color: var(--background-secondary); + border-radius: 4px; + font-size: 0.9em; + cursor: pointer; +} + +.sqlseal-inline-result .error { + color: var(--text-error); + font-style: italic; } \ No newline at end of file