feat: adding inline codeblocks support

This commit is contained in:
Kacper Kula 2025-01-09 20:12:39 +00:00
parent d0a76c1daa
commit 3c833abbff
7 changed files with 289 additions and 6 deletions

View file

@ -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.

View file

@ -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
}
}

View file

@ -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())
}
}
}

View file

@ -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<Decoration>();
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;
}
}

View file

@ -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

View file

@ -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()
}
}

View file

@ -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;
}