From 5dc16fe904506bfc5f14551d18cb95d9a77ffff7 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sun, 26 Jul 2026 23:20:08 +0200 Subject: [PATCH] feat(builder): advertise the format-token autocomplete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1542. Typing `{{` in a format field opens a list of every token, each with a one-line description (shipped in #1549). Nothing in the UI said so - the reporter only found it by typing `{{` on a hunch while looking for the syntax. Ask 1 of #1542 was deliberately left open because its two natural homes belonged to #1541 (docs links in the UI) and #1544 (a worked-example placeholder); both have shipped, and #1541 explicitly left "docs links inside the builders" here. The affordance is two characters, so the honest fix is to name the two characters, right below the caret: Type {{ to insert a token · Format syntax "Format syntax" is the docs entry point #1541 deferred: the token language is the one part of the builder you cannot work out by looking at it. The hint hides once the value contains `{{`. It has done its job, and a line that never goes away is the permanent chrome an "Insert token" button was rejected for. On three of the five token-autocomplete fields: Capture format, File name format, and Capture to - the only one a brand-new Capture builder renders, since the other two sit behind default-off toggles. Deliberately NOT on insert-after / insert-before: their own descriptions tell you to type a literal heading like `## Tasks`, so a token-less value is the documented normal state there and the hint would nag forever. Also considered and rejected: - Triggering the popup on a single `{`, which would make `{` + Enter insert a token instead of a newline in a capture format holding JSON or code. - Opening the list on focus: fires on every builder open, and races the file suggester on "Capture to". - Persisting a `hasUsedTokenAutocomplete` setting. Settings mutations rewrite the whole data.json, so the suggester would write to disk on a keystroke; no Svelte component subscribes to the store, so it would not be reactive; settings are per-vault, so "user lifetime" is false; and one stray `{{` would delete the affordance vault-wide with no way back. - Teaching via the placeholder alone: it vanishes on the first keystroke and on every existing choice, and cannot carry the docs link. --- src/docs.ts | 1 + .../ChoiceBuilder/CaptureChoiceForm.svelte | 2 + .../ChoiceBuilder/TemplateChoiceForm.svelte | 2 + .../components/CaptureTargetSetting.svelte | 2 + .../components/FormatTokenHint.svelte | 35 +++++++++++++ .../components/FormatTokenHint.test.ts | 52 +++++++++++++++++++ src/styles.css | 13 +++++ 7 files changed, 107 insertions(+) create mode 100644 src/gui/ChoiceBuilder/components/FormatTokenHint.svelte create mode 100644 src/gui/ChoiceBuilder/components/FormatTokenHint.test.ts diff --git a/src/docs.ts b/src/docs.ts index 277d8ec9..04f1d071 100644 --- a/src/docs.ts +++ b/src/docs.ts @@ -17,6 +17,7 @@ export const DOCS_BASE_URL = "https://quickadd.obsidian.guide"; export const DOCS_URLS = { gettingStarted: `${DOCS_BASE_URL}/docs/`, + formatSyntax: `${DOCS_BASE_URL}/docs/FormatSyntax/`, onePageInputs: `${DOCS_BASE_URL}/docs/Advanced/onePageInputs/`, userScripts: `${DOCS_BASE_URL}/docs/Choices/MacroChoice/#user-scripts`, packages: `${DOCS_BASE_URL}/docs/Choices/Packages/`, diff --git a/src/gui/ChoiceBuilder/CaptureChoiceForm.svelte b/src/gui/ChoiceBuilder/CaptureChoiceForm.svelte index 16590adc..3a577a3b 100644 --- a/src/gui/ChoiceBuilder/CaptureChoiceForm.svelte +++ b/src/gui/ChoiceBuilder/CaptureChoiceForm.svelte @@ -12,6 +12,7 @@ import ChoiceNameHeader from "./components/ChoiceNameHeader.svelte"; import ValidatedInput from "./components/ValidatedInput.svelte"; import LabeledField from "./components/LabeledField.svelte"; import FormatPreviewField from "./components/FormatPreviewField.svelte"; +import FormatTokenHint from "./components/FormatTokenHint.svelte"; import AppendLinkSetting from "./components/AppendLinkSetting.svelte"; import OpenFileSetting from "./components/OpenFileSetting.svelte"; import FileOpeningSetting from "./components/FileOpeningSetting.svelte"; @@ -170,6 +171,7 @@ function onTemplaterAfterCaptureChange(value: boolean) { requiredMessage="Capture format is required when enabled" makeSuggesters={formatSuggesters} /> + {/snippet} diff --git a/src/gui/ChoiceBuilder/TemplateChoiceForm.svelte b/src/gui/ChoiceBuilder/TemplateChoiceForm.svelte index aee1ebc6..44f5867f 100644 --- a/src/gui/ChoiceBuilder/TemplateChoiceForm.svelte +++ b/src/gui/ChoiceBuilder/TemplateChoiceForm.svelte @@ -34,6 +34,7 @@ import ChoiceNameHeader from "./components/ChoiceNameHeader.svelte"; import ValidatedInput from "./components/ValidatedInput.svelte"; import LabeledField from "./components/LabeledField.svelte"; import FormatPreviewField from "./components/FormatPreviewField.svelte"; +import FormatTokenHint from "./components/FormatTokenHint.svelte"; import AppendLinkSetting from "./components/AppendLinkSetting.svelte"; import OpenFileSetting from "./components/OpenFileSetting.svelte"; import FileOpeningSetting from "./components/FileOpeningSetting.svelte"; @@ -222,6 +223,7 @@ function onModeChange(value: string) { placeholder="File name format" makeSuggesters={fileNameSuggesters} /> + + {#if !usesPickerTargetSyntax} {/if} diff --git a/src/gui/ChoiceBuilder/components/FormatTokenHint.svelte b/src/gui/ChoiceBuilder/components/FormatTokenHint.svelte new file mode 100644 index 00000000..528f110a --- /dev/null +++ b/src/gui/ChoiceBuilder/components/FormatTokenHint.svelte @@ -0,0 +1,35 @@ + + +{#if shown} +
+ Type {{ to insert a token · + Format syntax +
+{/if} diff --git a/src/gui/ChoiceBuilder/components/FormatTokenHint.test.ts b/src/gui/ChoiceBuilder/components/FormatTokenHint.test.ts new file mode 100644 index 00000000..373cd6a0 --- /dev/null +++ b/src/gui/ChoiceBuilder/components/FormatTokenHint.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { render } from "@testing-library/svelte"; +import { tick } from "svelte"; +import FormatTokenHint from "./FormatTokenHint.svelte"; +import { DOCS_URLS } from "../../../docs"; + +const hint = (container: HTMLElement) => + container.querySelector(".qa-token-hint"); + +/** + * Issue #1542, ask 1. The token autocomplete opens on `{{` and nothing said so. + */ +describe("FormatTokenHint", () => { + it("names the two characters that open the autocomplete", async () => { + const { container } = render(FormatTokenHint, { props: { value: "" } }); + await tick(); + + const text = hint(container as HTMLElement)?.textContent ?? ""; + expect(text).toContain("{{"); + expect(text).toContain("to insert a token"); + }); + + it("links the format syntax reference", async () => { + const { container } = render(FormatTokenHint, { props: { value: "" } }); + await tick(); + + const link = container.querySelector("a.quickadd-docs-link"); + expect(link?.getAttribute("href")).toBe(DOCS_URLS.formatSyntax); + expect(link?.getAttribute("target")).toBe("_blank"); + expect(link?.getAttribute("rel")).toBe("noopener noreferrer"); + }); + + it("stays out of the way once the field uses a token", async () => { + const { container } = render(FormatTokenHint, { + props: { value: "- [ ] {{VALUE}}" }, + }); + await tick(); + expect(hint(container as HTMLElement)).toBeNull(); + }); + + it("shows again if the user removes every token", async () => { + const { container, rerender } = render(FormatTokenHint, { + props: { value: "{{DATE}}" }, + }); + await tick(); + expect(hint(container as HTMLElement)).toBeNull(); + + await rerender({ value: "plain text" }); + await tick(); + expect(hint(container as HTMLElement)).not.toBeNull(); + }); +}); diff --git a/src/styles.css b/src/styles.css index 855e159e..80cd0cfb 100644 --- a/src/styles.css +++ b/src/styles.css @@ -232,6 +232,19 @@ border: 0; } +/* "Type {{ to insert a token" under a format field (#1542). A quiet secondary + line, matching .qa-preview-row: it is a teaching aid, not part of the field. */ +.quickAddModal .qa-token-hint { + margin: -4px 0 8px; + font-size: var(--font-ui-smaller); + color: var(--text-muted); +} +.quickAddModal .qa-token-hint code { + font-size: inherit; + padding: 1px 4px; + border-radius: var(--radius-s, 4px); +} + /* Problems the preview pass ran into, shown here instead of as a Notice per keystroke (#1558). Colour carries severity — no glyph — matching .qa-folder-mode-warning. Clamped with the full text in `title`, because the