feat: scaffold plugin and implement move-to-new-folder workflow

This commit is contained in:
David 2026-03-07 09:37:10 -03:00
commit 751984280e
11 changed files with 1900 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules/
*.log

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# Move to New Folder
Move notes into newly created folders with a step-by-step workflow.
## Status
Implementation in progress.

48
esbuild.config.mjs Normal file
View file

@ -0,0 +1,48 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
"/* THIS IS AN AUTOGENERATED/BUNDLED FILE BY ESBUILD */" +
"\n" +
"if (typeof window !== 'undefined' && window.require) { window.require = undefined; window.module = undefined; }";
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
});
if (prod) {
await context.rebuild();
await context.dispose();
process.exit(0);
}
await context.watch();

475
main.js Normal file
View file

@ -0,0 +1,475 @@
/* THIS IS AN AUTOGENERATED/BUNDLED FILE BY ESBUILD */
if (typeof window !== 'undefined' && window.require) { window.require = undefined; window.module = undefined; }
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// main.ts
var main_exports = {};
__export(main_exports, {
default: () => MoveToNewFolderPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian = require("obsidian");
var DEFAULT_SETTINGS = {
defaultToCurrentParent: true
};
var ParentFolderPickerModal = class extends import_obsidian.Modal {
constructor(app, initialPath, onCloseResolve) {
super(app);
this.searchValue = "";
this.selectedIndex = 0;
this.listByPath = /* @__PURE__ */ new Map();
this.initialPath = initialPath;
this.selectedPath = initialPath;
this.onCloseResolve = onCloseResolve;
this.folderPaths = this.collectFolderPaths();
if (!this.folderPaths.includes(this.selectedPath)) {
this.selectedPath = "";
}
}
onOpen() {
const { contentEl } = this;
this.modalEl.addClass("move-to-new-folder-modal");
contentEl.empty();
contentEl.createEl("h2", { text: "Choose a parent folder" });
contentEl.createEl("p", {
text: "Search by typing or pick a folder from the list.",
cls: "move-to-new-folder-hint"
});
const searchInput = contentEl.createEl("input", {
type: "text",
placeholder: "Search folders...",
cls: "move-to-new-folder-search"
});
const listEl = contentEl.createDiv({ cls: "move-to-new-folder-list" });
const render = () => {
listEl.empty();
this.listByPath.clear();
const filtered = this.getFilteredFolders();
if (filtered.length === 0) {
listEl.createDiv({
text: "No folders match your search.",
cls: "move-to-new-folder-empty"
});
return;
}
if (this.selectedIndex >= filtered.length) {
this.selectedIndex = 0;
}
const selectedByIndex = filtered[this.selectedIndex];
this.selectedPath = selectedByIndex;
for (const folderPath of filtered) {
const button = listEl.createEl("button", {
cls: "move-to-new-folder-item",
text: folderPath.length > 0 ? folderPath : "/"
});
button.type = "button";
button.dataset.path = folderPath;
if (folderPath === this.selectedPath) {
button.addClass("is-selected");
}
button.addEventListener("click", () => {
this.selectedPath = folderPath;
this.closeWithResult(folderPath);
});
button.addEventListener("mouseenter", () => {
const idx = filtered.indexOf(folderPath);
if (idx >= 0) {
this.selectedIndex = idx;
this.selectedPath = folderPath;
this.refreshSelection(listEl, filtered);
}
});
this.listByPath.set(folderPath, button);
}
this.refreshSelection(listEl, filtered);
};
searchInput.addEventListener("input", () => {
this.searchValue = searchInput.value.trim();
this.selectedIndex = 0;
render();
});
searchInput.addEventListener("keydown", (event) => {
const filtered = this.getFilteredFolders();
if (event.key === "ArrowDown") {
event.preventDefault();
if (filtered.length > 0) {
this.selectedIndex = Math.min(this.selectedIndex + 1, filtered.length - 1);
this.selectedPath = filtered[this.selectedIndex];
this.refreshSelection(listEl, filtered);
}
}
if (event.key === "ArrowUp") {
event.preventDefault();
if (filtered.length > 0) {
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
this.selectedPath = filtered[this.selectedIndex];
this.refreshSelection(listEl, filtered);
}
}
if (event.key === "Enter") {
event.preventDefault();
if (filtered.length > 0) {
this.closeWithResult(filtered[this.selectedIndex]);
}
}
});
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
const cancelButton = actionsEl.createEl("button", {
text: "Cancel",
cls: "mod-muted"
});
cancelButton.type = "button";
cancelButton.addEventListener("click", () => this.closeWithResult(null));
const chooseButton = actionsEl.createEl("button", {
text: "Choose",
cls: "mod-cta"
});
chooseButton.type = "button";
chooseButton.addEventListener("click", () => this.closeWithResult(this.selectedPath));
render();
searchInput.focus();
searchInput.select();
}
onClose() {
this.modalEl.removeClass("move-to-new-folder-modal");
this.contentEl.empty();
}
collectFolderPaths() {
const folderSet = /* @__PURE__ */ new Set([""]);
for (const item of this.app.vault.getAllLoadedFiles()) {
if (item instanceof import_obsidian.TFolder) {
folderSet.add(item.path);
}
}
return Array.from(folderSet).sort((a, b) => a.localeCompare(b));
}
getFilteredFolders() {
if (!this.searchValue) {
return this.folderPaths;
}
const needle = this.searchValue.toLocaleLowerCase();
return this.folderPaths.filter((path) => {
const haystack = path.length > 0 ? path.toLocaleLowerCase() : "/";
return haystack.includes(needle);
});
}
refreshSelection(listEl, filtered) {
for (const [path, button] of this.listByPath.entries()) {
const shouldSelect = path === this.selectedPath;
button.toggleClass("is-selected", shouldSelect);
if (shouldSelect) {
button.scrollIntoView({ block: "nearest" });
}
}
if (!filtered.includes(this.selectedPath) && filtered.length > 0) {
this.selectedIndex = 0;
this.selectedPath = filtered[0];
this.refreshSelection(listEl, filtered);
}
}
closeWithResult(selectedPath) {
this.onCloseResolve({ selectedPath });
this.close();
}
};
var TextPromptModal = class extends import_obsidian.Modal {
constructor(app, title, placeholder, submitText, onCloseResolve) {
super(app);
this.title = title;
this.placeholder = placeholder;
this.submitText = submitText;
this.onCloseResolve = onCloseResolve;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: this.title });
const input = contentEl.createEl("input", {
type: "text",
placeholder: this.placeholder
});
const hintEl = contentEl.createDiv({
text: "Enter only a folder name, not a full path.",
cls: "move-to-new-folder-hint"
});
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
const cancelButton = actionsEl.createEl("button", {
text: "Cancel",
cls: "mod-muted"
});
cancelButton.type = "button";
cancelButton.addEventListener("click", () => this.closeWithResult(null));
const submitButton = actionsEl.createEl("button", {
text: this.submitText,
cls: "mod-cta"
});
submitButton.type = "button";
const submit = () => {
const value = input.value.trim();
if (!value) {
new import_obsidian.Notice("Folder name cannot be empty.");
return;
}
if (value.includes("/") || value.includes("\\")) {
hintEl.setText("Folder name cannot contain path separators.");
return;
}
this.closeWithResult(value);
};
submitButton.addEventListener("click", submit);
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
submit();
}
});
input.focus();
input.select();
}
onClose() {
this.contentEl.empty();
}
closeWithResult(value) {
this.onCloseResolve({ value });
this.close();
}
};
var ConfirmationModal = class extends import_obsidian.Modal {
constructor(app, titleText, bodyText, confirmText, onCloseResolve) {
super(app);
this.titleText = titleText;
this.bodyText = bodyText;
this.confirmText = confirmText;
this.onCloseResolve = onCloseResolve;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: this.titleText });
contentEl.createEl("p", { text: this.bodyText });
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
const cancelButton = actionsEl.createEl("button", {
text: "Cancel",
cls: "mod-muted"
});
cancelButton.type = "button";
cancelButton.addEventListener("click", () => this.closeWithResult(false));
const confirmButton = actionsEl.createEl("button", {
text: this.confirmText,
cls: "mod-cta"
});
confirmButton.type = "button";
confirmButton.addEventListener("click", () => this.closeWithResult(true));
}
onClose() {
this.contentEl.empty();
}
closeWithResult(confirmed) {
this.onCloseResolve(confirmed);
this.close();
}
};
var MoveToNewFolderSettingTab = class extends import_obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Move to New Folder settings" });
new import_obsidian.Setting(containerEl).setName("Default parent to current note folder").setDesc("When enabled, the folder picker starts at the current note's parent folder.").addToggle((toggle) => {
toggle.setValue(this.plugin.settings.defaultToCurrentParent).onChange(async (value) => {
this.plugin.settings.defaultToCurrentParent = value;
await this.plugin.saveSettings();
});
});
}
};
var MoveToNewFolderPlugin = class extends import_obsidian.Plugin {
constructor() {
super(...arguments);
this.settings = DEFAULT_SETTINGS;
}
async onload() {
await this.loadSettings();
this.addSettingTab(new MoveToNewFolderSettingTab(this.app, this));
this.addCommand({
id: "move-note-to-new-folder",
name: "Move note to new folder",
checkCallback: (checking) => {
const context = this.getActiveMarkdownFileContext();
if (!context) {
return false;
}
if (!checking) {
void this.runMoveFlow(context.file, context.leaf);
}
return true;
}
});
this.app.workspace.onLayoutReady(() => {
this.registerFileMenuAction();
});
}
async loadSettings() {
const loaded = await this.loadData();
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded);
}
async saveSettings() {
await this.saveData(this.settings);
}
registerFileMenuAction() {
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file, _source, leaf) => {
if (!this.isMarkdownFile(file)) {
return;
}
menu.addItem((item) => {
item.setTitle("Move note to new folder").setIcon("folder-plus").onClick(() => {
void this.runMoveFlow(file, leaf);
});
});
})
);
}
async runMoveFlow(file, leaf) {
var _a, _b;
const parentDefaultPath = this.settings.defaultToCurrentParent ? (_b = (_a = file.parent) == null ? void 0 : _a.path) != null ? _b : "" : "";
const pickedParent = await this.pickParentFolder(parentDefaultPath);
if (pickedParent === null) {
return;
}
const newFolderName = await this.promptForFolderName();
if (newFolderName === null) {
return;
}
const targetFolderPath = (0, import_obsidian.normalizePath)(
pickedParent.length > 0 ? `${pickedParent}/${newFolderName}` : newFolderName
);
const targetFolder = await this.ensureTargetFolder(targetFolderPath);
if (!targetFolder) {
return;
}
const targetFilePath = (0, import_obsidian.normalizePath)(`${targetFolder.path}/${file.name}`);
const existingDestination = this.app.vault.getAbstractFileByPath(targetFilePath);
if (existingDestination && existingDestination.path !== file.path) {
new import_obsidian.Notice(`Move canceled: file already exists at "${targetFilePath}".`);
return;
}
try {
await this.app.fileManager.renameFile(file, targetFilePath);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
new import_obsidian.Notice(`Could not move note: ${message}`);
return;
}
const movedFile = this.app.vault.getAbstractFileByPath(targetFilePath);
if (movedFile instanceof import_obsidian.TFile) {
await this.openMovedFile(movedFile, leaf);
new import_obsidian.Notice(`Moved to "${targetFolderPath}".`);
return;
}
new import_obsidian.Notice(`Move completed, but could not reopen "${targetFilePath}".`);
}
async ensureTargetFolder(targetFolderPath) {
const existing = this.app.vault.getAbstractFileByPath(targetFolderPath);
if (existing instanceof import_obsidian.TFolder) {
const confirmed = await this.confirmReuseFolder(targetFolderPath);
if (!confirmed) {
return null;
}
return existing;
}
if (existing) {
new import_obsidian.Notice(`Cannot use "${targetFolderPath}": a file already exists at that path.`);
return null;
}
try {
await this.app.vault.createFolder(targetFolderPath);
} catch (error) {
const raceWinner = this.app.vault.getAbstractFileByPath(targetFolderPath);
if (raceWinner instanceof import_obsidian.TFolder) {
return raceWinner;
}
const message = error instanceof Error ? error.message : String(error);
new import_obsidian.Notice(`Could not create folder: ${message}`);
return null;
}
const created = this.app.vault.getAbstractFileByPath(targetFolderPath);
if (created instanceof import_obsidian.TFolder) {
return created;
}
new import_obsidian.Notice(`Folder creation succeeded but "${targetFolderPath}" is unavailable.`);
return null;
}
async confirmReuseFolder(folderPath) {
return new Promise((resolve) => {
const modal = new ConfirmationModal(
this.app,
"Folder already exists",
`Use existing folder "${folderPath}"?`,
"Use folder",
resolve
);
modal.open();
});
}
async pickParentFolder(initialPath) {
return new Promise((resolve) => {
const modal = new ParentFolderPickerModal(this.app, initialPath, (result) => {
resolve(result.selectedPath);
});
modal.open();
});
}
async promptForFolderName() {
return new Promise((resolve) => {
const modal = new TextPromptModal(
this.app,
"Create a new folder",
"New folder name",
"Continue",
(result) => {
resolve(result.value);
}
);
modal.open();
});
}
async openMovedFile(file, leaf) {
const targetLeaf = leaf != null ? leaf : this.app.workspace.getMostRecentLeaf();
if (!targetLeaf) {
return;
}
await targetLeaf.openFile(file, { active: true });
}
getActiveMarkdownFileContext() {
const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") {
return null;
}
const leaf = this.app.workspace.getMostRecentLeaf();
return { file, leaf: leaf != null ? leaf : void 0 };
}
isMarkdownFile(file) {
return file instanceof import_obsidian.TFile && file.extension === "md";
}
};

