diff --git a/README.md b/README.md index 890e435..7cd53a1 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,14 @@ Quickly add new pages or content to your vault. You can also do a [manual installation](docs/ManualInstallation.md). ## What's new? +### 3.6.10 +- Added a warning when deleting a Multi choice that you will delete all choices within. +- Fix #51 - Templater syntax is now processed when appending to the current file. +- Fix "Template (not found)" for templates that exist. +- Fix #46 - Error if adding a template that doesn't exist. +- Template: Create file in same folder as current file. +- Fix bug with insertion and creation of 'Insert After' line (if it does not exist). + ### 0.3.6 - 0.3.9 - Added setting to create the 'Insert After' line if it isn't found. - (HOTFIX) Escape regular expression special characters in Insert After when searching for it. diff --git a/manifest.json b/manifest.json index fdb52b2..08594e4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "quickadd", "name": "QuickAdd", - "version": "0.3.9", + "version": "0.3.10", "minAppVersion": "0.12.5", "description": "Quickly add new pages or content to your vault.", "author": "Christian B. B. Houmann", diff --git a/package.json b/package.json index b340917..e43509d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "quickadd", - "version": "0.3.9", + "version": "0.3.10", "description": "Quickly add new pages or content to your vault.", "main": "main.js", "scripts": { diff --git a/src/engine/CaptureChoiceEngine.ts b/src/engine/CaptureChoiceEngine.ts index 83d50dd..eb551bf 100644 --- a/src/engine/CaptureChoiceEngine.ts +++ b/src/engine/CaptureChoiceEngine.ts @@ -2,7 +2,7 @@ import type ICaptureChoice from "../types/choices/ICaptureChoice"; import type {App, TFile} from "obsidian"; import {log} from "../logger/logManager"; import {CaptureChoiceFormatter} from "../formatters/captureChoiceFormatter"; -import {appendToCurrentLine, replaceTemplaterTemplatesInCreatedFile} from "../utility"; +import {appendToCurrentLine, replaceTemplaterTemplatesInCreatedFile, templaterParseTemplate} from "../utility"; import {VALUE_SYNTAX} from "../constants"; import type QuickAdd from "../main"; import {QuickAddChoiceEngine} from "./QuickAddChoiceEngine"; @@ -96,20 +96,21 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine { } private async captureToActiveFile() { + const activeFile: TFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + log.logError("Cannot capture to active file - no active file.") + } + let content: string = await this.getCaptureContent(); if (this.choice.format.enabled) { content = await this.formatter.formatContent(content, this.choice); + content = await templaterParseTemplate(this.app, content, activeFile); } if (!content) return; if (this.choice.prepend) { - const activeFile: TFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - log.logError("Cannot capture to active file - no active file.") - } - const fileContent: string = await this.app.vault.cachedRead(activeFile); const newFileContent: string = `${fileContent}${content}` diff --git a/src/engine/SingleTemplateEngine.ts b/src/engine/SingleTemplateEngine.ts index e00273a..5eb5cba 100644 --- a/src/engine/SingleTemplateEngine.ts +++ b/src/engine/SingleTemplateEngine.ts @@ -2,6 +2,7 @@ import {TemplateEngine} from "./TemplateEngine"; import type {App} from "obsidian"; import type QuickAdd from "../main"; import type {IChoiceExecutor} from "../IChoiceExecutor"; +import {log} from "../logger/logManager"; export class SingleTemplateEngine extends TemplateEngine { constructor(app: App, plugin: QuickAdd, private templatePath: string, choiceExecutor: IChoiceExecutor) { @@ -10,7 +11,7 @@ export class SingleTemplateEngine extends TemplateEngine { public async run(): Promise { let templateContent: string = await this.getTemplateContent(this.templatePath); if (!templateContent) { - throw new Error(`Template ${this.templatePath} not found.`); + log.logError(`Template ${this.templatePath} not found.`); } templateContent = await this.formatter.formatFileContent(templateContent); diff --git a/src/engine/TemplateChoiceEngine.ts b/src/engine/TemplateChoiceEngine.ts index c861de9..ac69e0f 100644 --- a/src/engine/TemplateChoiceEngine.ts +++ b/src/engine/TemplateChoiceEngine.ts @@ -1,7 +1,7 @@ import type ITemplateChoice from "../types/choices/ITemplateChoice"; import type {App} from "obsidian"; import {TFile} from "obsidian"; -import {appendToCurrentLine, getAllFolders} from "../utility"; +import {appendToCurrentLine, getAllFolderPathsInVault} from "../utility"; import { fileExistsAppendToBottom, fileExistsAppendToTop, @@ -28,13 +28,7 @@ export class TemplateChoiceEngine extends TemplateEngine { let folderPath: string = ""; if (this.choice.folder.enabled) { - let folders: string[] = this.choice.folder.folders; - - if (this.choice.folder?.chooseWhenCreatingNote) { - folders = await getAllFolders(this.app); - } - - folderPath = await this.getOrCreateFolder(folders); + folderPath = await this.getFolderPath(); } let filePath; @@ -96,4 +90,24 @@ export class TemplateChoiceEngine extends TemplateEngine { } } } + + private async getFolderPath() { + let folders: string[] = [...this.choice.folder.folders]; + + if (this.choice.folder?.chooseWhenCreatingNote) { + const allFoldersInVault: string[] = getAllFolderPathsInVault(this.app); + return await this.getOrCreateFolder(allFoldersInVault); + } + + if (this.choice.folder?.createInSameFolderAsActiveFile) { + const activeFile: TFile = this.app.workspace.getActiveFile(); + + if (!activeFile) + log.logError("No active file. Cannot create new file."); + + return this.getOrCreateFolder([activeFile.parent.path]); + } + + return await this.getOrCreateFolder(folders); + } } diff --git a/src/formatters/captureChoiceFormatter.ts b/src/formatters/captureChoiceFormatter.ts index cf35ce9..97e5edf 100644 --- a/src/formatters/captureChoiceFormatter.ts +++ b/src/formatters/captureChoiceFormatter.ts @@ -5,7 +5,7 @@ import {log} from "../logger/logManager"; import type QuickAdd from "../main"; import type {IChoiceExecutor} from "../IChoiceExecutor"; import {escapeRegExp, getLinesInString, templaterParseTemplate} from "../utility"; -import {CREATE_IF_NOT_FOUND_TOP} from "../constants"; +import {CREATE_IF_NOT_FOUND_BOTTOM, CREATE_IF_NOT_FOUND_TOP} from "../constants"; export class CaptureChoiceFormatter extends CompleteFormatter { private choice: ICaptureChoice; @@ -47,60 +47,72 @@ export class CaptureChoiceFormatter extends CompleteFormatter { return `${this.fileContent}\n${formatted}` if (this.choice.insertAfter.enabled) { - const target: string = await this.format(this.choice.insertAfter.after); - const targetRegex = new RegExp(`\s*${escapeRegExp(target)}\s*`) - let fileContentLines: string[] = getLinesInString(this.fileContent); - - const targetPosition = fileContentLines.findIndex(line => targetRegex.test(line)); - if (targetPosition === -1) { - if (this.choice.insertAfter?.createIfNotFound) { - const insertAfterLine: string = this.replaceLinebreakInString(await this.format(this.choice.insertAfter.after)); - const insertAfterLineAndFormatted: string = `${insertAfterLine}\n${formatted}`; - - if (this.choice.insertAfter?.createIfNotFoundLocation === CREATE_IF_NOT_FOUND_TOP) { - const frontmatterEndPosition = this.file ? await this.getFrontmatterEndPosition(this.file) : 0; - return this.insertTextAfterPositionInBody(insertAfterLineAndFormatted, this.fileContent, frontmatterEndPosition); - } - else { - return this.insertTextAfterPositionInBody(insertAfterLineAndFormatted, this.fileContent, fileContentLines.length - 1); - } - } + return await this.insertAfterHandler(formatted); + } - log.logError("unable to find insert after line in file.") + const frontmatterEndPosition = this.file ? await this.getFrontmatterEndPosition(this.file) : null; + if (!frontmatterEndPosition) + return `${formatted}${this.fileContent}`; + + return this.insertTextAfterPositionInBody(formatted, this.fileContent, frontmatterEndPosition); + } + + private async insertAfterHandler(formatted: string) { + const targetString: string = await this.format(this.choice.insertAfter.after); + const targetRegex = new RegExp(`\s*${escapeRegExp(targetString.replace('\\n', ''))}\s*`); + let fileContentLines: string[] = getLinesInString(this.fileContent); + + const targetPosition = fileContentLines.findIndex(line => targetRegex.test(line)); + const targetNotFound = targetPosition === -1; + if (targetNotFound) { + if (this.choice.insertAfter?.createIfNotFound) { + return await this.createInsertAfterIfNotFound(formatted); } - if (this.choice.insertAfter?.insertAtEnd) { - const nextHeaderPositionAfterTargetPosition = fileContentLines.slice(targetPosition + 1).findIndex(line => (/^#+ |---/).test(line)) - const foundNextHeader = nextHeaderPositionAfterTargetPosition !== -1; + log.logError("unable to find insert after line in file.") + } + + if (this.choice.insertAfter?.insertAtEnd) { + const nextHeaderPositionAfterTargetPosition = fileContentLines + .slice(targetPosition + 1) + .findIndex(line => (/^#+ |---/).test(line)) + const foundNextHeader = nextHeaderPositionAfterTargetPosition !== -1; - if (foundNextHeader) { - let endOfSectionIndex: number; + if (foundNextHeader) { + let endOfSectionIndex: number; - for (let i = nextHeaderPositionAfterTargetPosition + targetPosition; i > targetPosition; i--) { - const lineIsNewline: boolean = (/^[\s\n ]*$/).test(fileContentLines[i]); + for (let i = nextHeaderPositionAfterTargetPosition + targetPosition; i > targetPosition; i--) { + const lineIsNewline: boolean = (/^[\s\n ]*$/).test(fileContentLines[i]); - if (!lineIsNewline) { - endOfSectionIndex = i; - break; - } + if (!lineIsNewline) { + endOfSectionIndex = i; + break; } + } - if (!endOfSectionIndex) endOfSectionIndex = targetPosition; + if (!endOfSectionIndex) endOfSectionIndex = targetPosition; - return this.insertTextAfterPositionInBody(formatted, this.fileContent, endOfSectionIndex); - } else { - return this.insertTextAfterPositionInBody(formatted, this.fileContent, fileContentLines.length - 1); - } + return this.insertTextAfterPositionInBody(formatted, this.fileContent, endOfSectionIndex); + } else { + return this.insertTextAfterPositionInBody(formatted, this.fileContent, fileContentLines.length - 1); } - - return this.insertTextAfterPositionInBody(formatted, this.fileContent, targetPosition); } - const frontmatterEndPosition = this.file ? await this.getFrontmatterEndPosition(this.file) : null; - if (!frontmatterEndPosition) - return `${formatted}${this.fileContent}`; + return this.insertTextAfterPositionInBody(formatted, this.fileContent, targetPosition); + } - return this.insertTextAfterPositionInBody(formatted, this.fileContent, frontmatterEndPosition); + private async createInsertAfterIfNotFound(formatted: string) { + const insertAfterLine: string = this.replaceLinebreakInString(await this.format(this.choice.insertAfter.after)); + const insertAfterLineAndFormatted: string = `${insertAfterLine}\n${formatted}`; + + if (this.choice.insertAfter?.createIfNotFoundLocation === CREATE_IF_NOT_FOUND_TOP) { + const frontmatterEndPosition = this.file ? await this.getFrontmatterEndPosition(this.file) : -1; + return this.insertTextAfterPositionInBody(insertAfterLineAndFormatted, this.fileContent, frontmatterEndPosition); + } + + if (this.choice.insertAfter?.createIfNotFoundLocation === CREATE_IF_NOT_FOUND_BOTTOM) { + return `${this.fileContent}\n${insertAfterLineAndFormatted}`; + } } private async getFrontmatterEndPosition(file: TFile) { @@ -108,13 +120,13 @@ export class CaptureChoiceFormatter extends CompleteFormatter { if (!fileCache || !fileCache.frontmatter) { log.logMessage("could not get frontmatter. Maybe there isn't any.") - return 0; + return -1; } if (fileCache.frontmatter.position) return fileCache.frontmatter.position.end.line; - return 0; + return -1; } private insertTextAfterPositionInBody(text: string, body: string, pos: number): string { diff --git a/src/formatters/completeFormatter.ts b/src/formatters/completeFormatter.ts index a84d846..6d65234 100644 --- a/src/formatters/completeFormatter.ts +++ b/src/formatters/completeFormatter.ts @@ -16,7 +16,7 @@ export class CompleteFormatter extends Formatter { constructor(protected app: App, private plugin: QuickAdd, protected choiceExecutor: IChoiceExecutor) { super(); - this.variables = choiceExecutor.variables; + this.variables = choiceExecutor?.variables; } protected async format(input: string): Promise { diff --git a/src/gui/ChoiceBuilder/captureChoiceBuilder.ts b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts index b6b3c4b..dc45977 100644 --- a/src/gui/ChoiceBuilder/captureChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts @@ -179,10 +179,10 @@ export class CaptureChoiceBuilder extends ChoiceBuilder { this.choice.insertAfter.createIfNotFoundLocation = CREATE_IF_NOT_FOUND_TOP; // Set to default dropdown - .setValue(this.choice.insertAfter?.createIfNotFoundLocation) - .onChange(value => this.choice.insertAfter.createIfNotFoundLocation = value) .addOption(CREATE_IF_NOT_FOUND_TOP, "Top") .addOption(CREATE_IF_NOT_FOUND_BOTTOM, "Bottom") + .setValue(this.choice.insertAfter?.createIfNotFoundLocation) + .onChange(value => this.choice.insertAfter.createIfNotFoundLocation = value) } ) } diff --git a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts index 6f94ce6..203ef69 100644 --- a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts @@ -7,7 +7,7 @@ import FolderList from "./FolderList.svelte"; import {FileNameDisplayFormatter} from "../../formatters/fileNameDisplayFormatter"; import {ExclusiveSuggester} from "../exclusiveSuggester"; import {log} from "../../logger/logManager"; -import {getAllFolders, getTemplatePaths} from "../../utility"; +import {getAllFolderPathsInVault, getTemplatePaths} from "../../utility"; import {GenericTextSuggester} from "../genericTextSuggester"; import type QuickAdd from "../../main"; @@ -95,68 +95,87 @@ export class TemplateChoiceBuilder extends ChoiceBuilder { }); if (this.choice.folder.enabled) { - const chooseFolderWhenCreatingNoteContainer: HTMLDivElement = this.contentEl.createDiv('chooseFolderWhenCreatingNoteContainer'); - chooseFolderWhenCreatingNoteContainer.createEl('span', {text: "Choose folder when creating a new note"}); - const chooseFolderWhenCreatingNote: ToggleComponent = new ToggleComponent(chooseFolderWhenCreatingNoteContainer); - chooseFolderWhenCreatingNote.setValue(this.choice.folder?.chooseWhenCreatingNote) - .onChange(value => { - this.choice.folder.chooseWhenCreatingNote = value; - this.reload(); - }); + if (!this.choice.folder?.createInSameFolderAsActiveFile) { + const chooseFolderWhenCreatingNoteContainer: HTMLDivElement = this.contentEl.createDiv('chooseFolderWhenCreatingNoteContainer'); + chooseFolderWhenCreatingNoteContainer.createEl('span', {text: "Choose folder when creating a new note"}); + const chooseFolderWhenCreatingNote: ToggleComponent = new ToggleComponent(chooseFolderWhenCreatingNoteContainer); + chooseFolderWhenCreatingNote.setValue(this.choice.folder?.chooseWhenCreatingNote) + .onChange(value => { + this.choice.folder.chooseWhenCreatingNote = value; + this.reload(); + }); + + if (!this.choice.folder?.chooseWhenCreatingNote) { + this.addFolderSelector(); + } + } if (!this.choice.folder?.chooseWhenCreatingNote) { - const folderSelectionContainer: HTMLDivElement = this.contentEl.createDiv('folderSelectionContainer'); - const folderList: HTMLDivElement = folderSelectionContainer.createDiv('folderList'); - - const folderListEl = new FolderList({ - target: folderList, - props: { - folders: this.choice.folder.folders, - deleteFolder: (folder: string) => { - this.choice.folder.folders = this.choice.folder.folders.filter(f => f !== folder); - folderListEl.updateFolders(this.choice.folder.folders); - suggester.updateCurrentItems(this.choice.folder.folders); - } - } - }); + const createInSameFolderAsActiveFileSetting: Setting = new Setting(this.contentEl); + createInSameFolderAsActiveFileSetting.setName("Create in same folder as active file") + .setDesc("Creates the file in the same folder as the currently active file. Will not create the file if there is no active file.") + .addToggle(toggle => toggle + .setValue(this.choice.folder?.createInSameFolderAsActiveFile) + .onChange(value => { + this.choice.folder.createInSameFolderAsActiveFile = value; + this.reload(); + }) + ) + } + } + } + + private addFolderSelector() { + const folderSelectionContainer: HTMLDivElement = this.contentEl.createDiv('folderSelectionContainer'); + const folderList: HTMLDivElement = folderSelectionContainer.createDiv('folderList'); - this.svelteElements.push(folderListEl); + const folderListEl = new FolderList({ + target: folderList, + props: { + folders: this.choice.folder.folders, + deleteFolder: (folder: string) => { + this.choice.folder.folders = this.choice.folder.folders.filter(f => f !== folder); + folderListEl.updateFolders(this.choice.folder.folders); + suggester.updateCurrentItems(this.choice.folder.folders); + } + } + }); - const inputContainer = folderSelectionContainer.createDiv('folderInputContainer'); - const folderInput = new TextComponent(inputContainer); - folderInput.inputEl.style.width = "100%"; - folderInput.setPlaceholder("Folder path"); - const allFolders: string[] = getAllFolders(this.app); + this.svelteElements.push(folderListEl); - const suggester = new ExclusiveSuggester(this.app, folderInput.inputEl, allFolders, this.choice.folder.folders); + const inputContainer = folderSelectionContainer.createDiv('folderInputContainer'); + const folderInput = new TextComponent(inputContainer); + folderInput.inputEl.style.width = "100%"; + folderInput.setPlaceholder("Folder path"); + const allFolders: string[] = getAllFolderPathsInVault(this.app); - const addFolder = () => { - const input = folderInput.inputEl.value.trim(); + const suggester = new ExclusiveSuggester(this.app, folderInput.inputEl, allFolders, this.choice.folder.folders); - if (this.choice.folder.folders.some(folder => folder === input)) { - log.logWarning("cannot add same folder twice."); - return; - } + const addFolder = () => { + const input = folderInput.inputEl.value.trim(); - this.choice.folder.folders.push(input); - folderListEl.updateFolders(this.choice.folder.folders); - folderInput.inputEl.value = ""; + if (this.choice.folder.folders.some(folder => folder === input)) { + log.logWarning("cannot add same folder twice."); + return; + } - suggester.updateCurrentItems(this.choice.folder.folders); - } + this.choice.folder.folders.push(input); + folderListEl.updateFolders(this.choice.folder.folders); + folderInput.inputEl.value = ""; - folderInput.inputEl.addEventListener('keypress', (e: KeyboardEvent) => { - if (e.key === 'Enter') { - addFolder(); - } - }); + suggester.updateCurrentItems(this.choice.folder.folders); + } - const addButton: ButtonComponent = new ButtonComponent(inputContainer); - addButton.setCta().setButtonText("Add").onClick(evt => { - addFolder(); - }); + folderInput.inputEl.addEventListener('keypress', (e: KeyboardEvent) => { + if (e.key === 'Enter') { + addFolder(); } - } + }); + + const addButton: ButtonComponent = new ButtonComponent(inputContainer); + addButton.setCta().setButtonText("Add").onClick(evt => { + addFolder(); + }); } private addAppendLinkSetting(): void { diff --git a/src/gui/choiceList/ChoiceView.svelte b/src/gui/choiceList/ChoiceView.svelte index c95a968..ee5047a 100644 --- a/src/gui/choiceList/ChoiceView.svelte +++ b/src/gui/choiceList/ChoiceView.svelte @@ -57,7 +57,11 @@ const choice: IChoice = e.detail.choice; const userConfirmed: boolean = await GenericYesNoPrompt.Prompt(app, - `Confirm deletion of choice`, `Please confirm that you wish to delete '${choice.name}.'`); + `Confirm deletion of choice`, `Please confirm that you wish to delete '${choice.name}'. + ${choice.type === ChoiceType.Multi + ? "Deleting this choice will delete all (" + (choice as IMultiChoice).choices.length + ") choices inside it!" + : ""} + `); if (userConfirmed) { choices = choices.filter((value) => deleteChoiceHelper(choice.id, value)); diff --git a/src/types/choices/ITemplateChoice.ts b/src/types/choices/ITemplateChoice.ts index 8c1b403..762178e 100644 --- a/src/types/choices/ITemplateChoice.ts +++ b/src/types/choices/ITemplateChoice.ts @@ -3,7 +3,7 @@ import type {NewTabDirection} from "../newTabDirection"; export default interface ITemplateChoice extends IChoice { templatePath: string; - folder: { enabled: boolean, folders: string[], chooseWhenCreatingNote: boolean } + folder: { enabled: boolean, folders: string[], chooseWhenCreatingNote: boolean, createInSameFolderAsActiveFile: boolean } fileNameFormat: { enabled: boolean, format: string }; appendLink: boolean; incrementFileName: boolean; diff --git a/src/types/choices/TemplateChoice.ts b/src/types/choices/TemplateChoice.ts index 0ac1ca0..7e443ed 100644 --- a/src/types/choices/TemplateChoice.ts +++ b/src/types/choices/TemplateChoice.ts @@ -6,7 +6,7 @@ import {NewTabDirection} from "../newTabDirection"; export class TemplateChoice extends Choice implements ITemplateChoice { appendLink: boolean; fileNameFormat: { enabled: boolean; format: string }; - folder: { enabled: boolean; folders: string[], chooseWhenCreatingNote: boolean }; + folder: { enabled: boolean; folders: string[], chooseWhenCreatingNote: boolean, createInSameFolderAsActiveFile: boolean }; incrementFileName: boolean; openFileInNewTab: { enabled: boolean; direction: NewTabDirection }; openFile: boolean; @@ -17,7 +17,7 @@ export class TemplateChoice extends Choice implements ITemplateChoice { this.templatePath = ""; this.fileNameFormat = {enabled: false, format: ""}; - this.folder = {enabled: false, folders: [], chooseWhenCreatingNote: false}; + this.folder = {enabled: false, folders: [], chooseWhenCreatingNote: false, createInSameFolderAsActiveFile: false}; this.openFileInNewTab = {enabled: false, direction: NewTabDirection.vertical}; this.appendLink = false; this.incrementFileName = false; diff --git a/src/utility.ts b/src/utility.ts index b6b54c9..a7984d1 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -105,7 +105,7 @@ export function deleteObsidianCommand(app: App, commandId: string) { } } -export function getAllFolders(app: App): string[] { +export function getAllFolderPathsInVault(app: App): string[] { return app.vault.getAllLoadedFiles() .filter(f => f instanceof TFolder) .map(folder => folder.path); diff --git a/versions.json b/versions.json index c59d9c8..e874d6a 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,4 @@ { "0.2.16": "0.12.4", - "0.3.9": "0.12.5" + "0.3.10": "0.12.5" }