From f8e3f65488d61092aa70df14a6571fadd0de349c Mon Sep 17 00:00:00 2001 From: Zachatoo Date: Mon, 21 Oct 2024 20:34:48 -0600 Subject: [PATCH] feat: Add `tp.app` module with autocompletions --- docs/documentation.toml | 5 ++ docs/src/SUMMARY.md | 1 + .../internal-modules/app-module.md | 26 +++++++ .../internal-modules/hooks-module.md | 4 +- .../internal-modules/obsidian-module.md | 2 +- docs/src/internal-functions/overview.md | 1 + src/core/functions/FunctionsGenerator.ts | 1 + src/editor/Autocomplete.ts | 11 +-- src/editor/TpDocumentation.ts | 77 ++++++++++++++++++- src/utils/Utils.ts | 13 ++++ 10 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 docs/src/internal-functions/internal-modules/app-module.md diff --git a/docs/documentation.toml b/docs/documentation.toml index 2c250c46..c99b4a3e 100644 --- a/docs/documentation.toml +++ b/docs/documentation.toml @@ -1,3 +1,8 @@ +[tp.app] +name = "app" +description = "This module exposes the app instance. Prefer to use this over the global app instance." + + [tp.config] name = "config" description = """This module exposes Templater's running configuration. diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index d5f9dbf2..aa51fbd6 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -7,6 +7,7 @@ - [Settings](./settings.md) - [FAQ](./faq.md) - [Internal Functions](./internal-functions/overview.md) + - [tp.app](./internal-functions/internal-modules/app-module.md) - [tp.config](./internal-functions/internal-modules/config-module.md) - [tp.date](./internal-functions/internal-modules/date-module.md) - [tp.file](./internal-functions/internal-modules/file-module.md) diff --git a/docs/src/internal-functions/internal-modules/app-module.md b/docs/src/internal-functions/internal-modules/app-module.md new file mode 100644 index 00000000..93fa3033 --- /dev/null +++ b/docs/src/internal-functions/internal-modules/app-module.md @@ -0,0 +1,26 @@ +# App Module + +{{ tp.app.description }} + +This is mostly useful when writing scripts. + +Refer to the Obsidian [developer documentation](https://docs.obsidian.md/Reference/TypeScript+API/App) for more information. + +## Examples + +```javascript +// Get all folders +<% +tp.app.vault.getAllLoadedFiles() + .filter(x => x instanceof tp.obsidian.TFolder) + .map(x => x.name) +%> + +// Update frontmatter of existing file +<%* +const file = tp.file.find_tfile("path/to/file"); +await tp.app.fileManager.processFrontMatter(file, (frontmatter) => { + frontmatter["key"] = "value"; +}); +%> +``` \ No newline at end of file diff --git a/docs/src/internal-functions/internal-modules/hooks-module.md b/docs/src/internal-functions/internal-modules/hooks-module.md index 25f22cb8..7204f6f6 100644 --- a/docs/src/internal-functions/internal-modules/hooks-module.md +++ b/docs/src/internal-functions/internal-modules/hooks-module.md @@ -30,7 +30,7 @@ Function documentation is using a specific syntax. More information [here](../.. <%* tp.hooks.on_all_templates_executed(async () => { const file = tp.file.find_tfile(tp.file.path(true)); - await app.fileManager.processFrontMatter(file, (frontmatter) => { + await tp.app.fileManager.processFrontMatter(file, (frontmatter) => { frontmatter["key"] = "value"; }); }); @@ -38,7 +38,7 @@ tp.hooks.on_all_templates_executed(async () => { // Run a command from another plugin that modifies the current file, after Templater has updated the file <%* tp.hooks.on_all_templates_executed(() => { - app.commands.executeCommandById("obsidian-linter:lint-file"); + tp.app.commands.executeCommandById("obsidian-linter:lint-file"); }); -%> ``` diff --git a/docs/src/internal-functions/internal-modules/obsidian-module.md b/docs/src/internal-functions/internal-modules/obsidian-module.md index 837a7688..8b0af437 100644 --- a/docs/src/internal-functions/internal-modules/obsidian-module.md +++ b/docs/src/internal-functions/internal-modules/obsidian-module.md @@ -11,7 +11,7 @@ Refer to the Obsidian API [declaration file](https://github.com/obsidianmd/obsid ```javascript // Get all folders <% -app.vault.getAllLoadedFiles() +tp.app.vault.getAllLoadedFiles() .filter(x => x instanceof tp.obsidian.TFolder) .map(x => x.name) %> diff --git a/docs/src/internal-functions/overview.md b/docs/src/internal-functions/overview.md index 4a71e867..b6a92958 100644 --- a/docs/src/internal-functions/overview.md +++ b/docs/src/internal-functions/overview.md @@ -2,6 +2,7 @@ The different internal variables and functions offered by [Templater](https://github.com/SilentVoid13/Templater) are available under different **modules**, to sort them. The existing **internal modules** are: +- [App module](./internal-modules/app-module.md): `tp.app` - [Config module](./internal-modules/config-module.md): `tp.config` - [Date module](./internal-modules/date-module.md): `tp.date` - [File module](./internal-modules/file-module.md): `tp.file` diff --git a/src/core/functions/FunctionsGenerator.ts b/src/core/functions/FunctionsGenerator.ts index 0f18b094..a7a2e353 100644 --- a/src/core/functions/FunctionsGenerator.ts +++ b/src/core/functions/FunctionsGenerator.ts @@ -29,6 +29,7 @@ export class FunctionsGenerator implements IGenerateObject { additional_functions(): Record { return { + app: this.plugin.app, obsidian: obsidian_module, }; } diff --git a/src/editor/Autocomplete.ts b/src/editor/Autocomplete.ts index 2512c5ea..80d3f1ae 100644 --- a/src/editor/Autocomplete.ts +++ b/src/editor/Autocomplete.ts @@ -21,7 +21,7 @@ export class Autocomplete extends EditorSuggest { //private in_command = false; // https://regex101.com/r/ocmHzR/1 private tp_keyword_regex = - /tp\.(?[a-z]*)?(?\.(?[a-z_]*)?)?$/; + /tp\.(?[a-z]*)?(?\.(?[a-zA-Z_.]*)?)?$/; private documentation: Documentation; private latest_trigger_info: EditorSuggestTriggerInfo; private module_name: ModuleName | string; @@ -76,7 +76,8 @@ export class Autocomplete extends EditorSuggest { let suggestions: Array; if (this.module_name && this.function_trigger) { suggestions = this.documentation.get_all_functions_documentation( - this.module_name as ModuleName + this.module_name as ModuleName, + this.function_name ) as TpFunctionDocumentation[]; } else { suggestions = this.documentation.get_all_modules_documentation(); @@ -84,7 +85,7 @@ export class Autocomplete extends EditorSuggest { if (!suggestions) { return []; } - return suggestions.filter((s) => s.name.startsWith(context.query)); + return suggestions.filter((s) => s.queryKey.startsWith(context.query)); } renderSuggestion(value: TpSuggestDocumentation, el: HTMLElement): void { @@ -108,7 +109,7 @@ export class Autocomplete extends EditorSuggest { return; } active_editor.editor.replaceRange( - value.name, + value.queryKey, this.latest_trigger_info.start, this.latest_trigger_info.end ); @@ -119,7 +120,7 @@ export class Autocomplete extends EditorSuggest { // beginning of the word after completion, // Not sure what's the cause of this bug. const cursor_pos = this.latest_trigger_info.end; - cursor_pos.ch += value.name.length; + cursor_pos.ch += value.queryKey.length; active_editor.editor.setCursor(cursor_pos); } } diff --git a/src/editor/TpDocumentation.ts b/src/editor/TpDocumentation.ts index c0ec9ff0..4c7f04b4 100644 --- a/src/editor/TpDocumentation.ts +++ b/src/editor/TpDocumentation.ts @@ -1,9 +1,10 @@ import TemplaterPlugin from "main"; import { errorWrapperSync } from "utils/Error"; -import { get_tfiles_from_folder } from "utils/Utils"; +import { get_fn_params, get_tfiles_from_folder, is_object } from "utils/Utils"; import documentation from "../../docs/documentation.toml"; const module_names = [ + "app", "config", "date", "file", @@ -29,6 +30,7 @@ export type TpDocumentation = { export type TpModuleDocumentation = { name: string; + queryKey: string; description: string; functions: { [key: string]: TpFunctionDocumentation; @@ -37,6 +39,7 @@ export type TpModuleDocumentation = { export type TpFunctionDocumentation = { name: string; + queryKey: string; definition: string; description: string; example: string; @@ -69,12 +72,22 @@ export class Documentation { constructor(private plugin: TemplaterPlugin) {} get_all_modules_documentation(): TpModuleDocumentation[] { - return Object.values(this.documentation.tp); + return Object.values(this.documentation.tp).map((mod) => { + mod.queryKey = mod.name; + return mod; + }); } get_all_functions_documentation( - module_name: ModuleName + module_name: ModuleName, + function_name: string ): TpFunctionDocumentation[] | undefined { + if (module_name === "app") { + return this.get_app_functions_documentation( + this.plugin.app, + function_name + ); + } if (module_name === "user") { if ( !this.plugin.settings || @@ -97,6 +110,7 @@ export class Documentation { ...processedFiles, { name: file.basename, + queryKey: file.basename, definition: "", description: "", example: "", @@ -109,7 +123,62 @@ export class Documentation { if (!this.documentation.tp[module_name].functions) { return; } - return Object.values(this.documentation.tp[module_name].functions); + return Object.values(this.documentation.tp[module_name].functions).map( + (mod) => { + mod.queryKey = mod.name; + return mod; + } + ); + } + + private get_app_functions_documentation( + obj: unknown, + path: string + ): TpFunctionDocumentation[] { + if (!is_object(obj)) { + return []; + } + const parts = path.split("."); + if (parts.length === 0) { + return []; + } + + let currentObj = obj; + for (let index = 0; index < parts.length - 1; index++) { + const part = parts[index]; + if (part in currentObj) { + if (!is_object(currentObj[part])) { + return []; + } + currentObj = currentObj[part]; + } + } + + const definitionPrefix = [ + "tp", + "app", + ...parts.slice(0, parts.length - 1), + ].join("."); + const queryKeyPrefix = parts.slice(0, parts.length - 1).join("."); + const docs: TpFunctionDocumentation[] = []; + for (const key in currentObj) { + const definition = `${definitionPrefix}.${key}`; + const queryKey = queryKeyPrefix ? `${queryKeyPrefix}.${key}` : key; + docs.push({ + name: key, + queryKey, + definition: + typeof currentObj[key] === "function" + ? `${definition}(${get_fn_params( + currentObj[key] as (...args: unknown[]) => unknown + )})` + : definition, + description: "", + example: "", + }); + } + + return docs; } get_module_documentation(module_name: ModuleName): TpModuleDocumentation { diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 6e904270..f644b8e1 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -100,3 +100,16 @@ export function get_folder_path_from_file_path(path: string) { if (path_separator !== -1) return path.slice(0, path_separator); return ""; } + +export function is_object(obj: unknown): obj is Record { + return obj !== null && typeof obj === "object"; +} + +export function get_fn_params(func: (...args: unknown[]) => unknown) { + const str = func.toString(); + const len = str.indexOf("("); + return str + .substring(len + 1, str.indexOf(")")) + .replace(/ /g, "") + .split(","); +}