597
main.ts Normal file
View file

@ -0,0 +1,597 @@
import {
App,
Menu,
Modal,
Notice,
Plugin,
PluginSettingTab,
Setting,
TAbstractFile,
TFile,
TFolder,
WorkspaceLeaf,
normalizePath,
} from "obsidian";
interface MoveToNewFolderSettings {
defaultToCurrentParent: boolean;
}
const DEFAULT_SETTINGS: MoveToNewFolderSettings = {
defaultToCurrentParent: true,
};
interface ParentFolderPickerResult {
selectedPath: string | null;
}
interface TextPromptResult {
value: string | null;
}
class ParentFolderPickerModal extends Modal {
private readonly initialPath: string;
private readonly onCloseResolve: (result: ParentFolderPickerResult) => void;
private searchValue = "";
private selectedIndex = 0;
private selectedPath: string;
private readonly folderPaths: string[];
private readonly listByPath: Map<string, HTMLButtonElement> = new Map();
constructor(app: App, initialPath: string, onCloseResolve: (result: ParentFolderPickerResult) => void) {
super(app);
this.initialPath = initialPath;
this.selectedPath = initialPath;
this.onCloseResolve = onCloseResolve;
this.folderPaths = this.collectFolderPaths();
if (!this.folderPaths.includes(this.selectedPath)) {
this.selectedPath = "";
}
}
onOpen(): void {
const { contentEl } = this;
this.modalEl.addClass("move-to-new-folder-modal");
contentEl.empty();
contentEl.createEl("h2", { text: "Choose a parent folder" });
contentEl.createEl("p", {
text: "Search by typing or pick a folder from the list.",
cls: "move-to-new-folder-hint",
});
const searchInput = contentEl.createEl("input", {
type: "text",
placeholder: "Search folders...",
cls: "move-to-new-folder-search",
});
const listEl = contentEl.createDiv({ cls: "move-to-new-folder-list" });
const render = (): void => {
listEl.empty();
this.listByPath.clear();
const filtered = this.getFilteredFolders();
if (filtered.length === 0) {
listEl.createDiv({
text: "No folders match your search.",
cls: "move-to-new-folder-empty",
});
return;
}
if (this.selectedIndex >= filtered.length) {
this.selectedIndex = 0;
}
const selectedByIndex = filtered[this.selectedIndex];
this.selectedPath = selectedByIndex;
for (const folderPath of filtered) {
const button = listEl.createEl("button", {
cls: "move-to-new-folder-item",
text: folderPath.length > 0 ? folderPath : "/",
});
button.type = "button";
button.dataset.path = folderPath;
if (folderPath === this.selectedPath) {
button.addClass("is-selected");
}
button.addEventListener("click", () => {
this.selectedPath = folderPath;
this.closeWithResult(folderPath);
});
button.addEventListener("mouseenter", () => {
const idx = filtered.indexOf(folderPath);
if (idx >= 0) {
this.selectedIndex = idx;
this.selectedPath = folderPath;
this.refreshSelection(listEl, filtered);
}
});
this.listByPath.set(folderPath, button);
}
this.refreshSelection(listEl, filtered);
};
searchInput.addEventListener("input", () => {
this.searchValue = searchInput.value.trim();
this.selectedIndex = 0;
render();
});
searchInput.addEventListener("keydown", (event) => {
const filtered = this.getFilteredFolders();
if (event.key === "ArrowDown") {
event.preventDefault();
if (filtered.length > 0) {
this.selectedIndex = Math.min(this.selectedIndex + 1, filtered.length - 1);
this.selectedPath = filtered[this.selectedIndex];
this.refreshSelection(listEl, filtered);
}
}
if (event.key === "ArrowUp") {
event.preventDefault();
if (filtered.length > 0) {
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
this.selectedPath = filtered[this.selectedIndex];
this.refreshSelection(listEl, filtered);
}
}
if (event.key === "Enter") {
event.preventDefault();
if (filtered.length > 0) {
this.closeWithResult(filtered[this.selectedIndex]);
}
}
});
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
const cancelButton = actionsEl.createEl("button", {
text: "Cancel",
cls: "mod-muted",
});
cancelButton.type = "button";
cancelButton.addEventListener("click", () => this.closeWithResult(null));
const chooseButton = actionsEl.createEl("button", {
text: "Choose",
cls: "mod-cta",
});
chooseButton.type = "button";
chooseButton.addEventListener("click", () => this.closeWithResult(this.selectedPath));
render();
searchInput.focus();
searchInput.select();
}
onClose(): void {
this.modalEl.removeClass("move-to-new-folder-modal");
this.contentEl.empty();
}
private collectFolderPaths(): string[] {
const folderSet = new Set<string>([""]);
for (const item of this.app.vault.getAllLoadedFiles()) {
if (item instanceof TFolder) {
folderSet.add(item.path);
}
}
return Array.from(folderSet).sort((a, b) => a.localeCompare(b));
}
private getFilteredFolders(): string[] {
if (!this.searchValue) {
return this.folderPaths;
}
const needle = this.searchValue.toLocaleLowerCase();
return this.folderPaths.filter((path) => {
const haystack = path.length > 0 ? path.toLocaleLowerCase() : "/";
return haystack.includes(needle);
});
}
private refreshSelection(listEl: HTMLElement, filtered: string[]): void {
for (const [path, button] of this.listByPath.entries()) {
const shouldSelect = path === this.selectedPath;
button.toggleClass("is-selected", shouldSelect);
if (shouldSelect) {
button.scrollIntoView({ block: "nearest" });
}
}
if (!filtered.includes(this.selectedPath) && filtered.length > 0) {
this.selectedIndex = 0;
this.selectedPath = filtered[0];
this.refreshSelection(listEl, filtered);
}
}
private closeWithResult(selectedPath: string | null): void {
this.onCloseResolve({ selectedPath });
this.close();
}
}
class TextPromptModal extends Modal {
private readonly title: string;
private readonly placeholder: string;
private readonly submitText: string;
private readonly onCloseResolve: (result: TextPromptResult) => void;
constructor(
app: App,
title: string,
placeholder: string,
submitText: string,
onCloseResolve: (result: TextPromptResult) => void,
) {
super(app);
this.title = title;
this.placeholder = placeholder;
this.submitText = submitText;
this.onCloseResolve = onCloseResolve;
}
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: this.title });
const input = contentEl.createEl("input", {
type: "text",
placeholder: this.placeholder,
});
const hintEl = contentEl.createDiv({
text: "Enter only a folder name, not a full path.",
cls: "move-to-new-folder-hint",
});
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
const cancelButton = actionsEl.createEl("button", {
text: "Cancel",
cls: "mod-muted",
});
cancelButton.type = "button";
cancelButton.addEventListener("click", () => this.closeWithResult(null));
const submitButton = actionsEl.createEl("button", {
text: this.submitText,
cls: "mod-cta",
});
submitButton.type = "button";
const submit = (): void => {
const value = input.value.trim();
if (!value) {
new Notice("Folder name cannot be empty.");
return;
}
if (value.includes("/") || value.includes("\\")) {
hintEl.setText("Folder name cannot contain path separators.");
return;
}
this.closeWithResult(value);
};
submitButton.addEventListener("click", submit);
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
submit();
}
});
input.focus();
input.select();
}
onClose(): void {
this.contentEl.empty();
}
private closeWithResult(value: string | null): void {
this.onCloseResolve({ value });
this.close();
}
}
class ConfirmationModal extends Modal {
private readonly titleText: string;
private readonly bodyText: string;
private readonly confirmText: string;
private readonly onCloseResolve: (confirmed: boolean) => void;
constructor(
app: App,
titleText: string,
bodyText: string,
confirmText: string,
onCloseResolve: (confirmed: boolean) => void,
) {
super(app);
this.titleText = titleText;
this.bodyText = bodyText;
this.confirmText = confirmText;
this.onCloseResolve = onCloseResolve;
}
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: this.titleText });
contentEl.createEl("p", { text: this.bodyText });
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
const cancelButton = actionsEl.createEl("button", {
text: "Cancel",
cls: "mod-muted",
});
cancelButton.type = "button";
cancelButton.addEventListener("click", () => this.closeWithResult(false));
const confirmButton = actionsEl.createEl("button", {
text: this.confirmText,
cls: "mod-cta",
});
confirmButton.type = "button";
confirmButton.addEventListener("click", () => this.closeWithResult(true));
}
onClose(): void {
this.contentEl.empty();
}
private closeWithResult(confirmed: boolean): void {
this.onCloseResolve(confirmed);
this.close();
}
}
class MoveToNewFolderSettingTab extends PluginSettingTab {
private readonly plugin: MoveToNewFolderPlugin;
constructor(app: App, plugin: MoveToNewFolderPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Move to New Folder settings" });
new Setting(containerEl)
.setName("Default parent to current note folder")
.setDesc("When enabled, the folder picker starts at the current note's parent folder.")
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.defaultToCurrentParent).onChange(async (value) => {
this.plugin.settings.defaultToCurrentParent = value;
await this.plugin.saveSettings();
});
});
}
}
export default class MoveToNewFolderPlugin extends Plugin {
settings: MoveToNewFolderSettings = DEFAULT_SETTINGS;
async onload(): Promise<void> {
await this.loadSettings();
this.addSettingTab(new MoveToNewFolderSettingTab(this.app, this));
this.addCommand({
id: "move-note-to-new-folder",
name: "Move note to new folder",
checkCallback: (checking) => {
const context = this.getActiveMarkdownFileContext();
if (!context) {
return false;
}
if (!checking) {
void this.runMoveFlow(context.file, context.leaf);
}
return true;
},
});
this.app.workspace.onLayoutReady(() => {
this.registerFileMenuAction();
});
}
async loadSettings(): Promise<void> {
const loaded = await this.loadData();
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded);
}
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
private registerFileMenuAction(): void {
this.registerEvent(
this.app.workspace.on("file-menu", (menu: Menu, file: TAbstractFile, _source: string, leaf?: WorkspaceLeaf) => {
if (!this.isMarkdownFile(file)) {
return;
}
menu.addItem((item) => {
item
.setTitle("Move note to new folder")
.setIcon("folder-plus")
.onClick(() => {
void this.runMoveFlow(file, leaf);
});
});
}),
);
}
private async runMoveFlow(file: TFile, leaf?: WorkspaceLeaf): Promise<void> {
const parentDefaultPath = this.settings.defaultToCurrentParent ? file.parent?.path ?? "" : "";
const pickedParent = await this.pickParentFolder(parentDefaultPath);
if (pickedParent === null) {
return;
}
const newFolderName = await this.promptForFolderName();
if (newFolderName === null) {
return;
}
const targetFolderPath = normalizePath(
pickedParent.length > 0 ? `${pickedParent}/${newFolderName}` : newFolderName,
);
const targetFolder = await this.ensureTargetFolder(targetFolderPath);
if (!targetFolder) {
return;
}
const targetFilePath = normalizePath(`${targetFolder.path}/${file.name}`);
const existingDestination = this.app.vault.getAbstractFileByPath(targetFilePath);
if (existingDestination && existingDestination.path !== file.path) {
new Notice(`Move canceled: file already exists at "${targetFilePath}".`);
return;
}
try {
await this.app.fileManager.renameFile(file, targetFilePath);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
new Notice(`Could not move note: ${message}`);
return;
}
const movedFile = this.app.vault.getAbstractFileByPath(targetFilePath);
if (movedFile instanceof TFile) {
await this.openMovedFile(movedFile, leaf);
new Notice(`Moved to "${targetFolderPath}".`);
return;
}
new Notice(`Move completed, but could not reopen "${targetFilePath}".`);
}
private async ensureTargetFolder(targetFolderPath: string): Promise<TFolder | null> {
const existing = this.app.vault.getAbstractFileByPath(targetFolderPath);
if (existing instanceof TFolder) {
const confirmed = await this.confirmReuseFolder(targetFolderPath);
if (!confirmed) {
return null;
}
return existing;
}
if (existing) {
new Notice(`Cannot use "${targetFolderPath}": a file already exists at that path.`);
return null;
}
try {
await this.app.vault.createFolder(targetFolderPath);
} catch (error) {
const raceWinner = this.app.vault.getAbstractFileByPath(targetFolderPath);
if (raceWinner instanceof TFolder) {
return raceWinner;
}
const message = error instanceof Error ? error.message : String(error);
new Notice(`Could not create folder: ${message}`);
return null;
}
const created = this.app.vault.getAbstractFileByPath(targetFolderPath);
if (created instanceof TFolder) {
return created;
}
new Notice(`Folder creation succeeded but "${targetFolderPath}" is unavailable.`);
return null;
}
private async confirmReuseFolder(folderPath: string): Promise<boolean> {
return new Promise((resolve) => {
const modal = new ConfirmationModal(
this.app,
"Folder already exists",
`Use existing folder "${folderPath}"?`,
"Use folder",
resolve,
);
modal.open();
});
}
private async pickParentFolder(initialPath: string): Promise<string | null> {
return new Promise((resolve) => {
const modal = new ParentFolderPickerModal(this.app, initialPath, (result) => {
resolve(result.selectedPath);
});
modal.open();
});
}
private async promptForFolderName(): Promise<string | null> {
return new Promise((resolve) => {
const modal = new TextPromptModal(
this.app,
"Create a new folder",
"New folder name",
"Continue",
(result) => {
resolve(result.value);
},
);
modal.open();
});
}
private async openMovedFile(file: TFile, leaf?: WorkspaceLeaf): Promise<void> {
const targetLeaf = leaf ?? this.app.workspace.getMostRecentLeaf();
if (!targetLeaf) {
return;
}
await targetLeaf.openFile(file, { active: true });
}
private getActiveMarkdownFileContext(): { file: TFile; leaf?: WorkspaceLeaf } | null {
const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") {
return null;
}
const leaf = this.app.workspace.getMostRecentLeaf();
return { file, leaf: leaf ?? undefined };
}
private isMarkdownFile(file: TAbstractFile): file is TFile {
return file instanceof TFile && file.extension === "md";
}
}

