Skip to content

Commit

Permalink
feat: Add 'intellisense' like support for user scripts
Browse files Browse the repository at this point in the history
Merge pull request #1539 from srz2/feature/add-user-script-descriptions
Feature/add user script descriptions
  • Loading branch information
Zachatoo authored Feb 28, 2025
2 parents 47ad412 + c019bbd commit 4281da9
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 16 deletions.
3 changes: 3 additions & 0 deletions docs/documentation.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
name = "app"
description = "This module exposes the app instance. Prefer to use this over the global app instance."

[tp.user]
name = "user"
description = "This module exposes custom made scripts, written by yourself within the script file folder location"

[tp.config]
name = "config"
Expand Down
17 changes: 17 additions & 0 deletions docs/src/user-functions/script-user-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,20 @@ However, you can't access the template engine scoped variables like `tp` or `tR`
You can pass as many arguments as you want to your function, depending on how you defined it.

You can for example pass the `tp` object to your function, to be able to use all of the [internal variables / functions](../internal-variables-functions/overview.md) of Templater: `<% tp.user.<user_function_name>(tp) %>`

## User Script Documentation

Optionally you can document what a script does using the [TSDoc Standard](https://tsdoc.org/) at the **top** of your method file. If provided, this will provide an intellisense-like experience for your user scripts similar to the experience of the other templater functions.

### Example of User Script with Documentation

```javascript
/**
* This does something cool
*/
function doSomething() {
console.log('Something was done')
}

module.exports = doSomething;
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@codemirror/language": "github:lishid/cm-language",
"@codemirror/state": "^6.1.2",
"@codemirror/view": "^6.4.1",
"@microsoft/tsdoc": "^0.15.1",
"@popperjs/core": "^2.11.6",
"@silentvoid13/rusty_engine": "^0.4.0",
"child_process": "^1.0.2",
Expand Down
31 changes: 28 additions & 3 deletions src/editor/Autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TpSuggestDocumentation,
} from "./TpDocumentation";
import TemplaterPlugin from "main";
import { append_bolded_label_with_value_to_parent } from "utils/Utils";

export class Autocomplete extends EditorSuggest<TpSuggestDocumentation> {
//private in_command = false;
Expand Down Expand Up @@ -72,10 +73,10 @@ export class Autocomplete extends EditorSuggest<TpSuggestDocumentation> {
return trigger_info;
}

getSuggestions(context: EditorSuggestContext): TpSuggestDocumentation[] {
async getSuggestions(context: EditorSuggestContext): Promise<TpSuggestDocumentation[]> {
let suggestions: Array<TpSuggestDocumentation>;
if (this.module_name && this.function_trigger) {
suggestions = this.documentation.get_all_functions_documentation(
suggestions = await this.documentation.get_all_functions_documentation(
this.module_name as ModuleName,
this.function_name
) as TpFunctionDocumentation[];
Expand All @@ -92,7 +93,21 @@ export class Autocomplete extends EditorSuggest<TpSuggestDocumentation> {

renderSuggestion(value: TpSuggestDocumentation, el: HTMLElement): void {
el.createEl("b", { text: value.name });
el.createEl("br");
if (is_function_documentation(value))
{
if (value.args &&
this.getNumberOfArguments(value.args) > 0
) {
el.createEl('p', {text: "Parameter list:"})
const list = el.createEl("ol");
for (const [key, val] of Object.entries(value.args)) {
append_bolded_label_with_value_to_parent(list, key, val.description)
}
}
if (value.returns) {
append_bolded_label_with_value_to_parent(el, 'Returns', value.returns)
}
}
if (this.function_trigger && is_function_documentation(value)) {
el.createEl("code", { text: value.definition });
}
Expand Down Expand Up @@ -126,4 +141,14 @@ export class Autocomplete extends EditorSuggest<TpSuggestDocumentation> {
active_editor.editor.setCursor(cursor_pos);
}
}

getNumberOfArguments(
args: object
): number {
try {
return new Map(Object.entries(args)).size;
} catch (error) {
return 0;
}
}
}
54 changes: 41 additions & 13 deletions src/editor/TpDocumentation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import TemplaterPlugin from "main";
import { errorWrapperSync } from "utils/Error";
import { get_fn_params, get_tfiles_from_folder, is_object } from "utils/Utils";
import { errorWrapper } from "utils/Error";
import { get_fn_params, get_tfiles_from_folder, is_object, populate_docs_from_user_scripts } from "utils/Utils";
import documentation from "../../docs/documentation.toml";

const module_names = [
Expand Down Expand Up @@ -42,6 +42,7 @@ export type TpFunctionDocumentation = {
queryKey: string;
definition: string;
description: string;
returns: string;
example: string;
args?: {
[key: string]: TpArgumentDocumentation;
Expand All @@ -55,12 +56,15 @@ export type TpArgumentDocumentation = {

export type TpSuggestDocumentation =
| TpModuleDocumentation
| TpFunctionDocumentation;
| TpFunctionDocumentation
| TpArgumentDocumentation;

export function is_function_documentation(
x: TpSuggestDocumentation
): x is TpFunctionDocumentation {
if ((x as TpFunctionDocumentation).definition) {
if ((x as TpFunctionDocumentation).definition ||
(x as TpFunctionDocumentation).returns ||
(x as TpFunctionDocumentation).args) {
return true;
}
return false;
Expand All @@ -72,16 +76,24 @@ export class Documentation {
constructor(private plugin: TemplaterPlugin) {}

get_all_modules_documentation(): TpModuleDocumentation[] {
return Object.values(this.documentation.tp).map((mod) => {
let tp = this.documentation.tp

// Remove 'user' if no user scripts found
if (!this.plugin.settings ||
!this.plugin.settings.user_scripts_folder) {
tp = Object.values(tp).filter((x) => x.name !== 'user')
}

return Object.values(tp).map((mod) => {
mod.queryKey = mod.name;
return mod;
});
}

get_all_functions_documentation(
async get_all_functions_documentation(
module_name: ModuleName,
function_name: string
): TpFunctionDocumentation[] | undefined {
): Promise<TpFunctionDocumentation[] | undefined> {
if (module_name === "app") {
return this.get_app_functions_documentation(
this.plugin.app,
Expand All @@ -94,28 +106,43 @@ export class Documentation {
!this.plugin.settings.user_scripts_folder
)
return;
const files = errorWrapperSync(
() =>
get_tfiles_from_folder(
const files = await errorWrapper(
async () => {
const files = get_tfiles_from_folder(
this.plugin.app,
this.plugin.settings.user_scripts_folder
),
).filter(x => x.extension == "js")
const docFiles = await populate_docs_from_user_scripts(
this.plugin.app,
files
)
return docFiles;
},
`User Scripts folder doesn't exist`
);
if (!files || files.length === 0) return;
return files.reduce<TpFunctionDocumentation[]>(
(processedFiles, file) => {
if (file.extension !== "js") return processedFiles;
return [
const values = [
...processedFiles,
{
name: file.basename,
queryKey: file.basename,
definition: "",
description: "",
description: file.description,
returns: file.returns,
args: file.arguments.reduce<{[key: string]: TpArgumentDocumentation}>((acc, arg) => {
acc[arg.name] = {
name: arg.name,
description: arg.description
};
return acc;
}, {}),
example: "",
},
];
return values;
},
[]
);
Expand Down Expand Up @@ -174,6 +201,7 @@ export class Documentation {
)})`
: definition,
description: "",
returns: "",
example: "",
});
}
Expand Down
21 changes: 21 additions & 0 deletions src/utils/TJDocFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { TFile } from 'obsidian'

export class TJDocFile extends TFile {
public description: string
public returns: string
public arguments: TJDocFileArgument[]

constructor(file: TFile) {
super(file.vault, file.path)
Object.assign(this, file)
}
}

export class TJDocFileArgument {
public name: string
public description: string
constructor(name: string, desc: string) {
this.name = name;
this.description = desc;
}
}
113 changes: 113 additions & 0 deletions src/utils/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { DocBlock, DocNode, DocParamBlock, DocParamCollection, DocPlainText, DocSection, ParserContext, TSDocParser } from "@microsoft/tsdoc";

import { TJDocFile, TJDocFileArgument } from "./TJDocFile";

import { TemplaterError } from "./Error";
import {
App,
Expand Down Expand Up @@ -72,6 +76,91 @@ export function get_tfiles_from_folder(
return files;
}

export async function populate_docs_from_user_scripts(
app: App,
files: Array<TFile>
): Promise<TJDocFile[]> {
const docFiles = await Promise.all(files.map(async file => {
// Get file contents
const content = await app.vault.cachedRead(file)

const newDocFile = generate_jsdoc(file, content);

return newDocFile;
}
));

return docFiles;
}

function generate_jsdoc(
file: TFile,
content: string
): TJDocFile{
// Parse the content
const tsdocParser = new TSDocParser();
const parsedDoc = tsdocParser.parseString(content);

// Copy and extract information into the TJDocFile
const newDocFile = new TJDocFile(file);

newDocFile.description = generate_jsdoc_description(parsedDoc.docComment.summarySection);
newDocFile.returns = generate_jsdoc_return(parsedDoc.docComment.returnsBlock);
newDocFile.arguments = generate_jsdoc_arguments(parsedDoc.docComment.params);

return newDocFile
}

function generate_jsdoc_description(
summarySection: DocSection
) : string {
try {
const description = summarySection.nodes.map((node: DocNode) =>
node.getChildNodes()
.filter((node: DocNode) => node instanceof DocPlainText)
.map((x: DocPlainText) => x.text)
.join("\n")
);

return description.join("\n");
} catch (error) {
console.error('Failed to parse sumamry section');
throw error;
}
}

function generate_jsdoc_return(
returnSection : DocBlock | undefined
): string {
if (!returnSection) return "";

try {
const returnValue = returnSection.content.nodes[0].getChildNodes()[0].text.trim();
return returnValue;
} catch (error) {
return "";
}
}

function generate_jsdoc_arguments(
paramSection: DocParamCollection
) : TJDocFileArgument[] {
try {
const blocks = paramSection.blocks;
const args = blocks.map((block) => {
const name = block.parameterName;
const description = block.content.getChildNodes()[0].getChildNodes()
.filter(x => x instanceof DocPlainText)
.map(x => x.text).join(" ")
return new TJDocFileArgument(name, description);
})

return args;
} catch (error) {
return [];
}
}

export function arraymove<T>(
arr: T[],
fromIndex: number,
Expand Down Expand Up @@ -113,3 +202,27 @@ export function get_fn_params(func: (...args: unknown[]) => unknown) {
.replace(/ /g, "")
.split(",");
}

/**
* Use a parent HtmlElement to create a label with a value
* @param parent The parent HtmlElement; Use HtmlOListElement to return a `li` element
* @param title The title for the label which will be bolded
* @param value The value of the label
* @returns A label HtmlElement (p | li)
*/
export function append_bolded_label_with_value_to_parent(
parent: HTMLElement,
title: string,
value: string
): HTMLElement{
const tag = parent instanceof HTMLOListElement ? "li" : "p";

const para = parent.createEl(tag);
const bold = parent.createEl('b', {text: title});
para.appendChild(bold);
para.appendChild(document.createTextNode(`: ${value}`))

// Returns a p or li element
// Resulting in <b>Title</b>: value
return para;
}

0 comments on commit 4281da9

Please sign in to comment.