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..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 "../utility"; +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/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); } } 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..6f02ad9 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 { - escapeRegExp, - getLinesInString, - templaterParseTemplate, -} from "../utility"; +import { templaterParseTemplate } from "../utilityObsidian"; import { CREATE_IF_NOT_FOUND_BOTTOM, CREATE_IF_NOT_FOUND_TOP, } from "../constants"; +import { escapeRegExp, getLinesInString } from "src/utility"; +import getEndOfSection from "./helpers/getEndOfSection"; export class CaptureChoiceFormatter extends CompleteFormatter { private choice: ICaptureChoice; @@ -40,7 +38,7 @@ export class CaptureChoiceFormatter extends CompleteFormatter { formatted, this.file ); - if (!templaterFormatted) return formatted; + if (!(await templaterFormatted)) return formatted; return templaterFormatted; } @@ -74,7 +72,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,12 +97,13 @@ export class CaptureChoiceFormatter extends CompleteFormatter { const targetString: string = await this.format( this.choice.insertAfter.after ); + 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; @@ -117,44 +116,15 @@ 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 endOfSectionIndex = getEndOfSection( + fileContentLines, + targetPosition, + !!this.choice.insertAfter.considerSubsections + ); + targetPosition = endOfSectionIndex ?? fileContentLines.length - 1; } return this.insertTextAfterPositionInBody( @@ -175,7 +145,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 +162,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/getEndOfSection.test.ts b/src/formatters/helpers/getEndOfSection.test.ts new file mode 100644 index 0000000..0cdff94 --- /dev/null +++ b/src/formatters/helpers/getEndOfSection.test.ts @@ -0,0 +1,206 @@ +import { test, expect } from "vitest"; +import getEndOfSection from "./getEndOfSection"; + +test("getEndOfSection - find the end of a section", () => { + const lines = [ + "# Title", + "", + "## Section 1", // target (2) + "Content 1", // result (3) + "", + "## Section 2", + "Content 2", + "", + "# Title 2", + ]; + const targetLine = 2; + + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(3); +}); + +test("getEndOfSection - find the end of the last section", () => { + const lines = [ + "# Title", + "", + "## Section 1", + "Content 1", + "", + "## Section 2", + "Content 2", + "", + "# Title 2", // target (8) + "", // result (9) + ]; + const targetLine = 8; + + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(9); +}); + +test("getEndOfSection - find end of section with multiple empty lines", () => { + const lines = [ + "# Title", + "", + "## Section 1", // target (2) + "Content 1", // result (4) + "", + "", + "## Section 2", + "Content 2", + "", + "# Title 2", + ]; + const targetLine = 2; + + 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", // target (2) + "Content 1", // result (3) + "", + "## Section 2", + "Content 2", + "", + "## Section 3", + "Content 3", + ]; + const targetLine = 2; + + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(3); +}); + +test("getEndOfSection - find end of section with higher level section", () => { + const lines = [ + "# Title", // target (0) + "", + "## Section 1", + "Content 1", + "", + "## Section 2", + "Content 2", // result (6) + "", + "# Title 2", + "Content 3", + ]; + const targetLine = 0; + + 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", // target (2) + "Content 3", // result (3) + "", + "Content 4" + ]; + const targetLine = 2; + + const result = getEndOfSection(lines, targetLine); + expect(result).toBe(3); +}); + +test("getEndOfSection - find end of section with top level heading and only sub headings", () => { + const lines = [ + "# Notes", // target (0) + "", + "## Topic A", + "content a1", + "content a2", + "content a3", + "", + "---", + "Thematic break", + "1", + "2", + "3", + "", + "## Topic B", + "content b1", // result (14) + "", + "", + ]; + + const targetLine = 0; + + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(14); +}); + +test("getEndOfSection - target isn't heading", () => { + const lines = [ + "# Notes", + "", + "## Topic A", + "content a1", // target (3) + "content a2", + "content a3", // result (5) + "", + "---", + "Thematic break", + "1", + "2", + "3", + "", + "## Topic B", + "content b1", + "", + "", + ]; + + const targetLine = 3; + + const result = getEndOfSection(lines, targetLine, false); + expect(result).toBe(5); +}); + +test("getEndOfSection - target is heading, should not consider subsections", () => { + const lines = [ + "# Notes", + "", + "## Topic A", // target (2) + "content a1", + "content a2", + "content a3", // result (5) + "## Topic B", + "content b1", + "", + "", + ]; + + const targetLine = 2; + + const result = getEndOfSection(lines, targetLine, false); + expect(result).toBe(5); +}); + +test("getEndOfSection - target is heading, should consider subsections", () => { + const lines = [ + "# Notes", // target (0) + "", + "## Topic A", + "content a1", + "## Topic B", + "content b1", + "### contentA", + "content", + "#### contentB", + "content", + "content", // target (10) + ]; + + const targetLine = 0; + + const result = getEndOfSection(lines, targetLine, true); + expect(result).toBe(10); +}); \ No newline at end of file diff --git a/src/formatters/helpers/getEndOfSection.ts b/src/formatters/helpers/getEndOfSection.ts new file mode 100644 index 0000000..d053c09 --- /dev/null +++ b/src/formatters/helpers/getEndOfSection.ts @@ -0,0 +1,180 @@ +type Heading = { + level: number; + line: number; + text: string; +}; + +function isSameHeading(heading1: Heading, heading2: Heading): boolean { + return heading1.line === heading2.line; +} + +function getMarkdownHeadings(bodyLines: string[]): Heading[] { + const headers: Heading[] = []; + + bodyLines.forEach((line, index) => { + const match = line.match(/^(#+)[\s]?(.*)$/); + + if (!match) return; + + headers.push({ + level: match[1].length, + text: match[2], + 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( + lines: string[], + targetLine: number, + shouldConsiderSubsections = false +): number { + const headings = getMarkdownHeadings(lines); + + const targetHeading = headings.find( + (heading) => heading.line === targetLine + ); + const targetIsHeading = !!targetHeading; + + if (!targetIsHeading && shouldConsiderSubsections) { + throw new Error( + `Target line ${targetLine} is not a heading, but we are trying to find the end of its section.` + ); + } + + if (!targetIsHeading && !shouldConsiderSubsections) { + const nextEmptyStringIdx = findNextIdx( + lines, + targetLine, + (str: string) => str.trim() === "" + ); + + if (nextEmptyStringIdx !== null && nextEmptyStringIdx > targetLine) { + return nextEmptyStringIdx - 1; + } + + return targetLine; + } + + const lastLineInBodyIdx = lines.length - 1; + const endOfSectionLineIdx = getEndOfSectionLineByHeadings( + targetHeading as Heading, + headings, + lastLineInBodyIdx, + shouldConsiderSubsections + ); + + const lastNonEmptyLineInSectionIdx = findPriorIdx( + lines, + endOfSectionLineIdx, + (str: string) => str.trim() !== "" + ); + + if (lastNonEmptyLineInSectionIdx !== null) { + if (lastNonEmptyLineInSectionIdx + 1 === lastLineInBodyIdx) { + return endOfSectionLineIdx; + } + return lastNonEmptyLineInSectionIdx; + } + + return endOfSectionLineIdx; +} + +function getEndOfSectionLineByHeadings( + targetHeading: Heading, + headings: Heading[], + lastLineInBodyIdx: number, + shouldConsiderSubsections: boolean +): number { + const targetHeadingIdx = headings.findIndex((heading) => + isSameHeading(heading, targetHeading) + ); + const targetHeadingIsLastHeading = targetHeadingIdx === headings.length - 1; + + if (targetHeadingIsLastHeading) { + return lastLineInBodyIdx; + } + + const [nextHigherOrSameLevelHeadingIndex, foundHigherOrSameLevelHeading] = + findNextHigherOrSameLevelHeading(targetHeading, headings); + + const higherLevelSectionIsLastHeading = + foundHigherOrSameLevelHeading && + nextHigherOrSameLevelHeadingIndex === headings.length; + + if (higherLevelSectionIsLastHeading) { + return lastLineInBodyIdx; + } + + if (foundHigherOrSameLevelHeading && shouldConsiderSubsections) { + // If the target section is the last section of its level, and there are higher level sections, + const nextHigherLevelHeadingLineIdx = + headings[nextHigherOrSameLevelHeadingIndex].line; + return nextHigherLevelHeadingLineIdx - 1; + } + + if (foundHigherOrSameLevelHeading && !shouldConsiderSubsections) { + return headings[targetHeadingIdx + 1].line; + } + + // There are no higher level sections, but there may be more sections. + return lastLineInBodyIdx; +} + +function findNextHigherOrSameLevelHeading( + targetHeading: Heading, + headings: Heading[] +): readonly [number, boolean] { + const targetHeadingIdx = headings.findIndex((heading) => + isSameHeading(heading, targetHeading) + ); + + const nextSameOrHigherLevelHeadingIdx = findNextIdx( + headings, + targetHeadingIdx, + (heading) => 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 null; // If no non-empty string is found before the given index +} + +function findNextIdx( + items: T[], + fromIdx: number, + condition: (item: T) => boolean +): number | null { + for (let i = fromIdx + 1; i < items.length; i++) { + if (condition(items[i])) { + return i; + } + } + + return null; +} 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/gui/ChoiceBuilder/captureChoiceBuilder.ts b/src/gui/ChoiceBuilder/captureChoiceBuilder.ts index 25e9e3b..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; @@ -88,7 +89,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 )))(); @@ -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; } @@ -195,7 +202,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 )))(); @@ -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") @@ -306,7 +346,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/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/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/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/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[] = []; 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/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. 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; }; 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..78d136c --- /dev/null +++ b/src/utilityObsidian.ts @@ -0,0 +1,228 @@ +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) { + return app.commands.findCommand(commandId); +} + +export function deleteObsidianCommand(app: App, commandId: string) { + if (findObsidianCommand(app, commandId)) { + delete app.commands.commands[commandId]; + 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(); + + await leaf.setViewState({ + ...leafViewState, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + 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) { + // 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 + 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 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + 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) + ); +}