9
manifest.json Normal file
View file

@ -0,0 +1,9 @@
{
"id": "move-to-new-folder",
"name": "Move to New Folder",
"version": "1.0.0",
"minAppVersion": "1.5.0",
"description": "Move notes into newly created folders using a fast, step-by-step flow.",
"author": "d0d1",
"isDesktopOnly": false
}

665
package-lock.json generated Normal file
View file

@ -0,0 +1,665 @@
{
"name": "move-to-new-folder",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "move-to-new-folder",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.17.57",
"builtin-modules": "^4.0.0",
"esbuild": "^0.25.3",
"obsidian": "^1.8.10",
"tslib": "^2.8.1",
"typescript": "^5.8.3"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.38.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/codemirror": {
"version": "5.60.8",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/tern": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/builtin-modules": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/obsidian": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/codemirror": "5.60.8",
"moment": "2.29.4"
},
"peerDependencies": {
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.38.6"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"dev": true,
"license": "MIT",
"peer": true
}
}
}

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "move-to-new-folder",
"version": "1.0.0",
"description": "Obsidian plugin for a better move-to-new-folder workflow",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc --noEmit --skipLibCheck && node esbuild.config.mjs production"
},
"keywords": [
"obsidian",
"plugin"
],
"author": "d0d1",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.17.57",
"builtin-modules": "^4.0.0",
"esbuild": "^0.25.3",
"obsidian": "^1.8.10",
"tslib": "^2.8.1",
"typescript": "^5.8.3"
}
}

