Implement inverse edge creation functionality in writerActions
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run

- Added a new function to automatically create inverse edges in target notes/sections after inserting edges in the "note_links" and "candidates" zones.
- Refactored the zone detection logic to separate content-based detection from file-based detection, improving code clarity and maintainability.
- Enhanced logging for better tracking of edge creation processes and potential issues during execution.
This commit is contained in:
Lars 2026-02-05 12:04:28 +01:00
parent 725adb5302
commit 6d5f6203c4
2 changed files with 313 additions and 9 deletions

View File

@ -5,7 +5,7 @@
import { Modal, Setting, TFile, Notice } from "obsidian"; import { Modal, Setting, TFile, Notice } from "obsidian";
import type { App, Editor } from "obsidian"; import type { App, Editor } from "obsidian";
import type { MissingLinkTodo, CandidateCleanupTodo, MissingSlotTodo } from "./types"; import type { MissingLinkTodo, CandidateCleanupTodo, MissingSlotTodo } from "./types";
import { detectZone, findSection } from "./zoneDetector"; import { detectZone, detectZoneFromContent, findSection } from "./zoneDetector";
import { splitIntoSections } from "../mapping/sectionParser"; import { splitIntoSections } from "../mapping/sectionParser";
import { EntityPickerModal } from "../ui/EntityPickerModal"; import { EntityPickerModal } from "../ui/EntityPickerModal";
import { NoteIndex } from "../entityPicker/noteIndex"; import { NoteIndex } from "../entityPicker/noteIndex";
@ -84,12 +84,34 @@ export async function insertEdgeForward(
console.log("[insertEdgeForward] Inserting into note_links zone"); console.log("[insertEdgeForward] Inserting into note_links zone");
} }
await insertEdgeInZone(app, editor, file, "note_links", edgeType, targetLink, settings); await insertEdgeInZone(app, editor, file, "note_links", edgeType, targetLink, settings);
// Automatically create inverse edge in target note/section
await createInverseEdge(
app,
edgeType,
todo,
vocabulary,
edgeVocabulary,
settings,
debugLogging
);
} else if (targetZone === "candidates") { } else if (targetZone === "candidates") {
// Insert in Kandidaten zone // Insert in Kandidaten zone
if (debugLogging) { if (debugLogging) {
console.log("[insertEdgeForward] Inserting into candidates zone"); console.log("[insertEdgeForward] Inserting into candidates zone");
} }
await insertEdgeInZone(app, editor, file, "candidates", edgeType, targetLink, settings); await insertEdgeInZone(app, editor, file, "candidates", edgeType, targetLink, settings);
// Automatically create inverse edge in target note/section
await createInverseEdge(
app,
edgeType,
todo,
vocabulary,
edgeVocabulary,
settings,
debugLogging
);
} else { } else {
// Insert in source section - need to select section if multiple exist // Insert in source section - need to select section if multiple exist
// IMPORTANT: Use source note (fromNodeRef.file), not current note! // IMPORTANT: Use source note (fromNodeRef.file), not current note!
@ -218,9 +240,260 @@ export async function insertEdgeForward(
if (debugLogging) { if (debugLogging) {
console.log("[insertEdgeForward] Edge insertion completed"); console.log("[insertEdgeForward] Edge insertion completed");
} }
// Automatically create inverse edge in target note/section
await createInverseEdge(
app,
edgeType,
todo,
vocabulary,
edgeVocabulary,
settings,
debugLogging
);
} }
} }
/**
* Automatically create inverse edge in target note/section.
*/
async function createInverseEdge(
app: App,
forwardEdgeType: string,
todo: MissingLinkTodo,
vocabulary: Vocabulary,
edgeVocabulary: EdgeVocabulary,
settings: MindnetSettings,
debugLogging?: boolean
): Promise<void> {
try {
console.log("[createInverseEdge] Starting inverse edge creation for:", forwardEdgeType);
// Get canonical type and inverse
const canonical = vocabulary.getCanonical(forwardEdgeType);
if (!canonical) {
console.log("[createInverseEdge] No canonical type found for:", forwardEdgeType);
return; // Can't create inverse without canonical type
}
const inverseCanonical = vocabulary.getInverse(canonical);
if (!inverseCanonical) {
console.log("[createInverseEdge] No inverse defined for:", canonical);
return; // No inverse defined, skip
}
// Get the raw inverse type (use canonical or find an alias)
const inverseEntry = edgeVocabulary.byCanonical.get(inverseCanonical);
const inverseEdgeType = inverseEntry?.aliases?.[0] || inverseCanonical;
console.log("[createInverseEdge] Forward:", forwardEdgeType, "-> Canonical:", canonical, "-> Inverse:", inverseEdgeType);
// Find target file
const targetFileRef = todo.toNodeRef.file;
let targetFile: TFile | null = null;
const possiblePaths = [
targetFileRef,
targetFileRef + ".md",
targetFileRef.replace(/\.md$/, ""),
targetFileRef.replace(/\.md$/, "") + ".md",
];
for (const path of possiblePaths) {
const found = app.vault.getAbstractFileByPath(path);
if (found && found instanceof TFile) {
targetFile = found;
break;
}
}
if (!targetFile) {
const basename = targetFileRef.replace(/\.md$/, "").split("/").pop() || targetFileRef;
const resolved = app.metadataCache.getFirstLinkpathDest(basename, todo.fromNodeRef.file);
if (resolved) {
targetFile = resolved;
}
}
if (!targetFile) {
console.log("[createInverseEdge] Target file not found:", targetFileRef);
return;
}
// Build source link (for inverse edge, source is the original target)
const sourceBasename = todo.fromNodeRef.file.replace(/\.md$/, "").split("/").pop() || todo.fromNodeRef.file;
const sourceLink = todo.fromNodeRef.heading
? `${sourceBasename}#${todo.fromNodeRef.heading}`
: sourceBasename;
// Always ask user to select target section (or whole note)
// Open target file in editor first
console.log("[createInverseEdge] Opening target file:", targetFile.path);
await app.workspace.openLinkText(targetFile.path, "", false);
// Wait a bit for the editor to be ready
await new Promise(resolve => setTimeout(resolve, 100));
const targetEditor = app.workspace.activeEditor?.editor;
if (!targetEditor) {
console.error("[createInverseEdge] Could not get editor for target file:", targetFile.path);
return;
}
// Load sections from target file
const { splitIntoSections } = await import("../mapping/sectionParser");
const targetContent = await app.vault.read(targetFile);
const sections = splitIntoSections(targetContent);
// Filter out special zones
const contentSections = sections.filter(
(s) => s.heading !== "Kandidaten" && s.heading !== "Note-Verbindungen"
);
// Show selection modal: user can choose a section or "whole note"
const selectedTarget = await selectTargetForInverseEdge(
app,
targetFile,
contentSections,
todo.toNodeRef.heading,
debugLogging
);
if (!selectedTarget) {
console.log("[createInverseEdge] User cancelled target selection");
return; // User cancelled
}
console.log("[createInverseEdge] Selected target:", selectedTarget.type, selectedTarget.heading || "(whole note)");
console.log("[createInverseEdge] Inverse edge type:", inverseEdgeType, "source link:", sourceLink);
try {
if (selectedTarget.type === "note_links") {
console.log("[createInverseEdge] Calling insertEdgeInZone with note_links");
await insertEdgeInZone(app, targetEditor, targetFile, "note_links", inverseEdgeType, sourceLink, settings);
console.log("[createInverseEdge] insertEdgeInZone completed");
} else {
// Use selected section
const targetSection = await findSection(app, targetFile, selectedTarget.heading);
if (targetSection) {
console.log("[createInverseEdge] Found target section:", targetSection.heading || "(root)");
await insertEdgeInSection(
app,
targetEditor,
targetFile,
targetSection,
inverseEdgeType,
sourceLink,
settings
);
console.log("[createInverseEdge] insertEdgeInSection completed");
} else {
console.log("[createInverseEdge] Target section not found, falling back to note_links");
// Fallback to note_links if section not found
await insertEdgeInZone(app, targetEditor, targetFile, "note_links", inverseEdgeType, sourceLink, settings);
console.log("[createInverseEdge] insertEdgeInZone (fallback) completed");
}
}
console.log("[createInverseEdge] Inverse edge creation completed successfully");
} catch (error) {
console.error("[createInverseEdge] Error during edge insertion:", error);
throw error; // Re-throw to see the error
}
} catch (error) {
console.error("[createInverseEdge] Error creating inverse edge:", error);
// Don't throw - inverse edge creation is optional
}
}
/**
* Select target section or whole note for inverse edge insertion.
*/
async function selectTargetForInverseEdge(
app: App,
file: TFile,
sections: Array<{ heading: string | null; startLine: number; endLine: number }>,
preferredHeading: string | null,
debugLogging?: boolean
): Promise<{ type: "section" | "note_links"; heading: string | null } | null> {
return new Promise((resolve) => {
let resolved = false;
const modal = new Modal(app);
modal.titleEl.textContent = "Ziel für inverse Kante wählen";
const description = preferredHeading
? `Wo soll die inverse Kante eingefügt werden? (Empfohlen: "${preferredHeading}")`
: "Wo soll die inverse Kante eingefügt werden?";
modal.contentEl.createEl("p", { text: description });
const doResolve = (value: { type: "section" | "note_links"; heading: string | null } | null) => {
if (!resolved) {
resolved = true;
resolve(value);
}
};
// Add "Whole note" option
const wholeNoteSetting = new Setting(modal.contentEl);
wholeNoteSetting.setName("📄 Ganze Note (Note-Verbindungen)");
wholeNoteSetting.setDesc("Edge wird in der Note-Verbindungen Zone eingefügt (note-level)");
wholeNoteSetting.addButton((btn) => {
btn.setButtonText("Auswählen");
btn.onClick(() => {
if (debugLogging) {
console.log("[selectTargetForInverseEdge] User selected: whole note");
}
doResolve({ type: "note_links", heading: null });
modal.close();
});
});
// Add section options
for (const section of sections) {
if (!section) continue;
const sectionName = section.heading || "(Root section)";
const isPreferred = preferredHeading && headingsMatch(section.heading, preferredHeading);
const setting = new Setting(modal.contentEl);
if (isPreferred) {
setting.setName(`${sectionName} (empfohlen)`);
setting.setDesc(`Zeilen ${section.startLine + 1}-${section.endLine} - Empfohlen basierend auf Chain Template`);
} else {
setting.setName(sectionName);
setting.setDesc(`Zeilen ${section.startLine + 1}-${section.endLine}`);
}
setting.addButton((btn) => {
if (isPreferred) {
btn.setButtonText("Auswählen (Empfohlen)");
btn.setCta(); // Highlight recommended option
} else {
btn.setButtonText("Auswählen");
}
btn.onClick(() => {
if (debugLogging) {
console.log("[selectTargetForInverseEdge] User selected section:", sectionName);
}
doResolve({ type: "section", heading: section.heading });
modal.close();
});
});
}
modal.onClose = () => {
if (debugLogging) {
console.log("[selectTargetForInverseEdge] Modal closed, resolved:", resolved);
if (!resolved) {
console.log("[selectTargetForInverseEdge] Resolving with null (user cancelled)");
}
}
if (!resolved) {
doResolve(null);
}
};
modal.open();
});
}
/** /**
* Insert edge in zone (Kandidaten or Note-Verbindungen). * Insert edge in zone (Kandidaten or Note-Verbindungen).
*/ */
@ -233,7 +506,15 @@ async function insertEdgeInZone(
targetLink: string, targetLink: string,
settings: MindnetSettings settings: MindnetSettings
): Promise<void> { ): Promise<void> {
const zone = await detectZone(app, file, zoneType); console.log("[insertEdgeInZone] Starting, file:", file.path, "zoneType:", zoneType, "edgeType:", edgeType, "targetLink:", targetLink);
// Use editor content instead of reading from vault (editor might have unsaved changes)
const editorContent = editor.getValue();
console.log("[insertEdgeInZone] Editor content length:", editorContent.length);
// Detect zone from editor content
const zone = detectZoneFromContent(editorContent, zoneType);
console.log("[insertEdgeInZone] Zone detection result:", { exists: zone.exists, heading: zone.heading, startLine: zone.startLine, endLine: zone.endLine });
const wrapperCalloutType = settings.mappingWrapperCalloutType || "abstract"; const wrapperCalloutType = settings.mappingWrapperCalloutType || "abstract";
const wrapperTitle = settings.mappingWrapperTitle || "🕸️ Semantic Mapping"; const wrapperTitle = settings.mappingWrapperTitle || "🕸️ Semantic Mapping";
@ -241,40 +522,53 @@ async function insertEdgeInZone(
if (!zone.exists) { if (!zone.exists) {
// Create zone // Create zone
console.log("[insertEdgeInZone] Zone does not exist, creating new zone");
const content = editor.getValue(); const content = editor.getValue();
const newZone = `\n\n## ${zoneType === "candidates" ? "Kandidaten" : "Note-Verbindungen"}\n\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`; const newZone = `\n\n## ${zoneType === "candidates" ? "Kandidaten" : "Note-Verbindungen"}\n\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
const newContent = content + newZone; const newContent = content + newZone;
editor.setValue(newContent); editor.setValue(newContent);
console.log("[insertEdgeInZone] New zone created and content set");
return; return;
} }
// Insert in existing zone // Insert in existing zone
console.log("[insertEdgeInZone] Zone exists, inserting into existing zone");
const content = editor.getValue(); const content = editor.getValue();
const lines = content.split(/\r?\n/); const lines = content.split(/\r?\n/);
// Find insertion point (end of zone, before next heading) // Find insertion point (end of zone, before next heading)
let insertLine = zone.endLine - 1; let insertLine = zone.endLine - 1;
console.log("[insertEdgeInZone] Initial insertLine:", insertLine);
// Check if zone has a wrapper callout using settings // Check if zone has a wrapper callout using settings
const zoneContent = zone.content; const zoneContent = zone.content;
const hasWrapper = zoneContent.includes(`> [!${wrapperCalloutType}]`) && const hasWrapper = zoneContent.includes(`> [!${wrapperCalloutType}]`) &&
zoneContent.includes(wrapperTitle); zoneContent.includes(wrapperTitle);
console.log("[insertEdgeInZone] Has wrapper:", hasWrapper, "wrapperCalloutType:", wrapperCalloutType, "wrapperTitle:", wrapperTitle);
if (hasWrapper) { if (hasWrapper) {
// Insert inside wrapper - find the end of the wrapper block // Insert inside wrapper - find the end of the wrapper block
console.log("[insertEdgeInZone] Inserting inside existing wrapper");
const wrapperEndLine = findWrapperBlockEnd(lines, zone.startLine, wrapperCalloutType, wrapperTitle); const wrapperEndLine = findWrapperBlockEnd(lines, zone.startLine, wrapperCalloutType, wrapperTitle);
console.log("[insertEdgeInZone] Wrapper end line:", wrapperEndLine);
if (wrapperEndLine !== null) { if (wrapperEndLine !== null) {
insertLine = wrapperEndLine - 1; insertLine = wrapperEndLine - 1;
} }
const newEdge = `>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`; const newEdge = `>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
console.log("[insertEdgeInZone] Inserting edge at line:", insertLine, "edge:", newEdge);
lines.splice(insertLine, 0, ...newEdge.split("\n")); lines.splice(insertLine, 0, ...newEdge.split("\n"));
} else { } else {
// Create wrapper and insert edge // Create wrapper and insert edge
console.log("[insertEdgeInZone] Creating new wrapper and inserting edge");
const wrapper = `\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`; const wrapper = `\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
console.log("[insertEdgeInZone] Inserting wrapper at line:", insertLine, "wrapper:", wrapper);
lines.splice(insertLine, 0, ...wrapper.split("\n")); lines.splice(insertLine, 0, ...wrapper.split("\n"));
} }
editor.setValue(lines.join("\n")); const newContent = lines.join("\n");
console.log("[insertEdgeInZone] Setting new content, length:", newContent.length);
editor.setValue(newContent);
console.log("[insertEdgeInZone] Content set successfully");
} }
/** /**

View File

@ -13,14 +13,12 @@ export interface ZoneInfo {
} }
/** /**
* Detect if a zone exists in a file. * Detect if a zone exists in content (from editor or file).
*/ */
export async function detectZone( export function detectZoneFromContent(
app: App, content: string,
file: TFile,
zoneType: "candidates" | "note_links" zoneType: "candidates" | "note_links"
): Promise<ZoneInfo> { ): ZoneInfo {
const content = await app.vault.read(file);
const lines = content.split(/\r?\n/); const lines = content.split(/\r?\n/);
const heading = zoneType === "candidates" ? "## Kandidaten" : "## Note-Verbindungen"; const heading = zoneType === "candidates" ? "## Kandidaten" : "## Note-Verbindungen";
@ -72,6 +70,18 @@ export async function detectZone(
}; };
} }
/**
* Detect if a zone exists in a file.
*/
export async function detectZone(
app: App,
file: TFile,
zoneType: "candidates" | "note_links"
): Promise<ZoneInfo> {
const content = await app.vault.read(file);
return detectZoneFromContent(content, zoneType);
}
/** /**
* Find section by heading in file. * Find section by heading in file.
*/ */