From 239a4d5be2d16e4d76baa0ae218fae746ea7649a Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Thu, 9 May 2024 18:53:06 +0200 Subject: [PATCH 1/5] feat(std): make the function evaluator async and handle TE feat(suggesters): make SafeDataviewQuery async --- esbuild.config.mjs | 82 ++++++++++++++--------------- package-lock.json | 4 +- src/std/index.test.ts | 33 +++++++++--- src/std/index.ts | 57 ++++++++++---------- src/suggesters/SafeDataviewQuery.ts | 67 +++++++++++++---------- 5 files changed, 138 insertions(+), 105 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 2d727575..56119a46 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,9 +1,9 @@ -import esbuild from "esbuild"; -import process from "process"; import builtins from "builtin-modules"; +import esbuild from "esbuild"; import esbuildSvelte from "esbuild-svelte"; +import fs from "node:fs"; +import process from "process"; import sveltePreprocess from "svelte-preprocess"; -import fs from 'node:fs'; const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD @@ -14,46 +14,46 @@ if you want to view the source, please visit the github repository of this plugi const prod = process.argv[2] === "production"; const context = await esbuild.context({ - banner: { - js: banner, - }, - entryPoints: ["src/main.ts"], - bundle: true, - metafile: true, - plugins: [ - esbuildSvelte({ - compilerOptions: { css: "injected" }, - preprocess: sveltePreprocess(), - }), - ], - external: [ - "obsidian", - "electron", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", - ...builtins, - ], - format: "cjs", - target: "es2018", - logLevel: "info", - sourcemap: prod ? false : "inline", - treeShaking: true, - outfile: "main.js", + banner: { + js: banner, + }, + entryPoints: ["src/main.ts"], + bundle: true, + metafile: true, + plugins: [ + esbuildSvelte({ + compilerOptions: { css: "injected" }, + preprocess: sveltePreprocess(), + }), + ], + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins, + ], + format: "cjs", + target: "es2019", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", }); if (prod) { - const result = await context.rebuild(); - fs.writeFileSync('meta.json', JSON.stringify(result.metafile)) - process.exit(0); + const result = await context.rebuild(); + fs.writeFileSync("meta.json", JSON.stringify(result.metafile)); + process.exit(0); } else { - await context.watch(); + await context.watch(); } diff --git a/package-lock.json b/package-lock.json index fa39813a..da5ec238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-modal-form", - "version": "1.29.0", + "version": "1.40.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-modal-form", - "version": "1.29.0", + "version": "1.40.4", "license": "MIT", "dependencies": { "fp-ts": "^2.16.1", diff --git a/src/std/index.test.ts b/src/std/index.test.ts index 15dc930c..b8fcfd2c 100644 --- a/src/std/index.test.ts +++ b/src/std/index.test.ts @@ -1,5 +1,6 @@ +import * as TE from "fp-ts/TaskEither"; +import { array, boolean, number, object, string } from "valibot"; import { E, parseFunctionBody, pipe, trySchemas } from "./index"; -import { string, number, array, boolean, object } from "valibot"; describe("trySchemas", () => { const schema1 = object({ @@ -121,21 +122,37 @@ describe("parseFunctionBody", () => { ); }); it("should fail to parse a function body when it is incorrect", () => { - const input = "{ return x + 1; "; + const input = "% return x + 1; "; const result = parseFunctionBody(input); - expect(result).toEqual(E.left(new SyntaxError("Unexpected token ')'"))); + expect(result).toEqual(E.left(new SyntaxError("Unexpected token '%'"))); }); - it("should parse a function body with arguments and be able to execute it", () => { + it("should parse a function body with arguments and be able to execute it", (done) => { const input = "return x + 1;"; const result = parseFunctionBody<[number], number>(input, "x"); - pipe( + const fn = pipe( result, E.match( - () => fail("Expected a right"), - (result) => { - expect(result(1)).toEqual(E.right(2)); + () => { + throw new Error("Expected a right"); + }, + (parsedFn) => { + console.log(parsedFn.toString()); + return pipe( + parsedFn(1), + TE.match( + (error) => { + console.error({ error }); + throw new Error("Expected a right"); + }, + (result) => { + console.log({ result }); + return expect(result).toEqual(2); + }, + ), + ); }, ), ); + fn().then(done, done); }); }); diff --git a/src/std/index.ts b/src/std/index.ts index 4713cb07..7912ef25 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -1,41 +1,42 @@ -import { pipe as p, flow as f, absurd as _absurd } from "fp-ts/function"; import { - partitionMap, - findFirst, - findFirstMap, - partition, - map as mapArr, - filter, compact, + filter, filterMap, + findFirst, + findFirstMap, flatten, + map as mapArr, + partition, + partitionMap, } from "fp-ts/Array"; -import * as _O from "fp-ts/Option"; import { + Either, + ap, + bimap, + chainW, + flap, + flatMap, + fromNullable, + getOrElse, isLeft, isRight, - tryCatchK, - map, - getOrElse, - fromNullable, - right, left, + map, mapLeft, - Either, - bimap, - tryCatch, - flatMap, - ap, - flap, - chainW, match, + right, + tryCatch, + tryCatchK, } from "fp-ts/Either"; -export type Option = _O.Option; -import { BaseSchema, Output, ValiError, parse as parseV } from "valibot"; -import { Semigroup, concatAll } from "fp-ts/Semigroup"; import { NonEmptyArray, concatAll as concatAllNea } from "fp-ts/NonEmptyArray"; -export type { NonEmptyArray } from "fp-ts/NonEmptyArray"; +import * as _O from "fp-ts/Option"; +import { Semigroup, concatAll } from "fp-ts/Semigroup"; +import * as TE from "fp-ts/TaskEither"; +import { absurd as _absurd, flow as f, pipe as p } from "fp-ts/function"; +import { BaseSchema, Output, ValiError, parse as parseV } from "valibot"; +export type Option = _O.Option; export type { Either, Left, Right } from "fp-ts/Either"; +export type { NonEmptyArray } from "fp-ts/NonEmptyArray"; export const flow = f; export const pipe = p; export const absurd = _absurd; @@ -170,10 +171,12 @@ export function ensureError(e: unknown): Error { return e instanceof Error ? e : new Error(String(e)); } +// There is no way to access the constructor of an async function than the prototype chain +const AsyncFunction = new Function("return async () => {}")().constructor; /** * Creates a function from a string that is supposed to be a function body. * It ensures the "use strict" directive is present and returns the function. - * Because the parsing can fail, it returns an Either. + * Because the parsing can fail, it returns an TaskEither. * The reason why the type arguments are reversed is because * we often know what the function input types should be, but * we can't trust the function body to return the correct type, so by default1t it will be unknown @@ -183,8 +186,8 @@ export function parseFunctionBody(body: string, ...ar const fnBody = `"use strict"; ${body}`; try { - const fn = new Function(...args, fnBody) as (...args: Args) => T; - return right(tryCatchK(fn, ensureError)); + const fn = AsyncFunction(...args, fnBody) as (...args: Args) => Promise; + return right(TE.tryCatchK(fn, ensureError)); } catch (e) { return left(ensureError(e)); } diff --git a/src/suggesters/SafeDataviewQuery.ts b/src/suggesters/SafeDataviewQuery.ts index 91898265..c3707add 100644 --- a/src/suggesters/SafeDataviewQuery.ts +++ b/src/suggesters/SafeDataviewQuery.ts @@ -1,12 +1,17 @@ -import { E, Either, parseFunctionBody, pipe } from "@std"; +import { parseFunctionBody, pipe } from "@std"; +import * as T from "fp-ts/Task"; +import * as TE from "fp-ts/TaskEither"; import { App } from "obsidian"; -import { ModalFormError } from "src/utils/ModalFormError"; import { log_error } from "src/utils/Log"; +import { ModalFormError } from "src/utils/ModalFormError"; //type DataviewQuery = (dv: unknown, pages: unknown) => unknown; -export type SafeDataviewQuery = (dv: unknown, pages: unknown) => Either; +export type SafeDataviewQuery = ( + dv: unknown, + pages: unknown, +) => TE.TaskEither; /** - * From a string representing a dataview query, it returns the safest possible + * From a string representing a dataview query, it returns the safest possible * function that can be used to evaluate the query. * The function is sandboxed and it will return an Either. * If you want a convenient way to execute the query, use executeSandboxedDvQuery. @@ -14,23 +19,27 @@ export type SafeDataviewQuery = (dv: unknown, pages: unknown) => Either(query, 'dv', 'pages'); - return (dv: unknown, pages: unknown) => pipe( - parsed, - E.mapLeft((err) => - new ModalFormError('Error evaluating the dataview query', err.message), - ), - E.flatMap((fn) => fn(dv, pages)), - E.flatMap((result) => { - if (!Array.isArray(result)) { - return E.left(new ModalFormError('The dataview query did not return an array')); - } - return E.right(result); - }) - ); + const parsed = parseFunctionBody<[unknown, unknown], unknown>(query, "dv", "pages"); + return (dv: unknown, pages: unknown) => + pipe( + parsed, + TE.fromEither, + TE.mapLeft( + (err) => new ModalFormError("Error evaluating the dataview query", err.message), + ), + TE.flatMap((fn) => fn(dv, pages)), + TE.flatMap((result) => { + if (!Array.isArray(result)) { + return TE.left( + new ModalFormError("The dataview query did not return an array"), + ); + } + return TE.right(result); + }), + ); } type logger = typeof log_error; @@ -43,19 +52,23 @@ type logger = typeof log_error; * @param app the global obsidian app * @returns string[] if the query was executed successfully, otherwise an empty array */ -export function executeSandboxedDvQuery(query: SafeDataviewQuery, app: App, logger: logger = log_error): string[] { +export function executeSandboxedDvQuery( + query: SafeDataviewQuery, + app: App, + logger: logger = log_error, +): T.Task { const dv = app.plugins.plugins.dataview?.api; if (!dv) { - logger(new ModalFormError("Dataview plugin is not enabled")) - return [] as string[]; + logger(new ModalFormError("Dataview plugin is not enabled")); + return T.of([]); } const pages = dv.pages; return pipe( query(dv, pages), - E.getOrElse((e) => { + TE.getOrElse((e) => { logger(e); - return [] as string[]; - }) - ) + return T.of([] as string[]); + }), + ); } From dae4a251664d2e98118222400588dc1a4f7010bc Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Thu, 9 May 2024 18:56:33 +0200 Subject: [PATCH 2/5] feat(suggesters): DataviewSuggest is now async --- src/suggesters/suggestFromDataview.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/suggesters/suggestFromDataview.ts b/src/suggesters/suggestFromDataview.ts index 88ff14f5..ffe766e1 100644 --- a/src/suggesters/suggestFromDataview.ts +++ b/src/suggesters/suggestFromDataview.ts @@ -8,7 +8,7 @@ import { createRegexFromInput } from "./createRegexFromInput"; * For now, we are not very strict with the checks and just throw errors */ export class DataviewSuggest extends AbstractInputSuggest { - sandboxedQuery: SafeDataviewQuery + sandboxedQuery: SafeDataviewQuery; constructor( public inputEl: HTMLInputElement, @@ -16,14 +16,16 @@ export class DataviewSuggest extends AbstractInputSuggest { public app: App, ) { super(app, inputEl); - this.sandboxedQuery = sandboxedDvQuery(dvQuery) + this.sandboxedQuery = sandboxedDvQuery(dvQuery); } - getSuggestions(inputStr: string): string[] { - const result = executeSandboxedDvQuery(this.sandboxedQuery, this.app) - if (!inputStr) { return result } - const regex = createRegexFromInput(inputStr) - return result.filter((r) => regex.test(r)) + getSuggestions(inputStr: string): Promise { + const result = executeSandboxedDvQuery(this.sandboxedQuery, this.app); + if (!inputStr) { + return result(); + } + const regex = createRegexFromInput(inputStr); + return result().then((res) => res.filter((r) => regex.test(r))); } renderSuggestion(option: string, el: HTMLElement): void { From 1e23a1b3992ccc7e8f54365cea9cd63a1fe7f23c Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Thu, 9 May 2024 18:57:12 +0200 Subject: [PATCH 3/5] feat(input): document block is async --- src/FormModal.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/FormModal.ts b/src/FormModal.ts index ee77b469..de8a34f0 100644 --- a/src/FormModal.ts +++ b/src/FormModal.ts @@ -1,18 +1,19 @@ -import { App, Modal, Platform, Setting, sanitizeHTMLToDom } from "obsidian"; +import { E, absurd, parseFunctionBody, pipe, throttle } from "@std"; import * as R from "fp-ts/Record"; -import MultiSelect from "./views/components/MultiSelect.svelte"; +import * as TE from "fp-ts/TaskEither"; +import { App, Modal, Platform, Setting, sanitizeHTMLToDom } from "obsidian"; +import { SvelteComponent } from "svelte"; +import { Writable } from "svelte/store"; import FormResult, { type ModalFormData } from "./core/FormResult"; import { formDataFromFormDefaults } from "./core/formDataFromFormDefaults"; -import { get_tfiles_from_folder } from "./utils/files"; import type { FormDefinition, FormOptions } from "./core/formDefinition"; +import { FieldValue, FormEngine, makeFormEngine } from "./store/formStore"; import { FileSuggest } from "./suggesters/suggestFile"; +import { FolderSuggest } from "./suggesters/suggestFolder"; import { DataviewSuggest } from "./suggesters/suggestFromDataview"; -import { SvelteComponent } from "svelte"; -import { E, parseFunctionBody, pipe, throttle, absurd } from "@std"; import { log_error, log_notice } from "./utils/Log"; -import { FieldValue, FormEngine, makeFormEngine } from "./store/formStore"; -import { Writable } from "svelte/store"; -import { FolderSuggest } from "./suggesters/suggestFolder"; +import { get_tfiles_from_folder } from "./utils/files"; +import MultiSelect from "./views/components/MultiSelect.svelte"; import { MultiSelectModel, MultiSelectTags } from "./views/components/MultiSelectModel"; export type SubmitFn = (formResult: FormResult) => void; @@ -257,21 +258,22 @@ export class FormModal extends Modal { const sub = this.formEngine.subscribe((form) => { pipe( functionParsed, - E.chainW((fn) => + TE.fromEither, + TE.chainW((fn) => pipe( form.fields, R.filterMap((field) => field.value), fn, ), ), - E.match( + TE.match( (error) => { console.error(error); notifyError("Error in document block")(String(error)); }, (newText) => domNode.setText(sanitizeHTMLToDom(newText)), ), - ); + )(); }); return this.subscriptions.push(sub); } From cac23bb10aeb74b31b060f261948226cff854d63 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Thu, 9 May 2024 23:57:22 +0200 Subject: [PATCH 4/5] feat(input): dataview input can be fully async --- src/FormModal.ts | 10 +- src/views/components/MultiSelect.svelte | 94 ++++++++++--------- src/views/components/MultiSelectModel.ts | 8 +- .../components/inputBuilderDataview.svelte | 12 ++- 4 files changed, 67 insertions(+), 57 deletions(-) diff --git a/src/FormModal.ts b/src/FormModal.ts index de8a34f0..ef6c3b57 100644 --- a/src/FormModal.ts +++ b/src/FormModal.ts @@ -191,10 +191,12 @@ export class FormModal extends Modal { values: fieldStore.value as Writable, setting: fieldBase, errors: fieldStore.errors, - model: MultiSelectTags( - fieldInput, - this.app, - fieldStore.value as Writable, + model: Promise.resolve( + MultiSelectTags( + fieldInput, + this.app, + fieldStore.value as Writable, + ), ), }, }), diff --git a/src/views/components/MultiSelect.svelte b/src/views/components/MultiSelect.svelte index a5a12ac5..d66ab691 100644 --- a/src/views/components/MultiSelect.svelte +++ b/src/views/components/MultiSelect.svelte @@ -1,11 +1,9 @@
- 0} - /> - {#each $errors as error} - {error} - {/each} -
- {#each $values as value} -
- {value} - -
- {:else} - + {#await model then model} + {@const { createInput, removeValue } = model} + 0} + /> + {#each $errors as error} + {error} {/each} -
+
+ {#each $values as value} +
+ {value} + +
+ {:else} + + {/each} +
+ {:catch error} + Failure obtaining the options to display + {error.message} + {/await}