48
styles.css Normal file
View file

@ -0,0 +1,48 @@
.move-to-new-folder-modal {
width: min(640px, 92vw);
}
.move-to-new-folder-modal .move-to-new-folder-hint {
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.move-to-new-folder-modal .move-to-new-folder-search {
margin-bottom: 0.75rem;
}
.move-to-new-folder-modal .move-to-new-folder-list {
max-height: 320px;
overflow-y: auto;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: 0.25rem;
}
.move-to-new-folder-modal .move-to-new-folder-item {
width: 100%;
text-align: left;
border: 0;
background: transparent;
color: var(--text-normal);
border-radius: var(--radius-s);
padding: 0.35rem 0.45rem;
cursor: pointer;
}
.move-to-new-folder-modal .move-to-new-folder-item:hover,
.move-to-new-folder-modal .move-to-new-folder-item.is-selected {
background: var(--background-modifier-hover);
}
.move-to-new-folder-modal .move-to-new-folder-empty {
color: var(--text-muted);
padding: 0.5rem;
}
.move-to-new-folder-modal .move-to-new-folder-actions {
margin-top: 0.75rem;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}

22
tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES2020",
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "bundler",
"importHelpers": true,
"isolatedModules": true,
"strict": true,
"lib": [
"DOM",
"ES2020"
]
},
"include": [
"**/*.ts"
]
}

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "1.5.0"
}