Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { UpdateModal } from "./gui/UpdateModal/UpdateModal";
import { CommandType } from "./types/macros/CommandType";
import { InfiniteAIAssistantCommandSettingsModal } from "./gui/MacroGUIs/AIAssistantInfiniteCommandSettingsModal";
import { FieldSuggestionCache } from "./utils/FieldSuggestionCache";
import { FolderRenameHandler } from "./services/folderRenameHandler";
import { PathRenameHandler } from "./services/pathRenameHandler";

// Parameters prefixed with `value-` get used as named values for the executed choice
type CaptureValueParameters = { [key in `value-${string}`]?: string };
Expand All @@ -35,6 +37,7 @@ export default class QuickAdd extends Plugin {
static instance: QuickAdd;
settings: QuickAddSettings;
private unsubscribeSettingsStore: () => void;
private pathRenameHandler: PathRenameHandler;

get api(): ReturnType<typeof QuickAddApi.GetApi> {
return QuickAddApi.GetApi(
Expand Down Expand Up @@ -83,6 +86,10 @@ export default class QuickAdd extends Plugin {
this.registerInterval(intervalId),
);

// Initialize path rename handler
this.pathRenameHandler = new PathRenameHandler(this.app, this);
this.registerEvent(this.app.vault.on("rename", this.pathRenameHandler.onRename));

this.addCommand({
id: "testQuickAdd",
name: "Test QuickAdd (dev)",
Expand Down
136 changes: 136 additions & 0 deletions src/quickAddSettingsTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export interface QuickAddSettings {
disableOnlineFeatures: boolean;
enableRibbonIcon: boolean;
showCaptureNotification: boolean;
autoRenameDestinationFolders: boolean;
showAutoRenameNotifications: boolean;
autoRenameTemplateFiles: boolean;
autoRenameUserScripts: boolean;
autoRenameFormatReferences: boolean;
autoRenameGlobalSettings: boolean;
ai: {
defaultModel: Model["name"] | "Ask me";
defaultSystemPrompt: string;
Expand Down Expand Up @@ -57,6 +63,12 @@ export const DEFAULT_SETTINGS: QuickAddSettings = {
disableOnlineFeatures: true,
enableRibbonIcon: false,
showCaptureNotification: true,
autoRenameDestinationFolders: true,
showAutoRenameNotifications: true,
autoRenameTemplateFiles: true,
autoRenameUserScripts: true,
autoRenameFormatReferences: true,
autoRenameGlobalSettings: true,
ai: {
defaultModel: "Ask me",
defaultSystemPrompt: `As an AI assistant within Obsidian, your primary goal is to help users manage their ideas and knowledge more effectively. Format your responses using Markdown syntax. Please use the [[Obsidian]] link format. You can write aliases for the links by writing [[Obsidian|the alias after the pipe symbol]]. To use mathematical notation, use LaTeX syntax. LaTeX syntax for larger equations should be on separate lines, surrounded with double dollar signs ($$). You can also inline math expressions by wrapping it in $ symbols. For example, use $$w_{ij}^{\text{new}}:=w_{ij}^{\text{current}}+\eta\cdot\delta_j\cdot x_{ij}$$ on a separate line, but you can write "($\eta$ = learning rate, $\delta_j$ = error term, $x_{ij}$ = input)" inline.`,
Expand All @@ -82,6 +94,8 @@ export const DEFAULT_SETTINGS: QuickAddSettings = {
export class QuickAddSettingsTab extends PluginSettingTab {
public plugin: QuickAdd;
private choiceView: ChoiceView;
private autoRenameNotificationSetting: Setting;
private autoRenameSubSettings: Setting[];

constructor(app: App, plugin: QuickAdd) {
super(app, plugin);
Expand All @@ -98,6 +112,7 @@ export class QuickAddSettingsTab extends PluginSettingTab {
this.addTemplateFolderPathSetting();
this.addAnnounceUpdatesSetting();
this.addShowCaptureNotificationSetting();
this.addAutoRenameSettings();
this.addOnePageInputSetting();
this.addDisableOnlineFeaturesSetting();
this.addEnableRibbonIconSetting();
Expand Down Expand Up @@ -131,6 +146,127 @@ export class QuickAddSettingsTab extends PluginSettingTab {
});
}

addAutoRenameSettings() {
// Create header
const headerEl = this.containerEl.createEl("h3", { text: "Auto-Rename Settings" });
headerEl.style.marginTop = "20px";
headerEl.style.marginBottom = "10px";

const descEl = this.containerEl.createEl("p");
descEl.textContent = "Automatically update QuickAdd references when files and folders are renamed in the vault.";
descEl.style.marginBottom = "15px";
descEl.style.color = "var(--text-muted)";

// Main toggle for destination folders
const folderSetting = new Setting(this.containerEl);
folderSetting.setName("Auto-rename destination folders");
folderSetting.setDesc("Update folder paths in choice configurations when folders are renamed.");
folderSetting.addToggle((toggle) => {
toggle.setValue(settingsStore.getState().autoRenameDestinationFolders);
toggle.onChange((value) => {
settingsStore.setState({ autoRenameDestinationFolders: value });
this.updateAutoRenameSubSettings();
});
});

// Template files setting
const templateSetting = new Setting(this.containerEl);
templateSetting.setClass("quickadd-sub-setting");
templateSetting.settingEl.style.marginLeft = "20px";
templateSetting.setName("Auto-rename template files");
templateSetting.setDesc("Update template file paths when template files are renamed or moved.");
templateSetting.addToggle((toggle) => {
toggle.setValue(settingsStore.getState().autoRenameTemplateFiles);
toggle.onChange((value) => {
settingsStore.setState({ autoRenameTemplateFiles: value });
});
});

// User scripts setting
const scriptSetting = new Setting(this.containerEl);
scriptSetting.setClass("quickadd-sub-setting");
scriptSetting.settingEl.style.marginLeft = "20px";
scriptSetting.setName("Auto-rename user scripts");
scriptSetting.setDesc("Update user script file paths when script files are renamed or moved.");
scriptSetting.addToggle((toggle) => {
toggle.setValue(settingsStore.getState().autoRenameUserScripts);
toggle.onChange((value) => {
settingsStore.setState({ autoRenameUserScripts: value });
});
});

// Format references setting
const formatSetting = new Setting(this.containerEl);
formatSetting.setClass("quickadd-sub-setting");
formatSetting.settingEl.style.marginLeft = "20px";
formatSetting.setName("Auto-rename format references");
formatSetting.setDesc("Update file and folder references in format strings (e.g., {{TEMPLATE:path}}, {{FIELD:|folder:path}}).");
formatSetting.addToggle((toggle) => {
toggle.setValue(settingsStore.getState().autoRenameFormatReferences);
toggle.onChange((value) => {
settingsStore.setState({ autoRenameFormatReferences: value });
});
});

// Global settings setting
const globalSetting = new Setting(this.containerEl);
globalSetting.setClass("quickadd-sub-setting");
globalSetting.settingEl.style.marginLeft = "20px";
globalSetting.setName("Auto-rename global settings");
globalSetting.setDesc("Update template folder path and AI prompt templates folder path when renamed.");
globalSetting.addToggle((toggle) => {
toggle.setValue(settingsStore.getState().autoRenameGlobalSettings);
toggle.onChange((value) => {
settingsStore.setState({ autoRenameGlobalSettings: value });
});
});

// Notification setting
const notificationSetting = new Setting(this.containerEl);
notificationSetting.setClass("quickadd-sub-setting");
notificationSetting.settingEl.style.marginLeft = "20px";
notificationSetting.setName("Show auto-rename notifications");
notificationSetting.setDesc("Display notifications when auto-rename operations are performed.");
notificationSetting.addToggle((toggle) => {
toggle.setValue(settingsStore.getState().showAutoRenameNotifications);
toggle.onChange((value) => {
settingsStore.setState({ showAutoRenameNotifications: value });
});
});

// Store references for updating visibility
this.autoRenameNotificationSetting = notificationSetting;
this.autoRenameSubSettings = [
templateSetting,
scriptSetting,
formatSetting,
globalSetting,
notificationSetting
];

this.updateAutoRenameSubSettings();
}

private updateAutoRenameSubSettings() {
const isEnabled = settingsStore.getState().autoRenameDestinationFolders;

if (this.autoRenameSubSettings) {
this.autoRenameSubSettings.forEach(setting => {
setting.settingEl.style.display = isEnabled ? "" : "none";
});
}

// Keep the old method for backward compatibility
this.updateAutoRenameNotificationSetting();
}

private updateAutoRenameNotificationSetting() {
if (this.autoRenameNotificationSetting) {
const isEnabled = settingsStore.getState().autoRenameDestinationFolders;
this.autoRenameNotificationSetting.settingEl.style.display = isEnabled ? "" : "none";
}
}

hide(): void {
if (this.choiceView) this.choiceView.$destroy();
}
Expand Down
161 changes: 161 additions & 0 deletions src/services/folderRenameHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type { App, TAbstractFile } from "obsidian";
import { Notice, TFolder } from "obsidian";
import { log } from "../logger/logManager";
import type QuickAdd from "../main";
import { settingsStore } from "../settingsStore";
import { isFolder } from "../utilityObsidian";
import { FolderPathUpdater } from "../utils/folderPathUpdater";

/**
* Handles folder rename events and automatically updates QuickAdd choice configurations.
*/
export class FolderRenameHandler {
private app: App;
private plugin: QuickAdd;

constructor(app: App, plugin: QuickAdd) {
this.app = app;
this.plugin = plugin;
}

/**
* Handles the vault rename event.
*/
onRename = (file: TAbstractFile, oldPath: string): void => {
// Only process folder renames
if (!this.isFolderRename(file, oldPath)) {
return;
}

const newPath = file.path;

log.logMessage(`QuickAdd: Folder renamed from "${oldPath}" to "${newPath}"`);

// Check if auto-rename is enabled
if (!this.isAutoRenameEnabled()) {
log.logMessage("QuickAdd: Auto-rename is disabled, skipping update");
return;
}

this.updateChoicesForFolderRename(oldPath, newPath);
};

/**
* Updates all affected choices when a folder is renamed.
*/
private updateChoicesForFolderRename(oldPath: string, newPath: string): void {
try {
const currentSettings = settingsStore.getState();
const currentChoices = currentSettings.choices;

// Find affected choices for user notification
const affectedChoices = FolderPathUpdater.findChoicesWithFolderPath(
currentChoices,
oldPath
);

if (affectedChoices.length === 0) {
log.logMessage(`QuickAdd: No choices affected by folder rename from "${oldPath}" to "${newPath}"`);
return;
}

// Create backup of current settings
const settingsBackup = structuredClone(currentSettings);

try {
// Update the choices
const updatedChoices = FolderPathUpdater.updateChoicesFolderPaths(
currentChoices,
oldPath,
newPath
);

// Apply the updates
settingsStore.setState({
...currentSettings,
choices: updatedChoices,
});

// Log successful update
log.logMessage(
`QuickAdd: Successfully updated ${affectedChoices.length} choice(s) for folder rename from "${oldPath}" to "${newPath}"`
);

// Show notification to user if enabled
this.showUpdateNotification(affectedChoices.length, oldPath, newPath);

} catch (updateError) {
// Restore backup on error
settingsStore.setState(settingsBackup);
log.logError(`QuickAdd: Failed to update choices for folder rename: ${updateError}`);
this.showErrorNotification(oldPath, newPath);
}

} catch (error) {
log.logError(`QuickAdd: Error handling folder rename: ${error}`);
}
}

/**
* Checks if the rename event is for a folder (not a file).
*/
private isFolderRename(file: TAbstractFile, oldPath: string): boolean {
// Check if the new file is a folder
if (file instanceof TFolder) {
return true;
}

// Additional check using the old path (in case file object type is ambiguous)
// This handles edge cases where the file type might not be correctly determined
return isFolder(this.app, file.path) || this.looksLikeFolderPath(oldPath);
}

/**
* Heuristic to determine if a path looks like a folder path.
*/
private looksLikeFolderPath(path: string): boolean {
// If path doesn't have a file extension, it's likely a folder
const lastSlashIndex = path.lastIndexOf("/");
const lastPart = lastSlashIndex >= 0 ? path.substring(lastSlashIndex + 1) : path;

// No dot or dot at the beginning (hidden folder) suggests it's a folder
return !lastPart.includes(".") || lastPart.startsWith(".");
}

/**
* Checks if auto-rename feature is enabled in settings.
*/
private isAutoRenameEnabled(): boolean {
const settings = settingsStore.getState();
// Default to enabled if setting doesn't exist yet
return settings.autoRenameDestinationFolders ?? true;
}

/**
* Shows a success notification to the user.
*/
private showUpdateNotification(count: number, oldPath: string, newPath: string): void {
const settings = settingsStore.getState();

// Check if notifications are enabled
if (settings.showAutoRenameNotifications === false) {
return;
}

const message = `QuickAdd: Updated ${count} choice${count > 1 ? 's' : ''} after folder rename\n"${oldPath}" β†’ "${newPath}"`;

// Use Obsidian's notice system
// biome-ignore lint/style/noNonNullAssertion: app is always available in plugin context
new Notice(message, 5000);
}

/**
* Shows an error notification to the user.
*/
private showErrorNotification(oldPath: string, newPath: string): void {
const message = `QuickAdd: Failed to update choices after folder rename\n"${oldPath}" β†’ "${newPath}"\nPlease update manually if needed.`;

// biome-ignore lint/style/noNonNullAssertion: app is always available in plugin context
new Notice(message, 8000);
}
}
Loading