From 9aabf16370911be515677dfcda99411cf87f0cca Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Fri, 24 Mar 2023 11:44:32 +0100 Subject: [PATCH 01/11] WIP capture refactor --- src/engine/CaptureChoiceEngine.ts | 24 +- src/engine/MacroChoiceEngine.ts | 2 +- src/engine/SingleMacroEngine.ts | 2 +- src/engine/TemplateChoiceEngine.ts | 2 +- src/engine/TemplateEngine.ts | 2 +- src/formatters/captureChoiceFormatter.ts | 24 +- src/formatters/completeFormatter.ts | 8 +- src/formatters/fileNameDisplayFormatter.ts | 2 +- src/formatters/formatDisplayFormatter.ts | 2 +- src/formatters/formatter.ts | 42 ++-- src/formatters/helpers/insertAfter.test.ts | 23 ++ src/formatters/helpers/insertAfter.ts | 84 +++++++ .../ChoiceBuilder/templateChoiceBuilder.ts | 2 +- src/gui/MacroGUIs/MacroBuilder.ts | 8 +- src/main.ts | 4 +- src/quickAddApi.ts | 10 +- src/utility.ts | 231 +----------------- src/utilityObsidian.ts | 226 +++++++++++++++++ 18 files changed, 413 insertions(+), 285 deletions(-) create mode 100644 src/formatters/helpers/insertAfter.test.ts create mode 100644 src/formatters/helpers/insertAfter.ts create mode 100644 src/utilityObsidian.ts diff --git a/src/engine/CaptureChoiceEngine.ts b/src/engine/CaptureChoiceEngine.ts index 13d466e..65ef762 100644 --- a/src/engine/CaptureChoiceEngine.ts +++ b/src/engine/CaptureChoiceEngine.ts @@ -1,5 +1,5 @@ import type ICaptureChoice from "../types/choices/ICaptureChoice"; -import { TFile } from "obsidian"; +import type { TFile } from "obsidian"; import type { App } from "obsidian"; import { log } from "../logger/logManager"; import { CaptureChoiceFormatter } from "../formatters/captureChoiceFormatter"; @@ -8,7 +8,7 @@ import { openFile, replaceTemplaterTemplatesInCreatedFile, templaterParseTemplate, -} from "../utility"; +} from "../utilityObsidian"; import { VALUE_SYNTAX } from "../constants"; import type QuickAdd from "../main"; import { QuickAddChoiceEngine } from "./QuickAddChoiceEngine"; @@ -55,14 +55,18 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine { }); const filePath = await this.formatFilePath(captureTo); - const content = await this.getCaptureContent(); + const content = this.getCaptureContent(); let getFileAndAddContentFn: typeof this.onFileExists; if (await this.fileExists(filePath)) { - getFileAndAddContentFn = this.onFileExists; + getFileAndAddContentFn = this.onFileExists.bind( + this + ) as typeof this.onFileExists; } else if (this.choice?.createFileIfItDoesntExist?.enabled) { - getFileAndAddContentFn = this.onCreateFileIfItDoesntExist; + getFileAndAddContentFn = this.onCreateFileIfItDoesntExist.bind( + this + ) as typeof this.onCreateFileIfItDoesntExist; } else { log.logWarning( `The file ${filePath} does not exist and "Create file if it doesn't exist" is disabled.` @@ -71,7 +75,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine { } const { file, content: newFileContent } = - await getFileAndAddContentFn.bind(this)(filePath, content); + await getFileAndAddContentFn(filePath, content); await this.app.vault.modify(file, newFileContent); @@ -92,11 +96,11 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine { }); } } catch (e) { - log.logError(e); + log.logError(e as string); } } - private async getCaptureContent(): Promise { + private getCaptureContent(): string { let content: string; if (!this.choice.format.enabled) content = VALUE_SYNTAX; @@ -145,7 +149,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine { `\nThis is in order to prevent data loss.` ); - newFileContent = res.joinedResults(); + newFileContent = res.joinedResults() as string; } return { file, content: newFileContent }; @@ -206,7 +210,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine { return; } - let content: string = await this.getCaptureContent(); + let content: string = this.getCaptureContent(); content = await this.formatter.formatContent(content, this.choice); if (this.choice.format.enabled) { diff --git a/src/engine/MacroChoiceEngine.ts b/src/engine/MacroChoiceEngine.ts index 80da44e..b790416 100644 --- a/src/engine/MacroChoiceEngine.ts +++ b/src/engine/MacroChoiceEngine.ts @@ -13,7 +13,7 @@ import GenericSuggester from "../gui/GenericSuggester/genericSuggester"; import type { IChoiceCommand } from "../types/macros/IChoiceCommand"; import type QuickAdd from "../main"; import type { IChoiceExecutor } from "../IChoiceExecutor"; -import { getUserScript, waitFor } from "../utility"; +import { getUserScript, waitFor } from "../utilityObsidian"; import type { IWaitCommand } from "../types/macros/QuickCommands/IWaitCommand"; import type { INestedChoiceCommand } from "../types/macros/QuickCommands/INestedChoiceCommand"; import type IChoice from "../types/choices/IChoice"; diff --git a/src/engine/SingleMacroEngine.ts b/src/engine/SingleMacroEngine.ts index 9e6d5bf..31cfd4c 100644 --- a/src/engine/SingleMacroEngine.ts +++ b/src/engine/SingleMacroEngine.ts @@ -3,7 +3,7 @@ import type { IMacro } from "../types/macros/IMacro"; import { MacroChoiceEngine } from "./MacroChoiceEngine"; import type QuickAdd from "../main"; import type { IChoiceExecutor } from "../IChoiceExecutor"; -import { getUserScriptMemberAccess } from "../utility"; +import { getUserScriptMemberAccess } from "../utilityObsidian"; import { log } from "../logger/logManager"; export class SingleMacroEngine extends MacroChoiceEngine { diff --git a/src/engine/TemplateChoiceEngine.ts b/src/engine/TemplateChoiceEngine.ts index ee7014e..973b522 100644 --- a/src/engine/TemplateChoiceEngine.ts +++ b/src/engine/TemplateChoiceEngine.ts @@ -5,7 +5,7 @@ import { appendToCurrentLine, getAllFolderPathsInVault, openFile, -} from "../utility"; +} from "../utilityObsidian"; import { fileExistsAppendToBottom, fileExistsAppendToTop, diff --git a/src/engine/TemplateEngine.ts b/src/engine/TemplateEngine.ts index 8386a8e..27c834a 100644 --- a/src/engine/TemplateEngine.ts +++ b/src/engine/TemplateEngine.ts @@ -5,7 +5,7 @@ import type QuickAdd from "../main"; import { getTemplater, replaceTemplaterTemplatesInCreatedFile, -} from "../utility"; +} from "../utilityObsidian"; import GenericSuggester from "../gui/GenericSuggester/genericSuggester"; import { FILE_NUMBER_REGEX, MARKDOWN_FILE_EXTENSION_REGEX } from "../constants"; import { log } from "../logger/logManager"; diff --git a/src/formatters/captureChoiceFormatter.ts b/src/formatters/captureChoiceFormatter.ts index aa8b1ca..face69e 100644 --- a/src/formatters/captureChoiceFormatter.ts +++ b/src/formatters/captureChoiceFormatter.ts @@ -5,14 +5,14 @@ import { log } from "../logger/logManager"; import type QuickAdd from "../main"; import type { IChoiceExecutor } from "../IChoiceExecutor"; import { - escapeRegExp, - getLinesInString, templaterParseTemplate, -} from "../utility"; +} from "../utilityObsidian"; import { CREATE_IF_NOT_FOUND_BOTTOM, CREATE_IF_NOT_FOUND_TOP, } from "../constants"; +import insertAfter from "./helpers/insertAfter"; +import { escapeRegExp, getLinesInString } from "src/utility"; export class CaptureChoiceFormatter extends CompleteFormatter { private choice: ICaptureChoice; @@ -40,7 +40,7 @@ export class CaptureChoiceFormatter extends CompleteFormatter { formatted, this.file ); - if (!templaterFormatted) return formatted; + if (!(await templaterFormatted)) return formatted; return templaterFormatted; } @@ -74,7 +74,7 @@ export class CaptureChoiceFormatter extends CompleteFormatter { } const frontmatterEndPosition = this.file - ? await this.getFrontmatterEndPosition(this.file) + ? this.getFrontmatterEndPosition(this.file) : null; if (!frontmatterEndPosition) return `${formatted}${this.fileContent}`; @@ -99,6 +99,14 @@ export class CaptureChoiceFormatter extends CompleteFormatter { const targetString: string = await this.format( this.choice.insertAfter.after ); + + const target = targetString; + const value = formatted; + const body = this.fileContent; + insertAfter(target, value, body, { + insertAtEndOfSection: this.choice.insertAfter.insertAtEnd, + }); + const targetRegex = new RegExp( `\\s*${escapeRegExp(targetString.replace("\\n", ""))}\\s*` ); @@ -175,7 +183,7 @@ export class CaptureChoiceFormatter extends CompleteFormatter { CREATE_IF_NOT_FOUND_TOP ) { const frontmatterEndPosition = this.file - ? await this.getFrontmatterEndPosition(this.file) + ? this.getFrontmatterEndPosition(this.file) : -1; return this.insertTextAfterPositionInBody( insertAfterLineAndFormatted, @@ -192,8 +200,8 @@ export class CaptureChoiceFormatter extends CompleteFormatter { } } - private async getFrontmatterEndPosition(file: TFile) { - const fileCache = await this.app.metadataCache.getFileCache(file); + private getFrontmatterEndPosition(file: TFile) { + const fileCache = this.app.metadataCache.getFileCache(file); if (!fileCache || !fileCache.frontmatter) { log.logMessage("could not get frontmatter. Maybe there isn't any."); diff --git a/src/formatters/completeFormatter.ts b/src/formatters/completeFormatter.ts index 3089e7f..22ac540 100644 --- a/src/formatters/completeFormatter.ts +++ b/src/formatters/completeFormatter.ts @@ -1,6 +1,6 @@ import { Formatter } from "./formatter"; import type { App } from "obsidian"; -import { getNaturalLanguageDates } from "../utility"; +import { getNaturalLanguageDates } from "../utilityObsidian"; import GenericSuggester from "../gui/GenericSuggester/genericSuggester"; import type QuickAdd from "../main"; import { SingleMacroEngine } from "../engine/SingleMacroEngine"; @@ -109,13 +109,13 @@ export class CompleteFormatter extends Formatter { } protected async suggestForField(variableName: string) { - const suggestedValues = new Set() + const suggestedValues = new Set(); for (const file of this.app.vault.getMarkdownFiles()) { const cache = this.app.metadataCache.getFileCache(file); const value = cache?.frontmatter?.[variableName]; if (!value || typeof value == "object") continue; - + suggestedValues.add(value.toString()); } @@ -134,7 +134,7 @@ export class CompleteFormatter extends Formatter { suggestedValuesArr, suggestedValuesArr, { - placeholder: `Enter value for ${variableName}` + placeholder: `Enter value for ${variableName}`, } ); } diff --git a/src/formatters/fileNameDisplayFormatter.ts b/src/formatters/fileNameDisplayFormatter.ts index aa697db..ab505b6 100644 --- a/src/formatters/fileNameDisplayFormatter.ts +++ b/src/formatters/fileNameDisplayFormatter.ts @@ -1,6 +1,6 @@ import { Formatter } from "./formatter"; import type { App } from "obsidian"; -import { getNaturalLanguageDates } from "../utility"; +import { getNaturalLanguageDates } from "../utilityObsidian"; export class FileNameDisplayFormatter extends Formatter { constructor(private app: App) { diff --git a/src/formatters/formatDisplayFormatter.ts b/src/formatters/formatDisplayFormatter.ts index 8a5fb13..e037205 100644 --- a/src/formatters/formatDisplayFormatter.ts +++ b/src/formatters/formatDisplayFormatter.ts @@ -1,6 +1,6 @@ import { Formatter } from "./formatter"; import type { App } from "obsidian"; -import { getNaturalLanguageDates } from "../utility"; +import { getNaturalLanguageDates } from "../utilityObsidian"; import type QuickAdd from "../main"; import { SingleTemplateEngine } from "../engine/SingleTemplateEngine"; diff --git a/src/formatters/formatter.ts b/src/formatters/formatter.ts index 99d5f73..cc1d0c9 100644 --- a/src/formatters/formatter.ts +++ b/src/formatters/formatter.ts @@ -10,9 +10,9 @@ import { NUMBER_REGEX, TEMPLATE_REGEX, VARIABLE_REGEX, - FIELD_VAR_REGEX, + FIELD_VAR_REGEX, } from "../constants"; -import { getDate } from "../utility"; +import { getDate } from "../utilityObsidian"; export abstract class Formatter { protected value: string; @@ -104,7 +104,7 @@ export abstract class Formatter { return output; } - protected abstract getCurrentFileLink(): string | null; + protected abstract getCurrentFileLink(): string | null; protected async replaceVariableInString(input: string) { let output: string = input; @@ -143,8 +143,8 @@ export abstract class Formatter { return output; } - - protected async replaceFieldVarInString(input: string) { + + protected async replaceFieldVarInString(input: string) { let output: string = input; while (FIELD_VAR_REGEX.test(output)) { @@ -155,10 +155,10 @@ export abstract class Formatter { if (variableName) { if (!this.getVariableValue(variableName)) { - this.variables.set( - variableName, - await this.suggestForField(variableName) - ); + this.variables.set( + variableName, + await this.suggestForField(variableName) + ); } output = this.replacer( @@ -172,7 +172,7 @@ export abstract class Formatter { } return output; - } + } protected abstract promptForMathValue(): Promise; @@ -209,7 +209,9 @@ export abstract class Formatter { protected abstract getVariableValue(variableName: string): string; - protected abstract suggestForValue(suggestedValues: string[]): Promise | string; + protected abstract suggestForValue( + suggestedValues: string[] + ): Promise | string; protected abstract suggestForField(variableName: string): any; @@ -231,12 +233,18 @@ export abstract class Formatter { ); const nld = this.getNaturalLanguageDates(); - if (!nld || !nld.parseDate || typeof nld.parseDate !== "function") continue; - - const parseAttempt = - (nld.parseDate as (s: string | undefined) => { moment: { format: (s: string) => string}})( - this.variables.get(variableName) as string - ); + if ( + !nld || + !nld.parseDate || + typeof nld.parseDate !== "function" + ) + continue; + + const parseAttempt = ( + nld.parseDate as (s: string | undefined) => { + moment: { format: (s: string) => string }; + } + )(this.variables.get(variableName) as string); if (parseAttempt) this.variables.set( diff --git a/src/formatters/helpers/insertAfter.test.ts b/src/formatters/helpers/insertAfter.test.ts new file mode 100644 index 0000000..d77e04a --- /dev/null +++ b/src/formatters/helpers/insertAfter.test.ts @@ -0,0 +1,23 @@ +import insertAfter from "./insertAfter"; +import { test, expect } from "vitest"; + +test("inserts value after target", () => { + const target = "# Meeting Notes"; + const value = "## Topic C\n"; + const body = `# Meeting Notes + +## Topic A + +## Topic B`; + + const expected = `# Meeting Notes + +## Topic A + +## Topic B + +${value}`; + const result = insertAfter(target, value, body); + expect(result.success).toBeTruthy(); + if (result.success) expect(result.value).toBe(expected); +}); diff --git a/src/formatters/helpers/insertAfter.ts b/src/formatters/helpers/insertAfter.ts new file mode 100644 index 0000000..da99c44 --- /dev/null +++ b/src/formatters/helpers/insertAfter.ts @@ -0,0 +1,84 @@ +import { escapeRegExp, getLinesInString } from "src/utility"; + +type ErrorType = "NOT FOUND"; +type ReturnType = + | { success: boolean; value: string } + | { success: false; error: ErrorType }; + +/** + * Inserts a string after another string in a body of text + * @param target String to insert after + * @param value What to insert + * @param body The body where the insertion should occur + * @returns A string with the value inserted after the target + */ +export default function insertAfter( + target: string, + value: string, + body: string, + options: { insertAtEndOfSection?: boolean } = {} +): ReturnType { + const targetRegex = new RegExp( + `\\s*${escapeRegExp(target.replace("\\n", ""))}\\s*` + ); + const fileContentLines: string[] = getLinesInString(body); + + let targetPosition = fileContentLines.findIndex((line) => + targetRegex.test(line) + ); + + const targetFound = targetPosition !== -1; + if (!targetFound) { + return { success: false, error: "NOT FOUND" }; + } + + if (options.insertAtEndOfSection) { + const nextHeaderPositionAfterTargetPosition = fileContentLines + .slice(targetPosition + 1) + .findIndex((line) => /^#+ |---/.test(line)); + const foundNextHeader = nextHeaderPositionAfterTargetPosition !== -1; + + let endOfSectionIndex: number | null = null; + if (foundNextHeader) { + for ( + let i = nextHeaderPositionAfterTargetPosition + targetPosition; + i > targetPosition; + i-- + ) { + const lineIsNewline: boolean = /^[\s\n ]*$/.test( + fileContentLines[i] + ); + + if (!lineIsNewline) { + endOfSectionIndex = i; + break; + } + } + + if (!endOfSectionIndex) endOfSectionIndex = targetPosition; + targetPosition = endOfSectionIndex; + } else { + targetPosition = fileContentLines.length - 1; + } + } + + const insertedAfter = insertTextAfterPositionInBody( + value, + body, + targetPosition + ); + + return { success: true, value: insertedAfter }; +} + +function insertTextAfterPositionInBody( + text: string, + body: string, + pos: number +): string { + const splitContent = body.split("\n"); + const pre = splitContent.slice(0, pos + 1).join("\n"); + const post = splitContent.slice(pos + 1).join("\n"); + + return `${pre}\n${text}${post}`; +} diff --git a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts index 150b592..f5a7be7 100644 --- a/src/gui/ChoiceBuilder/templateChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/templateChoiceBuilder.ts @@ -11,7 +11,7 @@ import { NewTabDirection } from "../../types/newTabDirection"; import FolderList from "./FolderList.svelte"; import { FileNameDisplayFormatter } from "../../formatters/fileNameDisplayFormatter"; import { log } from "../../logger/logManager"; -import { getAllFolderPathsInVault } from "../../utility"; +import { getAllFolderPathsInVault } from "../../utilityObsidian"; import type QuickAdd from "../../main"; import type { FileViewMode } from "../../types/fileViewMode"; import { GenericTextSuggester } from "../suggesters/genericTextSuggester"; diff --git a/src/gui/MacroGUIs/MacroBuilder.ts b/src/gui/MacroGUIs/MacroBuilder.ts index 2e443f7..da4ec59 100644 --- a/src/gui/MacroGUIs/MacroBuilder.ts +++ b/src/gui/MacroGUIs/MacroBuilder.ts @@ -17,7 +17,7 @@ import type { SvelteComponent } from "svelte"; import CommandList from "./CommandList.svelte"; import type IChoice from "../../types/choices/IChoice"; import { ChoiceCommand } from "../../types/macros/ChoiceCommand"; -import { getUserScriptMemberAccess } from "../../utility"; +import { getUserScriptMemberAccess } from "../../utilityObsidian"; import GenericInputPrompt from "../GenericInputPrompt/GenericInputPrompt"; import { WaitCommand } from "../../types/macros/QuickCommands/WaitCommand"; import { CaptureChoice } from "../../types/choices/CaptureChoice"; @@ -112,10 +112,8 @@ export class MacroBuilder extends Modal { const addObsidianCommandFromInput = () => { const value: string = input.getValue(); - const obsidianCommand = this.commands.find( - (v) => v.name === value - ); - + const obsidianCommand = this.commands.find((v) => v.name === value); + if (!obsidianCommand) { log.logError( `Could not find Obsidian command with name "${value}"` diff --git a/src/main.ts b/src/main.ts index 02845a7..01139b3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import { ChoiceType } from "./types/choices/choiceType"; import { ChoiceExecutor } from "./choiceExecutor"; import type IChoice from "./types/choices/IChoice"; import type IMultiChoice from "./types/choices/IMultiChoice"; -import { deleteObsidianCommand } from "./utility"; +import { deleteObsidianCommand } from "./utilityObsidian"; import ChoiceSuggester from "./gui/suggesters/choiceSuggester"; import { QuickAddApi } from "./quickAddApi"; import migrate from "./migrations/migrate"; @@ -193,7 +193,7 @@ export default class QuickAdd extends Plugin { private announceUpdate() { const currentVersion = this.manifest.version; const knownVersion = this.settings.version; - + if (currentVersion === knownVersion) return; this.settings.version = currentVersion; diff --git a/src/quickAddApi.ts b/src/quickAddApi.ts index cf32348..7d442b1 100644 --- a/src/quickAddApi.ts +++ b/src/quickAddApi.ts @@ -9,7 +9,7 @@ import type QuickAdd from "./main"; import type IChoice from "./types/choices/IChoice"; import { log } from "./logger/logManager"; import { CompleteFormatter } from "./formatters/completeFormatter"; -import { getDate } from "./utility"; +import { getDate } from "./utilityObsidian"; import { MarkdownView } from "obsidian"; import GenericWideInputPrompt from "./gui/GenericWideInputPrompt/GenericWideInputPrompt"; @@ -47,7 +47,7 @@ export class QuickAddApi { value: string, index?: number, arr?: string[] - ) => string[]), + ) => string[]), actualItems: string[] ) => { return this.suggester(app, displayItems, actualItems); @@ -176,7 +176,11 @@ export class QuickAddApi { } } - public static async infoDialog(app: App, header: string, text: string[] | string) { + public static async infoDialog( + app: App, + header: string, + text: string[] | string + ) { try { return await GenericInfoDialog.Show(app, header, text); } catch { diff --git a/src/utility.ts b/src/utility.ts index 0ac50f3..878da54 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -1,120 +1,3 @@ -import type { App, TAbstractFile } from "obsidian"; -import { MarkdownView, TFile, TFolder, WorkspaceLeaf } from "obsidian"; -import { log } from "./logger/logManager"; -import type { NewTabDirection } from "./types/newTabDirection"; -import type { IUserScript } from "./types/macros/IUserScript"; -import type { FileViewMode } from "./types/fileViewMode"; -import { TemplateChoice } from "./types/choices/TemplateChoice"; -import { MultiChoice } from "./types/choices/MultiChoice"; -import { CaptureChoice } from "./types/choices/CaptureChoice"; -import { MacroChoice } from "./types/choices/MacroChoice"; -import IChoice from "./types/choices/IChoice"; -import { ChoiceType } from "./types/choices/choiceType"; - -export function getTemplater(app: App) { - return app.plugins.plugins["templater-obsidian"]; -} - -export async function replaceTemplaterTemplatesInCreatedFile( - app: App, - file: TFile, - force = false -) { - const templater = getTemplater(app); - - if ( - templater && - (force || !(templater.settings as Record)["trigger_on_file_creation"]) - ) { - const impl = (templater?.templater as { overwrite_file_commands?: (file: TFile) => Promise; }); - if (impl?.overwrite_file_commands) { - await impl.overwrite_file_commands(file); - } - } -} - -export async function templaterParseTemplate( - app: App, - templateContent: string, - targetFile: TFile -) { - const templater = getTemplater(app); - if (!templater) return templateContent; - - return await (templater.templater as { parse_template: (opt: { target_file: TFile, run_mode: number}, content: string) => Promise}).parse_template( - { target_file: targetFile, run_mode: 4 }, - templateContent - ); -} - -export function getNaturalLanguageDates(app: App) { - // @ts-ignore - return app.plugins.plugins["nldates-obsidian"]; -} - -export function getDate(input?: { format?: string; offset?: number }) { - let duration; - - if ( - input?.offset !== null && - input?.offset !== undefined && - typeof input.offset === "number" - ) { - duration = window.moment.duration(input.offset, "days"); - } - - return input?.format - ? window.moment().add(duration).format(input.format) - : window.moment().add(duration).format("YYYY-MM-DD"); -} - -export function appendToCurrentLine(toAppend: string, app: App) { - try { - const activeView = app.workspace.getActiveViewOfType(MarkdownView); - - if (!activeView) { - log.logError(`unable to append '${toAppend}' to current line.`); - return; - } - - activeView.editor.replaceSelection(toAppend); - } catch { - log.logError(`unable to append '${toAppend}' to current line.`); - } -} - -export function findObsidianCommand(app: App, commandId: string) { - // @ts-ignore - return app.commands.findCommand(commandId); -} - -export function deleteObsidianCommand(app: App, commandId: string) { - if (findObsidianCommand(app, commandId)) { - // @ts-ignore - delete app.commands.commands[commandId]; - // @ts-ignore - delete app.commands.editorCommands[commandId]; - } -} - -export function getAllFolderPathsInVault(app: App): string[] { - return app.vault - .getAllLoadedFiles() - .filter((f) => f instanceof TFolder) - .map((folder) => folder.path); -} - -export function getUserScriptMemberAccess(fullMemberPath: string): { - basename: string | undefined; - memberAccess: string[] | undefined; -} { - const fullMemberArray: string[] = fullMemberPath.split("::"); - return { - basename: fullMemberArray[0], - memberAccess: fullMemberArray.slice(1), - }; -} - export function waitFor(ms: number): Promise { return new Promise((res) => setTimeout(res, ms)); } @@ -123,7 +6,7 @@ export function getLinesInString(input: string) { const lines: string[] = []; let tempString = input; - while (tempString.contains("\n")) { + while (tempString.includes("\n")) { const lineEndIndex = tempString.indexOf("\n"); lines.push(tempString.slice(0, lineEndIndex)); tempString = tempString.slice(lineEndIndex + 1); @@ -137,114 +20,4 @@ export function getLinesInString(input: string) { // https://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript export function escapeRegExp(text: string) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); -} - -export async function openFile( - app: App, - file: TFile, - optional: { - openInNewTab?: boolean; - direction?: NewTabDirection; - mode?: FileViewMode; - focus?: boolean; - } -) { - let leaf: WorkspaceLeaf; - - if (optional.openInNewTab && optional.direction) { - leaf = app.workspace.getLeaf("split", optional.direction); - } else { - leaf = app.workspace.getLeaf("tab"); - } - - await leaf.openFile(file); - - if (optional?.focus) { - app.workspace.setActiveLeaf(leaf, { focus: optional.focus }); - } - - if (optional?.mode) { - const leafViewState = leaf.getViewState(); - - leaf.setViewState({ - ...leafViewState, - state: { - ...leafViewState.state, - mode: optional.mode, - }, - }); - } -} - -// Slightly modified version of Templater's user script import implementation -// Source: https://github.com/SilentVoid13/Templater -export async function getUserScript(command: IUserScript, app: App) { - // @ts-ignore - const file: TAbstractFile = app.vault.getAbstractFileByPath(command.path); - if (!file) { - log.logError(`failed to load file ${command.path}.`); - return; - } - - if (file instanceof TFile) { - const req = (s: string) => window.require && window.require(s); - const exp: Record = {}; - const mod = { exports: exp }; - - const fileContent = await app.vault.read(file); - const fn = window.eval( - `(function(require, module, exports) { ${fileContent} \n})` - ); - fn(req, mod, exp); - - // @ts-ignore - const userScript = exp["default"] || mod.exports; - if (!userScript) return; - - let script = userScript; - - const { memberAccess } = getUserScriptMemberAccess(command.name); - if (memberAccess && memberAccess.length > 0) { - let member: string; - while ((member = memberAccess.shift() as string)) { - //@ts-ignore - script = script[member]; - } - } - - return script; - } -} - -export function excludeKeys( - sourceObj: T, - except: K[] -): Omit { - const obj = structuredClone(sourceObj); - - for (const key of except) { - delete obj[key]; - } - - return obj; -} - -export function getChoiceType< - T extends TemplateChoice | MultiChoice | CaptureChoice | MacroChoice ->(choice: IChoice): choice is T { - const isTemplate = (choice: IChoice): choice is TemplateChoice => - choice.type === ChoiceType.Template; - const isMacro = (choice: IChoice): choice is MacroChoice => - choice.type === ChoiceType.Macro; - const isCapture = (choice: IChoice): choice is CaptureChoice => - choice.type === ChoiceType.Capture; - const isMulti = (choice: IChoice): choice is MultiChoice => - choice.type === ChoiceType.Multi; - - return ( - isTemplate(choice) || - isMacro(choice) || - isCapture(choice) || - isMulti(choice) - ); -} +} \ No newline at end of file diff --git a/src/utilityObsidian.ts b/src/utilityObsidian.ts new file mode 100644 index 0000000..22d44cf --- /dev/null +++ b/src/utilityObsidian.ts @@ -0,0 +1,226 @@ +import type { App, TAbstractFile, WorkspaceLeaf } from "obsidian"; +import { MarkdownView, TFile, TFolder } from "obsidian"; +import type { NewTabDirection } from "./types/newTabDirection"; +import type { IUserScript } from "./types/macros/IUserScript"; +import type { FileViewMode } from "./types/fileViewMode"; +import type { TemplateChoice } from "./types/choices/TemplateChoice"; +import type { MultiChoice } from "./types/choices/MultiChoice"; +import type { CaptureChoice } from "./types/choices/CaptureChoice"; +import type { MacroChoice } from "./types/choices/MacroChoice"; +import type IChoice from "./types/choices/IChoice"; +import { ChoiceType } from "./types/choices/choiceType"; +import { log } from "./logger/logManager"; + +export function getTemplater(app: App) { + return app.plugins.plugins["templater-obsidian"]; +} + +export async function replaceTemplaterTemplatesInCreatedFile( + app: App, + file: TFile, + force = false +) { + const templater = getTemplater(app); + + if ( + templater && + (force || !(templater.settings as Record)["trigger_on_file_creation"]) + ) { + const impl = (templater?.templater as { overwrite_file_commands?: (file: TFile) => Promise; }); + if (impl?.overwrite_file_commands) { + await impl.overwrite_file_commands(file); + } + } +} + +export async function templaterParseTemplate( + app: App, + templateContent: string, + targetFile: TFile +) { + const templater = getTemplater(app); + if (!templater) return templateContent; + + return await (templater.templater as { parse_template: (opt: { target_file: TFile, run_mode: number}, content: string) => Promise}).parse_template( + { target_file: targetFile, run_mode: 4 }, + templateContent + ); +} + +export function getNaturalLanguageDates(app: App) { + // @ts-ignore + return app.plugins.plugins["nldates-obsidian"]; +} + +export function getDate(input?: { format?: string; offset?: number }) { + let duration; + + if ( + input?.offset !== null && + input?.offset !== undefined && + typeof input.offset === "number" + ) { + duration = window.moment.duration(input.offset, "days"); + } + + return input?.format + ? window.moment().add(duration).format(input.format) + : window.moment().add(duration).format("YYYY-MM-DD"); +} + +export function appendToCurrentLine(toAppend: string, app: App) { + try { + const activeView = app.workspace.getActiveViewOfType(MarkdownView); + + if (!activeView) { + log.logError(`unable to append '${toAppend}' to current line.`); + return; + } + + activeView.editor.replaceSelection(toAppend); + } catch { + log.logError(`unable to append '${toAppend}' to current line.`); + } +} + +export function findObsidianCommand(app: App, commandId: string) { + // @ts-ignore + return app.commands.findCommand(commandId); +} + +export function deleteObsidianCommand(app: App, commandId: string) { + if (findObsidianCommand(app, commandId)) { + // @ts-ignore + delete app.commands.commands[commandId]; + // @ts-ignore + delete app.commands.editorCommands[commandId]; + } +} + +export function getAllFolderPathsInVault(app: App): string[] { + return app.vault + .getAllLoadedFiles() + .filter((f) => f instanceof TFolder) + .map((folder) => folder.path); +} + +export function getUserScriptMemberAccess(fullMemberPath: string): { + basename: string | undefined; + memberAccess: string[] | undefined; +} { + const fullMemberArray: string[] = fullMemberPath.split("::"); + return { + basename: fullMemberArray[0], + memberAccess: fullMemberArray.slice(1), + }; +} + +export async function openFile( + app: App, + file: TFile, + optional: { + openInNewTab?: boolean; + direction?: NewTabDirection; + mode?: FileViewMode; + focus?: boolean; + } +) { + let leaf: WorkspaceLeaf; + + if (optional.openInNewTab && optional.direction) { + leaf = app.workspace.getLeaf("split", optional.direction); + } else { + leaf = app.workspace.getLeaf("tab"); + } + + await leaf.openFile(file); + + if (optional?.focus) { + app.workspace.setActiveLeaf(leaf, { focus: optional.focus }); + } + + if (optional?.mode) { + const leafViewState = leaf.getViewState(); + + leaf.setViewState({ + ...leafViewState, + state: { + ...leafViewState.state, + mode: optional.mode, + }, + }); + } +} + +// Slightly modified version of Templater's user script import implementation +// Source: https://github.com/SilentVoid13/Templater +export async function getUserScript(command: IUserScript, app: App) { + // @ts-ignore + const file: TAbstractFile = app.vault.getAbstractFileByPath(command.path); + if (!file) { + log.logError(`failed to load file ${command.path}.`); + return; + } + + if (file instanceof TFile) { + const req = (s: string) => window.require && window.require(s); + const exp: Record = {}; + const mod = { exports: exp }; + + const fileContent = await app.vault.read(file); + const fn = window.eval( + `(function(require, module, exports) { ${fileContent} \n})` + ); + fn(req, mod, exp); + + // @ts-ignore + const userScript = exp["default"] || mod.exports; + if (!userScript) return; + + let script = userScript; + + const { memberAccess } = getUserScriptMemberAccess(command.name); + if (memberAccess && memberAccess.length > 0) { + let member: string; + while ((member = memberAccess.shift() as string)) { + //@ts-ignore + script = script[member]; + } + } + + return script; + } +} + +export function excludeKeys( + sourceObj: T, + except: K[] +): Omit { + const obj = structuredClone(sourceObj); + + for (const key of except) { + delete obj[key]; + } + + return obj; +} + +export function getChoiceType< + T extends TemplateChoice | MultiChoice | CaptureChoice | MacroChoice +>(choice: IChoice): choice is T { + const isTemplate = (choice: IChoice): choice is TemplateChoice => + choice.type === ChoiceType.Template; + const isMacro = (choice: IChoice): choice is MacroChoice => + choice.type === ChoiceType.Macro; + const isCapture = (choice: IChoice): choice is CaptureChoice => + choice.type === ChoiceType.Capture; + const isMulti = (choice: IChoice): choice is MultiChoice => + choice.type === ChoiceType.Multi; + + return ( + isTemplate(choice) || + isMacro(choice) || + isCapture(choice) || + isMulti(choice) + ); +} From 664686063613fa61e2f5318016257fd8454d9e33 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 25 Mar 2023 16:08:50 +0100 Subject: [PATCH 02/11] fix imports --- src/engine/MacroChoiceEngine.ts | 3 ++- src/gui/MacroGUIs/CommandList.svelte | 2 +- src/gui/choiceList/ChoiceView.svelte | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/engine/MacroChoiceEngine.ts b/src/engine/MacroChoiceEngine.ts index b790416..35f589b 100644 --- a/src/engine/MacroChoiceEngine.ts +++ b/src/engine/MacroChoiceEngine.ts @@ -13,7 +13,7 @@ import GenericSuggester from "../gui/GenericSuggester/genericSuggester"; import type { IChoiceCommand } from "../types/macros/IChoiceCommand"; import type QuickAdd from "../main"; import type { IChoiceExecutor } from "../IChoiceExecutor"; -import { getUserScript, waitFor } from "../utilityObsidian"; +import { getUserScript } from "../utilityObsidian"; import type { IWaitCommand } from "../types/macros/QuickCommands/IWaitCommand"; import type { INestedChoiceCommand } from "../types/macros/QuickCommands/INestedChoiceCommand"; import type IChoice from "../types/choices/IChoice"; @@ -24,6 +24,7 @@ import { CopyCommand } from "../types/macros/EditorCommands/CopyCommand"; import { PasteCommand } from "../types/macros/EditorCommands/PasteCommand"; import { SelectActiveLineCommand } from "../types/macros/EditorCommands/SelectActiveLineCommand"; import { SelectLinkOnActiveLineCommand } from "../types/macros/EditorCommands/SelectLinkOnActiveLineCommand"; +import { waitFor } from "src/utility"; export class MacroChoiceEngine extends QuickAddChoiceEngine { public choice: IMacroChoice; diff --git a/src/gui/MacroGUIs/CommandList.svelte b/src/gui/MacroGUIs/CommandList.svelte index 56b82b5..7d7c70a 100644 --- a/src/gui/MacroGUIs/CommandList.svelte +++ b/src/gui/MacroGUIs/CommandList.svelte @@ -16,8 +16,8 @@ import UserScriptCommand from "./Components/UserScriptCommand.svelte"; import type {IUserScript} from "../../types/macros/IUserScript"; import {UserScriptSettingsModal} from "./UserScriptSettingsModal"; - import {getUserScript} from "../../utility"; import {log} from "../../logger/logManager"; + import { getUserScript } from "src/utilityObsidian"; export let commands: ICommand[]; export let deleteCommand: (commandId: string) => Promise; diff --git a/src/gui/choiceList/ChoiceView.svelte b/src/gui/choiceList/ChoiceView.svelte index b5ef8af..1a83c31 100644 --- a/src/gui/choiceList/ChoiceView.svelte +++ b/src/gui/choiceList/ChoiceView.svelte @@ -20,9 +20,9 @@ import type {IMacro} from "../../types/macros/IMacro"; import QuickAdd from "../../main"; import GenericInputPrompt from "../GenericInputPrompt/GenericInputPrompt"; - import { excludeKeys, getChoiceType } from "src/utility"; import { settingsStore } from "src/settingsStore"; import { onMount } from "svelte"; + import { excludeKeys, getChoiceType } from "src/utilityObsidian"; export let choices: IChoice[] = []; export let macros: IMacro[] = []; From eb829c96ff98b4aa5220efd6a5b554f6867f8fde Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 25 Mar 2023 16:41:10 +0100 Subject: [PATCH 03/11] fix types in utility functions --- src/global.d.ts | 11 ++++++++++- src/utilityObsidian.ts | 10 ++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/global.d.ts b/src/global.d.ts index 2417527..64caae1 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,4 +1,4 @@ -import { Plugin } from "obsidian"; +import type { Plugin } from "obsidian"; declare module "obsidian" { interface App { @@ -20,5 +20,14 @@ declare module "obsidian" { enablePlugin: (id: string) => Promise; disablePlugin: (id: string) => Promise; }; + commands: { + commands: { + [commandName: string]: (...args: unknown[]) => Promise; + }, + editorCommands: { + [commandName: string]: (...args: unknown[]) => Promise; + }, + findCommand: (commandId: string) => Command; + } } } diff --git a/src/utilityObsidian.ts b/src/utilityObsidian.ts index 22d44cf..78d136c 100644 --- a/src/utilityObsidian.ts +++ b/src/utilityObsidian.ts @@ -84,15 +84,12 @@ export function appendToCurrentLine(toAppend: string, app: App) { } export function findObsidianCommand(app: App, commandId: string) { - // @ts-ignore return app.commands.findCommand(commandId); } export function deleteObsidianCommand(app: App, commandId: string) { if (findObsidianCommand(app, commandId)) { - // @ts-ignore delete app.commands.commands[commandId]; - // @ts-ignore delete app.commands.editorCommands[commandId]; } } @@ -142,8 +139,9 @@ export async function openFile( if (optional?.mode) { const leafViewState = leaf.getViewState(); - leaf.setViewState({ + await leaf.setViewState({ ...leafViewState, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment state: { ...leafViewState.state, mode: optional.mode, @@ -163,14 +161,17 @@ export async function getUserScript(command: IUserScript, app: App) { } if (file instanceof TFile) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return const req = (s: string) => window.require && window.require(s); const exp: Record = {}; const mod = { exports: exp }; const fileContent = await app.vault.read(file); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const fn = window.eval( `(function(require, module, exports) { ${fileContent} \n})` ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call fn(req, mod, exp); // @ts-ignore @@ -184,6 +185,7 @@ export async function getUserScript(command: IUserScript, app: App) { let member: string; while ((member = memberAccess.shift() as string)) { //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment script = script[member]; } } From 09e417ae2f70b1788cb2788287a6c61cf776ad6d Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 25 Mar 2023 17:15:40 +0100 Subject: [PATCH 04/11] refactor: don't use adapter when creating folder --- src/engine/QuickAddEngine.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/engine/QuickAddEngine.ts b/src/engine/QuickAddEngine.ts index f56185b..5c27040 100644 --- a/src/engine/QuickAddEngine.ts +++ b/src/engine/QuickAddEngine.ts @@ -63,11 +63,13 @@ export abstract class QuickAddEngine { let dirName = ""; if (dirMatch) dirName = dirMatch[1]; - if (await this.app.vault.adapter.exists(dirName)) { - return await this.app.vault.create(filePath, fileContent); - } else { + const dir = app.vault.getAbstractFileByPath(dirName); + + if (!dir || !(dir instanceof TFolder)) { await this.createFolder(dirName); - return await this.app.vault.create(filePath, fileContent); + } + + return await this.app.vault.create(filePath, fileContent); } } From 1a178696ff0b28ef6ddf235c72e58db56ebb047c Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 25 Mar 2023 17:15:51 +0100 Subject: [PATCH 05/11] add test to insertAfter --- src/formatters/helpers/insertAfter.test.ts | 23 ++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/formatters/helpers/insertAfter.test.ts b/src/formatters/helpers/insertAfter.test.ts index d77e04a..8270bb0 100644 --- a/src/formatters/helpers/insertAfter.test.ts +++ b/src/formatters/helpers/insertAfter.test.ts @@ -1,7 +1,22 @@ import insertAfter from "./insertAfter"; import { test, expect } from "vitest"; -test("inserts value after target", () => { +test("inserts value after target, at the beginning of the section", () => { + const target = "# Meeting Notes"; + const value = "## Topic C\n"; + const body = `# Meeting Notes +${value} + +## Topic A + +## Topic B`; + const result = insertAfter(target, value, body, { + insertAtEndOfSection: false, + }); + expect(result.success).toBeTruthy(); +}); + +test("inserts value after target, at the end of the section", () => { const target = "# Meeting Notes"; const value = "## Topic C\n"; const body = `# Meeting Notes @@ -10,7 +25,7 @@ test("inserts value after target", () => { ## Topic B`; - const expected = `# Meeting Notes + const expected = `# Meeting Notes ## Topic A @@ -18,6 +33,6 @@ test("inserts value after target", () => { ${value}`; const result = insertAfter(target, value, body); - expect(result.success).toBeTruthy(); - if (result.success) expect(result.value).toBe(expected); + expect(result.success).toBeTruthy(); + if (result.success) expect(result.value).toBe(expected); }); From c62e1b4a8b1b38acd9e684958cad1e64d0634bb8 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 25 Mar 2023 17:46:59 +0100 Subject: [PATCH 06/11] remove `insertAfter` helpers --- src/formatters/helpers/insertAfter.test.ts | 38 ---------- src/formatters/helpers/insertAfter.ts | 84 ---------------------- 2 files changed, 122 deletions(-) delete mode 100644 src/formatters/helpers/insertAfter.test.ts delete mode 100644 src/formatters/helpers/insertAfter.ts diff --git a/src/formatters/helpers/insertAfter.test.ts b/src/formatters/helpers/insertAfter.test.ts deleted file mode 100644 index 8270bb0..0000000 --- a/src/formatters/helpers/insertAfter.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import insertAfter from "./insertAfter"; -import { test, expect } from "vitest"; - -test("inserts value after target, at the beginning of the section", () => { - const target = "# Meeting Notes"; - const value = "## Topic C\n"; - const body = `# Meeting Notes -${value} - -## Topic A - -## Topic B`; - const result = insertAfter(target, value, body, { - insertAtEndOfSection: false, - }); - expect(result.success).toBeTruthy(); -}); - -test("inserts value after target, at the end of the section", () => { - const target = "# Meeting Notes"; - const value = "## Topic C\n"; - const body = `# Meeting Notes - -## Topic A - -## Topic B`; - - const expected = `# Meeting Notes - -## Topic A - -## Topic B - -${value}`; - const result = insertAfter(target, value, body); - expect(result.success).toBeTruthy(); - if (result.success) expect(result.value).toBe(expected); -}); diff --git a/src/formatters/helpers/insertAfter.ts b/src/formatters/helpers/insertAfter.ts deleted file mode 100644 index da99c44..0000000 --- a/src/formatters/helpers/insertAfter.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { escapeRegExp, getLinesInString } from "src/utility"; - -type ErrorType = "NOT FOUND"; -type ReturnType = - | { success: boolean; value: string } - | { success: false; error: ErrorType }; - -/** - * Inserts a string after another string in a body of text - * @param target String to insert after - * @param value What to insert - * @param body The body where the insertion should occur - * @returns A string with the value inserted after the target - */ -export default function insertAfter( - target: string, - value: string, - body: string, - options: { insertAtEndOfSection?: boolean } = {} -): ReturnType { - const targetRegex = new RegExp( - `\\s*${escapeRegExp(target.replace("\\n", ""))}\\s*` - ); - const fileContentLines: string[] = getLinesInString(body); - - let targetPosition = fileContentLines.findIndex((line) => - targetRegex.test(line) - ); - - const targetFound = targetPosition !== -1; - if (!targetFound) { - return { success: false, error: "NOT FOUND" }; - } - - if (options.insertAtEndOfSection) { - const nextHeaderPositionAfterTargetPosition = fileContentLines - .slice(targetPosition + 1) - .findIndex((line) => /^#+ |---/.test(line)); - const foundNextHeader = nextHeaderPositionAfterTargetPosition !== -1; - - let endOfSectionIndex: number | null = null; - if (foundNextHeader) { - for ( - let i = nextHeaderPositionAfterTargetPosition + targetPosition; - i > targetPosition; - i-- - ) { - const lineIsNewline: boolean = /^[\s\n ]*$/.test( - fileContentLines[i] - ); - - if (!lineIsNewline) { - endOfSectionIndex = i; - break; - } - } - - if (!endOfSectionIndex) endOfSectionIndex = targetPosition; - targetPosition = endOfSectionIndex; - } else { - targetPosition = fileContentLines.length - 1; - } - } - - const insertedAfter = insertTextAfterPositionInBody( - value, - body, - targetPosition - ); - - return { success: true, value: insertedAfter }; -} - -function insertTextAfterPositionInBody( - text: string, - body: string, - pos: number -): string { - const splitContent = body.split("\n"); - const pre = splitContent.slice(0, pos + 1).join("\n"); - const post = splitContent.slice(pos + 1).join("\n"); - - return `${pre}\n${text}${post}`; -} From b454ce05fd416f3245669beba661b89eae856bc7 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 27 Mar 2023 13:19:09 +0200 Subject: [PATCH 07/11] end of section captures now handles subsections (nested headers) - capture to last subheading E.g. capturing to # Notes with three subsections (nested headers) ## A, ## B, ## C will now capture below ## C. re #126 #134 --- src/formatters/captureChoiceFormatter.ts | 68 ++---- .../helpers/getEndOfSection.test.ts | 198 ++++++++++++++++++ src/formatters/helpers/getEndOfSection.ts | 86 ++++++++ 3 files changed, 302 insertions(+), 50 deletions(-) create mode 100644 src/formatters/helpers/getEndOfSection.test.ts create mode 100644 src/formatters/helpers/getEndOfSection.ts diff --git a/src/formatters/captureChoiceFormatter.ts b/src/formatters/captureChoiceFormatter.ts index face69e..5e3c09c 100644 --- a/src/formatters/captureChoiceFormatter.ts +++ b/src/formatters/captureChoiceFormatter.ts @@ -4,15 +4,13 @@ import type { App, TFile } from "obsidian"; import { log } from "../logger/logManager"; import type QuickAdd from "../main"; import type { IChoiceExecutor } from "../IChoiceExecutor"; -import { - templaterParseTemplate, -} from "../utilityObsidian"; +import { templaterParseTemplate } from "../utilityObsidian"; import { CREATE_IF_NOT_FOUND_BOTTOM, CREATE_IF_NOT_FOUND_TOP, } from "../constants"; -import insertAfter from "./helpers/insertAfter"; import { escapeRegExp, getLinesInString } from "src/utility"; +import getEndOfSection from "./helpers/getEndOfSection"; export class CaptureChoiceFormatter extends CompleteFormatter { private choice: ICaptureChoice; @@ -100,19 +98,12 @@ export class CaptureChoiceFormatter extends CompleteFormatter { this.choice.insertAfter.after ); - const target = targetString; - const value = formatted; - const body = this.fileContent; - insertAfter(target, value, body, { - insertAtEndOfSection: this.choice.insertAfter.insertAtEnd, - }); - const targetRegex = new RegExp( `\\s*${escapeRegExp(targetString.replace("\\n", ""))}\\s*` ); const fileContentLines: string[] = getLinesInString(this.fileContent); - const targetPosition = fileContentLines.findIndex((line) => + let targetPosition = fileContentLines.findIndex((line) => targetRegex.test(line) ); const targetNotFound = targetPosition === -1; @@ -125,44 +116,21 @@ export class CaptureChoiceFormatter extends CompleteFormatter { } if (this.choice.insertAfter?.insertAtEnd) { - const nextHeaderPositionAfterTargetPosition = fileContentLines - .slice(targetPosition + 1) - .findIndex((line) => /^#+ |---/.test(line)); - const foundNextHeader = - nextHeaderPositionAfterTargetPosition !== -1; - - let endOfSectionIndex: number | null = null; - if (foundNextHeader) { - for ( - let i = - nextHeaderPositionAfterTargetPosition + targetPosition; - i > targetPosition; - i-- - ) { - const lineIsNewline: boolean = /^[\s\n ]*$/.test( - fileContentLines[i] - ); - - if (!lineIsNewline) { - endOfSectionIndex = i; - break; - } - } - - if (!endOfSectionIndex) endOfSectionIndex = targetPosition; - - return this.insertTextAfterPositionInBody( - formatted, - this.fileContent, - endOfSectionIndex - ); - } else { - return this.insertTextAfterPositionInBody( - formatted, - this.fileContent, - fileContentLines.length - 1 - ); - } + if (!this.file) + throw new Error("Tried to get sections without file."); + const headings = ( + app.metadataCache.getFileCache(this.file)?.headings ?? [] + ).map((heading) => ({ + level: heading.level, + line: heading.position.end.line, + })); + + const endOfSectionIndex = getEndOfSection( + headings, + fileContentLines, + targetPosition + ); + targetPosition = endOfSectionIndex ?? fileContentLines.length - 1; } return this.insertTextAfterPositionInBody( diff --git a/src/formatters/helpers/getEndOfSection.test.ts b/src/formatters/helpers/getEndOfSection.test.ts new file mode 100644 index 0000000..59bdc1a --- /dev/null +++ b/src/formatters/helpers/getEndOfSection.test.ts @@ -0,0 +1,198 @@ +import { test, expect } from "vitest"; +import getEndOfSection from "./getEndOfSection"; + +test("getEndOfSection - find the end of a section", () => { + const headings = [ + { level: 1, line: 1 }, + { level: 2, line: 3 }, + { level: 2, line: 6 }, + { level: 1, line: 9 }, + ]; + const lines = [ + "# Title", + "", + "## Section 1", + "Content 1", + "", + "## Section 2", + "Content 2", + "", + "# Title 2", + ]; + const targetLine = 3; + + const result = getEndOfSection(headings, lines, targetLine); + expect(result).toBe(4); +}); + +test("getEndOfSection - find the end of the last section", () => { + const headings = [ + { level: 1, line: 1 }, + { level: 2, line: 3 }, + { level: 2, line: 6 }, + { level: 1, line: 9 }, + ]; + const lines = [ + "# Title", + "", + "## Section 1", + "Content 1", + "", + "## Section 2", + "Content 2", + "", + "# Title 2", + ]; + const targetLine = 9; + + const result = getEndOfSection(headings, lines, targetLine); + expect(result).toBe(8); +}); + +test("getEndOfSection - no target section", () => { + const headings = [ + { level: 1, line: 1 }, + { level: 2, line: 3 }, + { level: 2, line: 6 }, + { level: 1, line: 9 }, + ]; + const lines = [ + "# Title", + "", + "## Section 1", + "Content 1", + "", + "## Section 2", + "Content 2", + "", + "# Title 2", + ]; + const targetLine = 100; + + const result = getEndOfSection(headings, lines, targetLine); + expect(result).toBe(8); +}); + +test("getEndOfSection - find end of section with multiple empty lines", () => { + const headings = [ + { level: 1, line: 1 }, + { level: 2, line: 3 }, + { level: 2, line: 6 }, + { level: 1, line: 9 }, + ]; + const lines = [ + "# Title", + "", + "## Section 1", + "Content 1", + "", + "", + "## Section 2", + "Content 2", + "", + "# Title 2", + ]; + const targetLine = 3; + + const result = getEndOfSection(headings, lines, targetLine); + expect(result).toBe(4); +}); + +test("getEndOfSection - find end of section without a higher level section", () => { + const headings = [ + { level: 1, line: 1 }, + { level: 2, line: 3 }, + { level: 2, line: 6 }, + { level: 2, line: 9 }, + ]; + const lines = [ + "# Title", + "", + "## Section 1", + "Content 1", + "", + "## Section 2", + "Content 2", + "", + "## Section 3", + "Content 3", + ]; + const targetLine = 3; + + const result = getEndOfSection(headings, lines, targetLine); + expect(result).toBe(4); +}); + +test("getEndOfSection - find end of section with higher level section", () => { + const headings = [ + { level: 1, line: 1 }, + { level: 2, line: 3 }, + { level: 2, line: 6 }, + { level: 1, line: 9 }, + ]; + const lines = [ + "# Title", + "", + "## Section 1", + "Content 1", + "", + "## Section 2", + "Content 2", + "", + "# Title 2", + "Content 3", + ]; + const targetLine = 3; + + const result = getEndOfSection(headings, lines, targetLine); + expect(result).toBe(4); +}); + +test("getEndOfSection - find end of section with no headings", () => { + const lines = ["Content 1", "", "Content 2", "Content 3", "", "Content 4"]; + const targetLine = 1; + + const result = getEndOfSection([], lines, targetLine); + expect(result).toBe(5); +}); + +test("getEndOfSection - find end of section with top level heading and only sub headings", () => { + const lines = [ + "# Notes", + "", + "## Topic A", + "content a1", + "content a2", + "content a3", + "", + "---", + "Thematic break", + "1", + "2", + "3", + "", + "## Topic B", + "content b1", + "", + "", + ]; + const headings = [ + { + level: 1, + line: 0, + }, + { + level: 2, + line: 2, + }, + { + level: 2, + line: 13, + }, + ]; + + const targetLine = 0; + + const result = getEndOfSection(headings, lines, targetLine); + expect(result).toBe(15); +}); diff --git a/src/formatters/helpers/getEndOfSection.ts b/src/formatters/helpers/getEndOfSection.ts new file mode 100644 index 0000000..27bbaf5 --- /dev/null +++ b/src/formatters/helpers/getEndOfSection.ts @@ -0,0 +1,86 @@ +type Heading = { + level: number; + line: number; +}; + +export default function getEndOfSection( + headings: Heading[], + lines: string[], + targetLine: number +): number { + const targetSectionIndex = headings.findIndex( + (heading) => heading.line === targetLine + ); + + const lastLineIdx = lines.length - 1; + + if ( + targetSectionIndex === -1 || + targetSectionIndex === headings.length - 1 + ) { + // If there's no target section or it's the last section, return the last line in the body. + return lastLineIdx; + } + + let endOfSectionLine = targetLine; + + const targetSectionLevel = headings[targetSectionIndex].level; + + const [nextHigherLevelIndex, foundHigherLevelSection] = findNextHigherOrSameLevelHeading( + targetSectionIndex, + targetSectionLevel, + headings + ); + + const higherLevelSectionIsLastSection = nextHigherLevelIndex === headings.length; + if (foundHigherLevelSection && higherLevelSectionIsLastSection) { + // If the target section is the last section of its level or there are no higher level sections, return the end line of the file. + endOfSectionLine = lastLineIdx; + } else if (foundHigherLevelSection) { + // If the target section is the last section of its level, and there are higher level sections, + const nextHigherLevelSection = headings[nextHigherLevelIndex].line; + endOfSectionLine = nextHigherLevelSection - 1; + } else { + // End line of the section before the next section at same level as target. + // There are no higher level sections, but there are more sections. + endOfSectionLine = lastLineIdx; + } + + const nonEmptyLineIdx = findNonEmptyStringIndexPriorToGivenIndex( + lines, + endOfSectionLine + ); + + if (nonEmptyLineIdx !== null) { + endOfSectionLine = nonEmptyLineIdx + 1; + } + + return endOfSectionLine; +} + +function findNextHigherOrSameLevelHeading( + targetSectionIndex: number, + targetSectionLevel: number, + headings: Heading[] +): readonly [number, boolean] { + for (let i = targetSectionIndex + 1; i < headings.length; i++) { + if (headings[i].level <= targetSectionLevel) { + return [i, true]; + } + } + + return [-1, false]; +} + +function findNonEmptyStringIndexPriorToGivenIndex( + strings: string[], + givenIndex: number +): number | null { + for (let i = givenIndex - 1; i >= 0; i--) { + if (strings[i].trim() !== "") { + return i; + } + } + + return null; // If no non-empty string is found before the given index +} From fd1d96192a08f899dfb3bc7fe130fafbe3861d0d Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 27 Mar 2023 14:43:02 +0200 Subject: [PATCH 08/11] toggle for handling subsections --- src/formatters/captureChoiceFormatter.ts | 7 - .../helpers/getEndOfSection.test.ts | 140 ++++++--------- src/formatters/helpers/getEndOfSection.ts | 170 +++++++++++++----- 3 files changed, 172 insertions(+), 145 deletions(-) diff --git a/src/formatters/captureChoiceFormatter.ts b/src/formatters/captureChoiceFormatter.ts index 5e3c09c..b109ed1 100644 --- a/src/formatters/captureChoiceFormatter.ts +++ b/src/formatters/captureChoiceFormatter.ts @@ -118,15 +118,8 @@ export class CaptureChoiceFormatter extends CompleteFormatter { if (this.choice.insertAfter?.insertAtEnd) { if (!this.file) throw new Error("Tried to get sections without file."); - const headings = ( - app.metadataCache.getFileCache(this.file)?.headings ?? [] - ).map((heading) => ({ - level: heading.level, - line: heading.position.end.line, - })); const endOfSectionIndex = getEndOfSection( - headings, fileContentLines, targetPosition ); diff --git a/src/formatters/helpers/getEndOfSection.test.ts b/src/formatters/helpers/getEndOfSection.test.ts index 59bdc1a..6d7f943 100644 --- a/src/formatters/helpers/getEndOfSection.test.ts +++ b/src/formatters/helpers/getEndOfSection.test.ts @@ -2,60 +2,24 @@ import { test, expect } from "vitest"; import getEndOfSection from "./getEndOfSection"; test("getEndOfSection - find the end of a section", () => { - const headings = [ - { level: 1, line: 1 }, - { level: 2, line: 3 }, - { level: 2, line: 6 }, - { level: 1, line: 9 }, - ]; const lines = [ "# Title", "", - "## Section 1", + "## Section 1", // target (2) "Content 1", - "", + "", // result (4) "## Section 2", "Content 2", "", "# Title 2", ]; - const targetLine = 3; + const targetLine = 2; - const result = getEndOfSection(headings, lines, targetLine); + const result = getEndOfSection(lines, targetLine); expect(result).toBe(4); }); test("getEndOfSection - find the end of the last section", () => { - const headings = [ - { level: 1, line: 1 }, - { level: 2, line: 3 }, - { level: 2, line: 6 }, - { level: 1, line: 9 }, - ]; - const lines = [ - "# Title", - "", - "## Section 1", - "Content 1", - "", - "## Section 2", - "Content 2", - "", - "# Title 2", - ]; - const targetLine = 9; - - const result = getEndOfSection(headings, lines, targetLine); - expect(result).toBe(8); -}); - -test("getEndOfSection - no target section", () => { - const headings = [ - { level: 1, line: 1 }, - { level: 2, line: 3 }, - { level: 2, line: 6 }, - { level: 1, line: 9 }, - ]; const lines = [ "# Title", "", @@ -65,21 +29,16 @@ test("getEndOfSection - no target section", () => { "## Section 2", "Content 2", "", - "# Title 2", + "# Title 2", // target (8) + "", // result (9) ]; - const targetLine = 100; + const targetLine = 8; - const result = getEndOfSection(headings, lines, targetLine); - expect(result).toBe(8); + const result = getEndOfSection(lines, targetLine); + expect(result).toBe(9); }); test("getEndOfSection - find end of section with multiple empty lines", () => { - const headings = [ - { level: 1, line: 1 }, - { level: 2, line: 3 }, - { level: 2, line: 6 }, - { level: 1, line: 9 }, - ]; const lines = [ "# Title", "", @@ -92,19 +51,13 @@ test("getEndOfSection - find end of section with multiple empty lines", () => { "", "# Title 2", ]; - const targetLine = 3; + const targetLine = 2; - const result = getEndOfSection(headings, lines, targetLine); + const result = getEndOfSection(lines, targetLine); expect(result).toBe(4); }); test("getEndOfSection - find end of section without a higher level section", () => { - const headings = [ - { level: 1, line: 1 }, - { level: 2, line: 3 }, - { level: 2, line: 6 }, - { level: 2, line: 9 }, - ]; const lines = [ "# Title", "", @@ -117,19 +70,13 @@ test("getEndOfSection - find end of section without a higher level section", () "## Section 3", "Content 3", ]; - const targetLine = 3; + const targetLine = 2; - const result = getEndOfSection(headings, lines, targetLine); + const result = getEndOfSection(lines, targetLine); expect(result).toBe(4); }); test("getEndOfSection - find end of section with higher level section", () => { - const headings = [ - { level: 1, line: 1 }, - { level: 2, line: 3 }, - { level: 2, line: 6 }, - { level: 1, line: 9 }, - ]; const lines = [ "# Title", "", @@ -142,23 +89,23 @@ test("getEndOfSection - find end of section with higher level section", () => { "# Title 2", "Content 3", ]; - const targetLine = 3; + const targetLine = 0; - const result = getEndOfSection(headings, lines, targetLine); - expect(result).toBe(4); + const result = getEndOfSection(lines, targetLine); + expect(result).toBe(7); }); test("getEndOfSection - find end of section with no headings", () => { const lines = ["Content 1", "", "Content 2", "Content 3", "", "Content 4"]; - const targetLine = 1; + const targetLine = 2; - const result = getEndOfSection([], lines, targetLine); - expect(result).toBe(5); + const result = getEndOfSection(lines, targetLine); + expect(result).toBe(4); }); test("getEndOfSection - find end of section with top level heading and only sub headings", () => { const lines = [ - "# Notes", + "# Notes", // target (0) "", "## Topic A", "content a1", @@ -173,26 +120,39 @@ test("getEndOfSection - find end of section with top level heading and only sub "", "## Topic B", "content b1", - "", + "", // result (15) "", ]; - const headings = [ - { - level: 1, - line: 0, - }, - { - level: 2, - line: 2, - }, - { - level: 2, - line: 13, - }, + + const targetLine = 0; + + const result = getEndOfSection(lines, targetLine); + expect(result).toBe(15); +}); + +test("getEndOfSection - target isn't heading", () => { + const lines = [ + "# Notes", + "", + "## Topic A", + "content a1", // target (3) + "content a2", + "content a3", + "", // result (6) + "---", + "Thematic break", + "1", + "2", + "3", + "", + "## Topic B", + "content b1", + "", + "", ]; - const targetLine = 0; + const targetLine = 3; - const result = getEndOfSection(headings, lines, targetLine); - expect(result).toBe(15); + const result = getEndOfSection(lines, targetLine); + expect(result).toBe(6); }); diff --git a/src/formatters/helpers/getEndOfSection.ts b/src/formatters/helpers/getEndOfSection.ts index 27bbaf5..0c22d6f 100644 --- a/src/formatters/helpers/getEndOfSection.ts +++ b/src/formatters/helpers/getEndOfSection.ts @@ -3,84 +3,158 @@ type Heading = { line: number; }; +function getMarkdownHeadings(bodyLines: string[]): Heading[] { + const headers: Heading[] = []; + + bodyLines.forEach((line, index) => { + const match = line.match(/^(#+)/); + + if (!match) return; + + headers.push({ + level: match[0].length, + line: index, + }); + }); + + return headers; +} + +/** + * + * @param lines Lines in body to find end of section + * @param targetLine Target line to find end of section + * @param shouldConsiderSubsections Whether to consider subsections as part of the section + * @returns index of end of section + */ export default function getEndOfSection( - headings: Heading[], lines: string[], - targetLine: number + targetLine: number, + shouldConsiderSubsections = false ): number { - const targetSectionIndex = headings.findIndex( + const headings = getMarkdownHeadings(lines); + + const targetHeading = headings.find( (heading) => heading.line === targetLine ); + const targetIsNotHeading = !targetHeading; - const lastLineIdx = lines.length - 1; + if (targetIsNotHeading && shouldConsiderSubsections) { + throw new Error( + `Target line ${targetLine} is not a heading, but we are trying to find the end of its section.` + ); + } - if ( - targetSectionIndex === -1 || - targetSectionIndex === headings.length - 1 - ) { - // If there's no target section or it's the last section, return the last line in the body. - return lastLineIdx; - } + if (targetIsNotHeading) { + const nextEmptyStringIdx = findNextIdx( + lines, + targetLine, + (str: string) => str.trim() === "" + ); - let endOfSectionLine = targetLine; + if (nextEmptyStringIdx !== null) { + return nextEmptyStringIdx; + } - const targetSectionLevel = headings[targetSectionIndex].level; + return targetLine; + } - const [nextHigherLevelIndex, foundHigherLevelSection] = findNextHigherOrSameLevelHeading( - targetSectionIndex, - targetSectionLevel, - headings + const lastLineInBodyIdx = lines.length - 1; + const endOfSectionLineIdx = getEndOfSectionLineByHeadings( + targetHeading, + headings, + lastLineInBodyIdx ); - const higherLevelSectionIsLastSection = nextHigherLevelIndex === headings.length; - if (foundHigherLevelSection && higherLevelSectionIsLastSection) { - // If the target section is the last section of its level or there are no higher level sections, return the end line of the file. - endOfSectionLine = lastLineIdx; - } else if (foundHigherLevelSection) { - // If the target section is the last section of its level, and there are higher level sections, - const nextHigherLevelSection = headings[nextHigherLevelIndex].line; - endOfSectionLine = nextHigherLevelSection - 1; - } else { - // End line of the section before the next section at same level as target. - // There are no higher level sections, but there are more sections. - endOfSectionLine = lastLineIdx; - } - - const nonEmptyLineIdx = findNonEmptyStringIndexPriorToGivenIndex( + const lastNonEmptyLineInSectionIdx = findPriorIdx( lines, - endOfSectionLine + endOfSectionLineIdx, + (str: string) => str.trim() !== "" ); - if (nonEmptyLineIdx !== null) { - endOfSectionLine = nonEmptyLineIdx + 1; + if (lastNonEmptyLineInSectionIdx !== null) { + return lastNonEmptyLineInSectionIdx + 1; } - return endOfSectionLine; + return endOfSectionLineIdx; +} + +function getEndOfSectionLineByHeadings( + targetHeading: Heading, + headings: Heading[], + lastLineInBodyIdx: number +): number { + const targetHeadingIsLastHeading = targetHeading.line === headings.length - 1; + + if (targetHeadingIsLastHeading) { + return lastLineInBodyIdx; + } + + const [nextHigherLevelHeadingIndex, foundHigherLevelHeading] = + findNextHigherOrSameLevelHeading( + targetHeading, + headings + ); + + const higherLevelSectionIsLastHeading = + foundHigherLevelHeading && + nextHigherLevelHeadingIndex === headings.length; + + if (foundHigherLevelHeading && !higherLevelSectionIsLastHeading) { + // If the target section is the last section of its level, and there are higher level sections, + const nextHigherLevelHeadingLineIdx = + headings[nextHigherLevelHeadingIndex].line; + return nextHigherLevelHeadingLineIdx - 1; + } + + // End line of the section before the next section at same level as target. + // There are no higher level sections, but there are more sections. + return lastLineInBodyIdx; } function findNextHigherOrSameLevelHeading( - targetSectionIndex: number, - targetSectionLevel: number, + targetHeading: Heading, headings: Heading[] ): readonly [number, boolean] { - for (let i = targetSectionIndex + 1; i < headings.length; i++) { - if (headings[i].level <= targetSectionLevel) { - return [i, true]; + const targetHeadingIdx = headings.findIndex( + (heading) => heading.level === targetHeading.level && heading.line === targetHeading.line + ); + + const nextSameOrHigherLevelHeadingIdx = findNextIdx(headings, targetHeadingIdx, (heading) => { + return heading.level <= targetHeading.level; + }); + + if (nextSameOrHigherLevelHeadingIdx === null) { + return [-1, false]; + } + + return [nextSameOrHigherLevelHeadingIdx, true]; +} + +function findPriorIdx( + items: T[], + fromIdx: number, + condition: (item: T) => boolean +): number | null { + for (let i = fromIdx - 1; i >= 0; i--) { + if (condition(items[i])) { + return i; } } - return [-1, false]; + return null; // If no non-empty string is found before the given index } -function findNonEmptyStringIndexPriorToGivenIndex( - strings: string[], - givenIndex: number +function findNextIdx( + items: T[], + fromIdx: number, + condition: (item: T) => boolean ): number | null { - for (let i = givenIndex - 1; i >= 0; i--) { - if (strings[i].trim() !== "") { + for (let i = fromIdx + 1; i < items.length; i++) { + if (condition(items[i])) { return i; } } - return null; // If no non-empty string is found before the given index + return null; } From dbc606b7b81bd6052fdef9cdd4a050605efbde6d Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 27 Mar 2023 14:50:34 +0200 Subject: [PATCH 09/11] lint fixes --- src/gui/ChoiceBuilder/captureChoiceBuilder.ts | 6 +++--- src/quickAddSettingsTab.ts | 15 +++++++++------ src/settingsStore.ts | 5 +++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/gui/ChoiceBuilder/captureChoiceBuilder.ts b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts index 25e9e3b..0b6f00a 100644 --- a/src/gui/ChoiceBuilder/captureChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts @@ -88,7 +88,7 @@ export class CaptureChoiceBuilder extends ChoiceBuilder { captureToFileContainer.createEl("span"); const displayFormatter: FileNameDisplayFormatter = new FileNameDisplayFormatter(this.app); - (async () => + void (async () => (formatDisplay.textContent = await displayFormatter.format( this.choice.captureTo )))(); @@ -195,7 +195,7 @@ export class CaptureChoiceBuilder extends ChoiceBuilder { this.contentEl.createEl("span"); const displayFormatter: FormatDisplayFormatter = new FormatDisplayFormatter(this.app, this.plugin); - (async () => + void (async () => (insertAfterFormatDisplay.innerText = await displayFormatter.format( this.choice.insertAfter.after )))(); @@ -306,7 +306,7 @@ export class CaptureChoiceBuilder extends ChoiceBuilder { const formatDisplay: HTMLSpanElement = this.contentEl.createEl("span"); const displayFormatter: FormatDisplayFormatter = new FormatDisplayFormatter(this.app, this.plugin); - (async () => + void (async () => (formatDisplay.innerText = await displayFormatter.format( this.choice.format.format )))(); diff --git a/src/quickAddSettingsTab.ts b/src/quickAddSettingsTab.ts index dd73273..79482f7 100644 --- a/src/quickAddSettingsTab.ts +++ b/src/quickAddSettingsTab.ts @@ -1,4 +1,5 @@ -import { App, PluginSettingTab, Setting, TFolder } from "obsidian"; +import type { App } from "obsidian"; +import { PluginSettingTab, Setting, TFolder } from "obsidian"; import type QuickAdd from "./main"; import type IChoice from "./types/choices/IChoice"; import ChoiceView from "./gui/choiceList/ChoiceView.svelte"; @@ -63,10 +64,12 @@ export class QuickAddSettingsTab extends PluginSettingTab { addAnnounceUpdatesSetting() { const setting = new Setting(this.containerEl); setting.setName("Announce Updates"); - setting.setDesc("Display release notes when a new version is installed. This includes new features, demo videos, and bug fixes."); + setting.setDesc( + "Display release notes when a new version is installed. This includes new features, demo videos, and bug fixes." + ); setting.addToggle((toggle) => { toggle.setValue(settingsStore.getState().announceUpdates); - toggle.onChange(async (value) => { + toggle.onChange((value) => { settingsStore.setState({ announceUpdates: value }); }); }); @@ -87,11 +90,11 @@ export class QuickAddSettingsTab extends PluginSettingTab { app: this.app, plugin: this.plugin, choices: settingsStore.getState().choices, - saveChoices: async (choices: IChoice[]) => { + saveChoices: (choices: IChoice[]) => { settingsStore.setState({ choices }); }, macros: settingsStore.getState().macros, - saveMacros: async (macros: IMacro[]) => { + saveMacros: (macros: IMacro[]) => { settingsStore.setState({ macros }); }, }, @@ -133,7 +136,7 @@ export class QuickAddSettingsTab extends PluginSettingTab { setting.addText((text) => { text.setPlaceholder("templates/") .setValue(settingsStore.getState().templateFolderPath) - .onChange(async (value) => { + .onChange((value) => { settingsStore.setState({ templateFolderPath: value }); }); diff --git a/src/settingsStore.ts b/src/settingsStore.ts index dc9514c..f39858f 100644 --- a/src/settingsStore.ts +++ b/src/settingsStore.ts @@ -1,6 +1,7 @@ import { createStore } from "zustand/vanilla"; -import { DEFAULT_SETTINGS, QuickAddSettings } from "./quickAddSettingsTab"; -import { IMacro } from "./types/macros/IMacro"; +import type { QuickAddSettings } from "./quickAddSettingsTab"; +import { DEFAULT_SETTINGS } from "./quickAddSettingsTab"; +import type { IMacro } from "./types/macros/IMacro"; import { QuickAddMacro } from "./types/macros/QuickAddMacro"; // Define the state shape and actions for your store. From 26a9092de7fe4a6a6ae2eb87fe54d06fab7ea71a Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 27 Mar 2023 15:37:16 +0200 Subject: [PATCH 10/11] add option to consider subsections to capture choices --- src/gui/ChoiceBuilder/captureChoiceBuilder.ts | 46 +++++++++++++++++-- src/types/choices/CaptureChoice.ts | 2 + src/types/choices/ICaptureChoice.ts | 1 + 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/gui/ChoiceBuilder/captureChoiceBuilder.ts b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts index 0b6f00a..361db5a 100644 --- a/src/gui/ChoiceBuilder/captureChoiceBuilder.ts +++ b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts @@ -19,6 +19,7 @@ import { NewTabDirection } from "../../types/newTabDirection"; import type { FileViewMode } from "../../types/fileViewMode"; import { GenericTextSuggester } from "../suggesters/genericTextSuggester"; import { FormatSyntaxSuggester } from "../suggesters/formatSyntaxSuggester"; +import { log } from "src/logger/logManager"; export class CaptureChoiceBuilder extends ChoiceBuilder { choice: ICaptureChoice; @@ -136,7 +137,10 @@ export class CaptureChoiceBuilder extends ChoiceBuilder { toggle.onChange((value) => { this.choice.prepend = value; - if (this.choice.prepend && this.choice.insertAfter.enabled) { + if ( + this.choice.prepend && + this.choice.insertAfter.enabled + ) { this.choice.insertAfter.enabled = false; this.reload(); } @@ -182,8 +186,11 @@ export class CaptureChoiceBuilder extends ChoiceBuilder { toggle.onChange((value) => { this.choice.insertAfter.enabled = value; insertAfterInput.setDisabled(!value); - - if (this.choice.insertAfter.enabled && this.choice.prepend) { + + if ( + this.choice.insertAfter.enabled && + this.choice.prepend + ) { this.choice.prepend = false; } @@ -235,6 +242,39 @@ export class CaptureChoiceBuilder extends ChoiceBuilder { ) ); + const considerSubsectionsSetting: Setting = new Setting( + this.contentEl + ); + considerSubsectionsSetting + .setName("Consider subsections") + .setDesc( + "Enabling this will insert the text at the end of the section & its subsections, rather than just at the end of the target section." + + "A section is defined by a heading, and its subsections are all the headings inside that section." + ) + .addToggle((toggle) => + toggle + .setValue(this.choice.insertAfter?.considerSubsections) + .onChange( + (value) => { + // Trying to disable + if (!value) { + this.choice.insertAfter.considerSubsections = false; + return; + } + + // Trying to enable but `after` is not a heading + const targetIsHeading = this.choice.insertAfter.after.startsWith("#"); + if (targetIsHeading) { + this.choice.insertAfter.considerSubsections = value; + } else { + this.choice.insertAfter.considerSubsections = false; + log.logError("'Consider subsections' can only be enabled if the insert after line starts with a # (heading)."); + this.display(); + } + } + ) + ); + const createLineIfNotFound: Setting = new Setting(this.contentEl); createLineIfNotFound .setName("Create line if not found") diff --git a/src/types/choices/CaptureChoice.ts b/src/types/choices/CaptureChoice.ts index cdeebe9..ac4ca23 100644 --- a/src/types/choices/CaptureChoice.ts +++ b/src/types/choices/CaptureChoice.ts @@ -18,6 +18,7 @@ export class CaptureChoice extends Choice implements ICaptureChoice { enabled: boolean; after: string; insertAtEnd: boolean; + considerSubsections: boolean; createIfNotFound: boolean; createIfNotFoundLocation: string; }; @@ -47,6 +48,7 @@ export class CaptureChoice extends Choice implements ICaptureChoice { enabled: false, after: "", insertAtEnd: false, + considerSubsections: false, createIfNotFound: false, createIfNotFoundLocation: "top", }; diff --git a/src/types/choices/ICaptureChoice.ts b/src/types/choices/ICaptureChoice.ts index ac03cc4..86d1fea 100644 --- a/src/types/choices/ICaptureChoice.ts +++ b/src/types/choices/ICaptureChoice.ts @@ -19,6 +19,7 @@ export default interface ICaptureChoice extends IChoice { enabled: boolean; after: string; insertAtEnd: boolean; + considerSubsections: boolean; createIfNotFound: boolean; createIfNotFoundLocation: string; }; From 0cea9b4a8b27445798d2c76901b8a675761accc3 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 27 Mar 2023 15:38:05 +0200 Subject: [PATCH 11/11] use prior capture to end of section behavior with new subsection feature --- src/formatters/captureChoiceFormatter.ts | 3 +- .../helpers/getEndOfSection.test.ts | 69 ++++++++++--------- src/formatters/helpers/getEndOfSection.ts | 8 +-- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/formatters/captureChoiceFormatter.ts b/src/formatters/captureChoiceFormatter.ts index b109ed1..6f02ad9 100644 --- a/src/formatters/captureChoiceFormatter.ts +++ b/src/formatters/captureChoiceFormatter.ts @@ -121,7 +121,8 @@ export class CaptureChoiceFormatter extends CompleteFormatter { const endOfSectionIndex = getEndOfSection( fileContentLines, - targetPosition + targetPosition, + !!this.choice.insertAfter.considerSubsections ); targetPosition = endOfSectionIndex ?? fileContentLines.length - 1; } diff --git a/src/formatters/helpers/getEndOfSection.test.ts b/src/formatters/helpers/getEndOfSection.test.ts index 6d7f943..58a26b4 100644 --- a/src/formatters/helpers/getEndOfSection.test.ts +++ b/src/formatters/helpers/getEndOfSection.test.ts @@ -6,8 +6,8 @@ test("getEndOfSection - find the end of a section", () => { "# Title", "", "## Section 1", // target (2) - "Content 1", - "", // result (4) + "Content 1", // result (3) + "", "## Section 2", "Content 2", "", @@ -15,8 +15,8 @@ test("getEndOfSection - find the end of a section", () => { ]; const targetLine = 2; - const result = getEndOfSection(lines, targetLine); - expect(result).toBe(4); + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(3); }); test("getEndOfSection - find the end of the last section", () => { @@ -29,21 +29,21 @@ test("getEndOfSection - find the end of the last section", () => { "## Section 2", "Content 2", "", - "# Title 2", // target (8) - "", // result (9) + "# Title 2", // target & result (8) + "", ]; const targetLine = 8; - const result = getEndOfSection(lines, targetLine); - expect(result).toBe(9); + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(8); }); test("getEndOfSection - find end of section with multiple empty lines", () => { const lines = [ "# Title", "", - "## Section 1", - "Content 1", + "## Section 1", // target (2) + "Content 1", // result (4) "", "", "## Section 2", @@ -53,16 +53,16 @@ test("getEndOfSection - find end of section with multiple empty lines", () => { ]; const targetLine = 2; - const result = getEndOfSection(lines, targetLine); - expect(result).toBe(4); + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(3); }); test("getEndOfSection - find end of section without a higher level section", () => { const lines = [ "# Title", "", - "## Section 1", - "Content 1", + "## Section 1", // target (2) + "Content 1", // result (3) "", "## Section 2", "Content 2", @@ -72,35 +72,42 @@ test("getEndOfSection - find end of section without a higher level section", () ]; const targetLine = 2; - const result = getEndOfSection(lines, targetLine); - expect(result).toBe(4); + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(3); }); test("getEndOfSection - find end of section with higher level section", () => { const lines = [ - "# Title", + "# Title", // target (0) "", "## Section 1", "Content 1", "", "## Section 2", - "Content 2", - "", + "Content 2", // result (6) + "", "# Title 2", "Content 3", ]; const targetLine = 0; - const result = getEndOfSection(lines, targetLine); - expect(result).toBe(7); + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(6); }); test("getEndOfSection - find end of section with no headings", () => { - const lines = ["Content 1", "", "Content 2", "Content 3", "", "Content 4"]; + const lines = [ + "Content 1", + "", + "Content 2", // target (2) + "Content 3", // result (3) + "", + "Content 4" + ]; const targetLine = 2; const result = getEndOfSection(lines, targetLine); - expect(result).toBe(4); + expect(result).toBe(3); }); test("getEndOfSection - find end of section with top level heading and only sub headings", () => { @@ -119,15 +126,15 @@ test("getEndOfSection - find end of section with top level heading and only sub "3", "", "## Topic B", - "content b1", - "", // result (15) + "content b1", // result (14) + "", "", ]; const targetLine = 0; - const result = getEndOfSection(lines, targetLine); - expect(result).toBe(15); + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(14); }); test("getEndOfSection - target isn't heading", () => { @@ -137,8 +144,8 @@ test("getEndOfSection - target isn't heading", () => { "## Topic A", "content a1", // target (3) "content a2", - "content a3", - "", // result (6) + "content a3", // result (5) + "", "---", "Thematic break", "1", @@ -153,6 +160,6 @@ test("getEndOfSection - target isn't heading", () => { const targetLine = 3; - const result = getEndOfSection(lines, targetLine); - expect(result).toBe(6); + const result = getEndOfSection(lines, targetLine, false); + expect(result).toBe(5); }); diff --git a/src/formatters/helpers/getEndOfSection.ts b/src/formatters/helpers/getEndOfSection.ts index 0c22d6f..3c863a6 100644 --- a/src/formatters/helpers/getEndOfSection.ts +++ b/src/formatters/helpers/getEndOfSection.ts @@ -45,15 +45,15 @@ export default function getEndOfSection( ); } - if (targetIsNotHeading) { + if (targetIsNotHeading || !shouldConsiderSubsections) { const nextEmptyStringIdx = findNextIdx( lines, targetLine, (str: string) => str.trim() === "" ); - if (nextEmptyStringIdx !== null) { - return nextEmptyStringIdx; + if (nextEmptyStringIdx !== null && nextEmptyStringIdx > targetLine) { + return nextEmptyStringIdx - 1; } return targetLine; @@ -73,7 +73,7 @@ export default function getEndOfSection( ); if (lastNonEmptyLineInSectionIdx !== null) { - return lastNonEmptyLineInSectionIdx + 1; + return lastNonEmptyLineInSectionIdx; } return endOfSectionLineIdx;