2026-01-15 11:34:29 +01:00
|
|
|
import { Notice, Plugin, TFile } from "obsidian";
|
|
|
|
|
import { DEFAULT_SETTINGS, type MindnetSettings, normalizeVaultPath } from "./settings";
|
|
|
|
|
import { VocabularyLoader } from "./vocab/VocabularyLoader";
|
|
|
|
|
import { parseEdgeVocabulary } from "./vocab/parseEdgeVocabulary";
|
|
|
|
|
import { Vocabulary } from "./vocab/Vocabulary";
|
|
|
|
|
import { LintEngine } from "./lint/LintEngine";
|
|
|
|
|
import { MindnetSettingTab } from "./ui/MindnetSettingTab";
|
|
|
|
|
|
|
|
|
|
export default class MindnetCausalAssistantPlugin extends Plugin {
|
|
|
|
|
settings: MindnetSettings;
|
|
|
|
|
private vocabulary: Vocabulary | null = null;
|
|
|
|
|
private reloadDebounceTimer: number | null = null;
|
|
|
|
|
|
|
|
|
|
async onload(): Promise<void> {
|
|
|
|
|
await this.loadSettings();
|
|
|
|
|
|
|
|
|
|
// Add settings tab
|
|
|
|
|
this.addSettingTab(new MindnetSettingTab(this.app, this));
|
|
|
|
|
|
|
|
|
|
// Register live reload for edge vocabulary file
|
|
|
|
|
this.registerEvent(
|
|
|
|
|
this.app.vault.on("modify", async (file: TFile) => {
|
|
|
|
|
const normalizedFilePath = normalizeVaultPath(file.path);
|
|
|
|
|
const normalizedVocabPath = normalizeVaultPath(this.settings.edgeVocabularyPath);
|
|
|
|
|
|
|
|
|
|
// Check if modified file matches vocabulary path (exact match or ends with)
|
|
|
|
|
if (normalizedFilePath === normalizedVocabPath ||
|
|
|
|
|
normalizedFilePath === `/${normalizedVocabPath}` ||
|
|
|
|
|
normalizedFilePath.endsWith(`/${normalizedVocabPath}`)) {
|
|
|
|
|
// Debounce reload to avoid multiple rapid reloads
|
|
|
|
|
if (this.reloadDebounceTimer !== null) {
|
|
|
|
|
window.clearTimeout(this.reloadDebounceTimer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.reloadDebounceTimer = window.setTimeout(async () => {
|
|
|
|
|
await this.reloadVocabulary();
|
|
|
|
|
this.reloadDebounceTimer = null;
|
|
|
|
|
}, 200);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.addCommand({
|
|
|
|
|
id: "mindnet-reload-edge-vocabulary",
|
|
|
|
|
name: "Mindnet: Reload edge vocabulary",
|
|
|
|
|
callback: async () => {
|
|
|
|
|
await this.reloadVocabulary();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.addCommand({
|
|
|
|
|
id: "mindnet-validate-current-note",
|
|
|
|
|
name: "Mindnet: Validate current note",
|
|
|
|
|
callback: async () => {
|
|
|
|
|
try {
|
|
|
|
|
const vocabulary = await this.ensureVocabularyLoaded();
|
|
|
|
|
if (!vocabulary) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const findings = await LintEngine.lintCurrentNote(this.app, vocabulary);
|
|
|
|
|
|
|
|
|
|
// Count findings by severity
|
|
|
|
|
const errorCount = findings.filter(f => f.severity === "ERROR").length;
|
|
|
|
|
const warnCount = findings.filter(f => f.severity === "WARN").length;
|
|
|
|
|
const infoCount = findings.filter(f => f.severity === "INFO").length;
|
|
|
|
|
|
|
|
|
|
// Show summary notice
|
|
|
|
|
new Notice(`Lint: ${errorCount} errors, ${warnCount} warnings, ${infoCount} info`);
|
|
|
|
|
|
|
|
|
|
// Log findings to console
|
|
|
|
|
console.log("=== Lint Findings ===");
|
|
|
|
|
for (const finding of findings) {
|
|
|
|
|
const quickfixInfo = finding.quickFixes && finding.quickFixes.length > 0
|
|
|
|
|
? ` [QuickFix: ${finding.quickFixes.map(qf => qf.title).join(", ")}]`
|
|
|
|
|
: "";
|
|
|
|
|
console.log(`[${finding.severity}] ${finding.ruleId}: ${finding.message} (${finding.filePath}:${finding.lineStart}${quickfixInfo})`);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
new Notice(`Failed to validate note: ${msg}`);
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onunload(): void {
|
|
|
|
|
// nothing yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async loadSettings(): Promise<void> {
|
|
|
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async saveSettings(): Promise<void> {
|
|
|
|
|
await this.saveData(this.settings);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ensure vocabulary is loaded. Auto-loads if not present.
|
|
|
|
|
* Returns Vocabulary instance or null on failure.
|
|
|
|
|
*/
|
|
|
|
|
private async ensureVocabularyLoaded(): Promise<Vocabulary | null> {
|
|
|
|
|
if (this.vocabulary) {
|
|
|
|
|
return this.vocabulary;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const text = await VocabularyLoader.loadText(
|
|
|
|
|
this.app,
|
|
|
|
|
this.settings.edgeVocabularyPath
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const parsed = parseEdgeVocabulary(text);
|
|
|
|
|
this.vocabulary = new Vocabulary(parsed);
|
|
|
|
|
|
|
|
|
|
const stats = this.vocabulary.getStats();
|
|
|
|
|
console.log("Vocabulary auto-loaded", stats);
|
|
|
|
|
return this.vocabulary;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
if (msg.includes("not found") || msg.includes("Vocabulary file not found")) {
|
|
|
|
|
new Notice("edge_vocabulary.md not found. Check the path in plugin settings.");
|
|
|
|
|
} else {
|
|
|
|
|
new Notice(`Failed to load vocabulary: ${msg}. Check plugin settings.`);
|
|
|
|
|
}
|
|
|
|
|
console.error("Failed to load vocabulary:", e);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reload vocabulary from file. Used by manual command and live reload.
|
|
|
|
|
*/
|
|
|
|
|
private async reloadVocabulary(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const text = await VocabularyLoader.loadText(
|
|
|
|
|
this.app,
|
|
|
|
|
this.settings.edgeVocabularyPath
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const parsed = parseEdgeVocabulary(text);
|
|
|
|
|
this.vocabulary = new Vocabulary(parsed);
|
|
|
|
|
|
|
|
|
|
const stats = this.vocabulary.getStats();
|
|
|
|
|
console.log("Vocabulary loaded", stats);
|
|
|
|
|
new Notice(`Edge vocabulary reloaded: ${stats.canonicalCount} canonical, ${stats.aliasCount} aliases`);
|
|
|
|
|
|
|
|
|
|
// Log normalization examples
|
|
|
|
|
if (stats.canonicalCount > 0) {
|
|
|
|
|
const firstCanonical = Array.from(parsed.byCanonical.keys())[0];
|
|
|
|
|
if (firstCanonical) {
|
|
|
|
|
const canonicalNorm = this.vocabulary.normalize(firstCanonical);
|
|
|
|
|
console.log(`Normalization example (canonical): "${firstCanonical}" -> canonical: ${canonicalNorm.canonical}, inverse: ${canonicalNorm.inverse}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (stats.aliasCount > 0) {
|
|
|
|
|
const firstAlias = Array.from(parsed.aliasToCanonical.keys())[0];
|
|
|
|
|
if (firstAlias) {
|
|
|
|
|
const aliasNorm = this.vocabulary.normalize(firstAlias);
|
|
|
|
|
console.log(`Normalization example (alias): "${firstAlias}" -> canonical: ${aliasNorm.canonical}, inverse: ${aliasNorm.inverse}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
if (msg.includes("not found") || msg.includes("Vocabulary file not found")) {
|
|
|
|
|
new Notice("edge_vocabulary.md not found. Configure path in plugin settings.");
|
|
|
|
|
} else {
|
|
|
|
|
new Notice(`Failed to reload vocabulary: ${msg}`);
|
|
|
|
|
}
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-04-15 20:13:31 +02:00
|
|
|
}
|