Skip to content

Commit 78fd184

Browse files
authored
feat: improve prompt labeling for VALUE/MACRO and multi choices
* feat: add labels and placeholders for prompts * feat: use option syntax for VALUE labels * docs: clarify VALUE option default syntax * refactor: use sentinel delimiter for VALUE label keys * test: key VALUE label requirements via parser * refactor: share VALUE label key helper * refactor: centralize VALUE key handling * test: add valueSyntax coverage * feat: add label option to macro syntax * fix: require label option for macro labels
1 parent 53a417c commit 78fd184

33 files changed

+888
-167
lines changed

docs/docs/Choices/MultiChoice.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ Multi-choices are pretty simple. They're like folders for other choices. Here ar
99
To actually add something in this "folder", you need to drag it in! This is not easy to do when it is the first item in the multi-folder.
1010

1111
Make sure the multi is unfolded (as it is in the screenshot). Click the drag handle of one of the choices you want to add and drag it to just below and to the right of the drag handle for the multi. When successful, the choice will be indented under the multi.
12+
13+
## Placeholder text
14+
15+
You can optionally set a placeholder for each Multi choice. This text shows up in the choice picker search box when you open the multi, which is handy for complex menus or grouped workflows. Leave it empty to keep the default placeholder.

docs/docs/FormatSyntax.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ title: Format syntax
1010
| `{{VDATE:<variable name>, <date format>\|<default>}}` | Same as above, but with a default value. If you leave the prompt empty, the default value will be used instead. Example: `{{VDATE:date,YYYY-MM-DD\|today}}` will use "today" if no input is provided. Default values can be any natural language date like "tomorrow", "next monday", "+7 days", etc. Short aliases like `t`, `tm`, and `yd` work here too. **Note:** If your date format contains pipe characters (`|`), you'll need to escape them as `\|` or use square brackets like `[|]` to avoid conflicts with the default value separator. |
1111
| `{{VALUE}}` or `{{NAME}}` | Interchangeable. Represents the value given in an input prompt. If text is selected in the current editor, it will be used as the value. When using the QuickAdd API, this can be passed programmatically using the reserved variable name 'value'. |
1212
| `{{VALUE:<variable name>}}` | You can now use variable names in values. They'll get saved and inserted just like values, but the difference is that you can have as many of them as you want. Use comma separation to get a suggester rather than a prompt. |
13-
| `{{VALUE:<variable name>\|<default>}}` | Same as above, but with a default value. For single-value prompts (e.g., `{{VALUE:name\|Anonymous}}`), the default is pre-populated in the input field - press Enter to accept or clear/edit it. For multi-value suggesters without `\|custom`, you must select one of the provided options (no default applies). |
14-
| `{{VALUE:<options>\|custom}}` | Allows you to type custom values in addition to selecting from the provided options. Example: `{{VALUE:Red,Green,Blue\|custom}}` will suggest Red, Green, and Blue, but also allows you to type any other value like "Purple". This is useful when you have common options but want flexibility for edge cases. **Note:** You cannot combine `\|custom` with a default value - it's either one or the other. |
13+
| `{{VALUE:<variable name>\|label:<helper text>}}` | Adds helper text to the prompt for a single-value input. The helper appears below the header and is useful for reminders or instructions. For multi-value lists, use the same syntax to label the suggester (e.g., `{{VALUE:Red,Green,Blue\|label:Pick a color}}`). |
14+
| `{{VALUE:<variable name>\|<default>}}` | Same as above, but with a default value. For single-value prompts (e.g., `{{VALUE:name\|Anonymous}}`), the default is pre-populated in the input field - press Enter to accept or clear/edit it. For multi-value suggesters without `\|custom`, you must select one of the provided options (no default applies). If you combine options like `\|label:...`, use `\|default:<value>` instead of the shorthand (mixing option keys with a bare default is not supported). |
15+
| `{{VALUE:<variable name>\|default:<value>}}` | Option-form default value, required when combining with other options like `\|label:`. Example: `{{VALUE:title\|label:Snake case\|default:My_Title}}`. |
16+
| `{{VALUE:<options>\|custom}}` | Allows you to type custom values in addition to selecting from the provided options. Example: `{{VALUE:Red,Green,Blue\|custom}}` will suggest Red, Green, and Blue, but also allows you to type any other value like "Purple". This is useful when you have common options but want flexibility for edge cases. **Note:** You cannot combine `\|custom` with a shorthand default value - use `\|default:` if you need both. |
1517
| `{{LINKCURRENT}}` | A link to the file from which the template or capture was triggered (`[[link]]` format). When the append-link setting is set to **Enabled (skip if no active file)**, this token resolves to an empty string instead of throwing an error if no note is focused. |
1618
| `{{FILENAMECURRENT}}` | The basename (without extension) of the file from which the template or capture was triggered. Honors the same **required/optional** behavior as `{{LINKCURRENT}}` - when optional and no active file exists, resolves to an empty string. |
1719
| `{{MACRO:<MACRONAME>}}` | Execute a macro and write the return value here. |
20+
| `{{MACRO:<MACRONAME>\|label:<label>}}` | Executes the macro but shows the label as the placeholder when the macro prompts you to choose an export from a script object. This is helpful when multiple macro calls show similar lists. |
1821
| `{{TEMPLATE:<TEMPLATEPATH>}}` | Include templates in your `format`. Supports Templater syntax. |
1922
| `{{GLOBAL_VAR:<name>}}` | Inserts the value of a globally defined snippet from QuickAdd settings. Snippet values can include other QuickAdd tokens (e.g., `{{VALUE:...}}`, `{{VDATE:...}}`) and are processed by the usual formatter passes. Names match case‑insensitively in the token. |
2023
| `{{MVALUE}}` | Math modal for writing LaTeX. Use CTRL + Enter to submit. |

src/choiceExecutor.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,24 @@ export class ChoiceExecutor implements IChoiceExecutor {
3333

3434
async execute(choice: IChoice): Promise<void> {
3535
this.pendingAbort = null;
36-
// One-page preflight honoring per-choice override
37-
const globalEnabled = settingsStore.getState().onePageInputEnabled;
38-
const override = choice.onePageInput;
39-
const shouldUseOnePager = (override === "always") || (override !== "never" && globalEnabled);
40-
if (shouldUseOnePager && (choice.type === "Template" || choice.type === "Capture" || choice.type === "Macro")) {
36+
// One-page preflight honoring per-choice override.
37+
const globalEnabled = settingsStore.getState().onePageInputEnabled;
38+
const override = choice.onePageInput;
39+
const shouldUseOnePager =
40+
override === "always" || (override !== "never" && globalEnabled);
41+
if (
42+
shouldUseOnePager &&
43+
(choice.type === "Template" ||
44+
choice.type === "Capture" ||
45+
choice.type === "Macro")
46+
) {
4147
try {
42-
await runOnePagePreflight(this.app, this.plugin as unknown as QuickAdd, this, choice);
48+
await runOnePagePreflight(
49+
this.app,
50+
this.plugin as unknown as QuickAdd,
51+
this,
52+
choice,
53+
);
4354
} catch (error) {
4455
if (isCancellationError(error)) {
4556
throw new MacroAbortError("One-page input cancelled by user");
@@ -111,6 +122,9 @@ export class ChoiceExecutor implements IChoiceExecutor {
111122
}
112123

113124
private onChooseMultiType(multiChoice: IMultiChoice) {
114-
ChoiceSuggester.Open(this.plugin, multiChoice.choices, this);
125+
ChoiceSuggester.Open(this.plugin, multiChoice.choices, {
126+
choiceExecutor: this,
127+
placeholder: multiChoice.placeholder,
128+
});
115129
}
116130
}

src/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export const NAME_SYNTAX = "{{name}}";
55
export const VARIABLE_SYNTAX = "{{value:<variable name>}}";
66
export const VARIABLE_DEFAULT_SYNTAX =
77
"{{value:<variable name>|<default value>}}";
8+
export const VARIABLE_DEFAULT_OPTION_SYNTAX =
9+
"{{value:<variable name>|default:<value>}}";
10+
export const VARIABLE_LABEL_SYNTAX =
11+
"{{value:<variable name>|label:<helper text>}}";
812
export const FIELD_VAR_SYNTAX = "{{field:<field name>}}";
913
export const MATH_VALUE_SYNTAX = "{{mvalue}}";
1014
export const LINKCURRENT_SYNTAX = "{{linkcurrent}}";
@@ -25,13 +29,16 @@ export const FORMAT_SYNTAX: string[] = [
2529
NAME_SYNTAX,
2630
VARIABLE_SYNTAX,
2731
VARIABLE_DEFAULT_SYNTAX,
32+
VARIABLE_DEFAULT_OPTION_SYNTAX,
33+
VARIABLE_LABEL_SYNTAX,
2834
FIELD_VAR_SYNTAX,
2935
"{{field:<fieldname>|folder:<path>}}",
3036
"{{field:<fieldname>|tag:<tagname>}}",
3137
"{{field:<fieldname>|inline:true}}",
3238
LINKCURRENT_SYNTAX,
3339
FILENAMECURRENT_SYNTAX,
3440
"{{macro:<macroname>}}",
41+
"{{macro:<macroname>|label:<label>}}",
3542
"{{template:<templatepath>}}",
3643
MATH_VALUE_SYNTAX,
3744
SELECTED_SYNTAX,
@@ -49,6 +56,8 @@ export const FILE_NAME_FORMAT_SYNTAX: string[] = [
4956
NAME_SYNTAX,
5057
VARIABLE_SYNTAX,
5158
VARIABLE_DEFAULT_SYNTAX,
59+
VARIABLE_DEFAULT_OPTION_SYNTAX,
60+
VARIABLE_LABEL_SYNTAX,
5261
FIELD_VAR_SYNTAX,
5362
RANDOM_SYNTAX,
5463
];

src/engine/MacroChoiceEngine.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
7979
private userScriptCommand: IUserScript | null;
8080
private conditionalScriptCache = new Map<string, ConditionalScriptRunner>();
8181
private readonly preloadedUserScripts: Map<string, unknown>;
82+
private readonly promptLabel?: string;
8283
private buildParams(
8384
app: App,
8485
plugin: QuickAdd,
@@ -151,14 +152,16 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
151152
choice: IMacroChoice,
152153
choiceExecutor: IChoiceExecutor,
153154
variables: Map<string, unknown>,
154-
preloadedUserScripts?: Map<string, unknown>
155+
preloadedUserScripts?: Map<string, unknown>,
156+
promptLabel?: string,
155157
) {
156158
super(app);
157159
this.choice = choice;
158160
this.plugin = plugin;
159161
this.macro = choice?.macro;
160162
this.choiceExecutor = choiceExecutor;
161163
this.preloadedUserScripts = preloadedUserScripts ?? new Map();
164+
this.promptLabel = promptLabel;
162165
const sharedVariables = this.initSharedVariables(
163166
choiceExecutor,
164167
variables
@@ -372,7 +375,8 @@ export class MacroChoiceEngine extends QuickAddChoiceEngine {
372375
const selected: string = await GenericSuggester.Suggest(
373376
this.app,
374377
keys,
375-
keys
378+
keys,
379+
this.promptLabel,
376380
);
377381

378382
await this.userScriptDelegator(obj[selected]);

src/engine/SingleMacroEngine.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ export class SingleMacroEngine {
4242
}
4343
}
4444

45-
public async runAndGetOutput(macroName: string): Promise<string> {
45+
public async runAndGetOutput(
46+
macroName: string,
47+
context?: { label?: string },
48+
): Promise<string> {
4649
const { basename, memberAccess } = getUserScriptMemberAccess(macroName);
4750

4851
// ------------------------------------------------------------------
@@ -94,6 +97,7 @@ export class SingleMacroEngine {
9497
this.choiceExecutor,
9598
this.variables,
9699
preloadedScripts,
100+
context?.label,
97101
);
98102

99103
if (memberAccess?.length) {

src/formatters/completeFormatter.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
generateFieldCacheKey,
2525
} from "../utils/FieldValueCollector";
2626
import { FieldValueProcessor } from "../utils/FieldValueProcessor";
27-
import { Formatter } from "./formatter";
27+
import { Formatter, type PromptContext } from "./formatter";
2828
import { MacroAbortError } from "../errors/MacroAbortError";
2929
import { isCancellationError } from "../utils/errorUtils";
3030

@@ -192,14 +192,14 @@ export class CompleteFormatter extends Formatter {
192192

193193
protected async promptForVariable(
194194
header?: string,
195-
context?: { type?: string; dateFormat?: string; defaultValue?: string },
195+
context?: PromptContext,
196196
): Promise<string> {
197197
try {
198198
// Use VDateInputPrompt for VDATE variables
199199
if (context?.type === "VDATE" && context.dateFormat) {
200200
return await VDateInputPrompt.Prompt(
201201
this.app,
202-
header as string,
202+
(header as string) ?? context.label ?? "Enter date",
203203
"Enter a date (e.g., 'tomorrow', 'next friday', '2025-12-25')",
204204
context.defaultValue,
205205
context.dateFormat,
@@ -208,11 +208,13 @@ export class CompleteFormatter extends Formatter {
208208

209209
// Use default prompt for other variables
210210
return await new InputPrompt().factory().Prompt(
211-
this.app,
212-
header as string,
213-
context?.defaultValue ? context.defaultValue : undefined,
214-
context?.defaultValue
215-
);
211+
this.app,
212+
header ?? context?.label ?? "Enter value",
213+
context?.placeholder ??
214+
(context?.defaultValue ? context.defaultValue : undefined),
215+
context?.defaultValue,
216+
context?.description,
217+
);
216218
} catch (error) {
217219
if (isCancellationError(error)) {
218220
throw new MacroAbortError("Input cancelled by user");
@@ -232,19 +234,29 @@ export class CompleteFormatter extends Formatter {
232234
}
233235
}
234236

235-
protected async suggestForValue(suggestedValues: string[], allowCustomInput = false) {
237+
protected async suggestForValue(
238+
suggestedValues: string[],
239+
allowCustomInput = false,
240+
context?: { placeholder?: string; variableKey?: string },
241+
) {
236242
try {
237243
if (allowCustomInput) {
238244
return await InputSuggester.Suggest(
239245
this.app,
240246
suggestedValues,
241247
suggestedValues,
248+
{
249+
...(context?.placeholder
250+
? { placeholder: context.placeholder }
251+
: {}),
252+
},
242253
);
243254
}
244255
return await GenericSuggester.Suggest(
245256
this.app,
246257
suggestedValues,
247258
suggestedValues,
259+
context?.placeholder,
248260
);
249261
} catch (error) {
250262
if (isCancellationError(error)) {
@@ -308,7 +320,10 @@ export class CompleteFormatter extends Formatter {
308320
return generateFieldCacheKey(filters);
309321
}
310322

311-
protected async getMacroValue(macroName: string): Promise<string> {
323+
protected async getMacroValue(
324+
macroName: string,
325+
context?: { label?: string },
326+
): Promise<string> {
312327
const macroEngine: SingleMacroEngine = new SingleMacroEngine(
313328
this.app,
314329
this.plugin,
@@ -317,7 +332,8 @@ export class CompleteFormatter extends Formatter {
317332
this.choiceExecutor,
318333
this.variables,
319334
);
320-
const macroOutput = (await macroEngine.runAndGetOutput(macroName)) ?? "";
335+
const macroOutput =
336+
(await macroEngine.runAndGetOutput(macroName, context)) ?? "";
321337

322338
// Copy variables from macro execution
323339
macroEngine.getVariables().forEach((value, key) => {

src/formatters/fileNameDisplayFormatter.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Formatter } from "./formatter";
1+
import { Formatter, type PromptContext } from "./formatter";
22
import type { App } from "obsidian";
33
import { DATE_VARIABLE_REGEX, GLOBAL_VAR_REGEX } from "../constants";
44
import type { IDateParser } from "../parsers/IDateParser";
@@ -12,6 +12,7 @@ import {
1212
getCurrentFileNamePreview,
1313
DateFormatPreviewGenerator
1414
} from "./helpers/previewHelpers";
15+
import { getValueVariableBaseName } from "../utils/valueSyntax";
1516

1617
import type QuickAdd from "../main";
1718

@@ -71,7 +72,10 @@ export class FileNameDisplayFormatter extends Formatter {
7172
}
7273

7374
protected getVariableValue(variableName: string): string {
74-
return getVariableExample(variableName);
75+
const stored = this.variables.get(variableName);
76+
if (typeof stored === "string") return stored;
77+
const baseName = getValueVariableBaseName(variableName);
78+
return getVariableExample(baseName);
7579
}
7680

7781
protected getCurrentFileLink(): string | null {
@@ -84,21 +88,28 @@ export class FileNameDisplayFormatter extends Formatter {
8488
return getCurrentFileNamePreview(this.app.workspace.getActiveFile());
8589
}
8690

87-
protected suggestForValue(suggestedValues: string[], allowCustomInput = false) {
91+
protected suggestForValue(
92+
suggestedValues: string[],
93+
allowCustomInput = false,
94+
_context?: { placeholder?: string; variableKey?: string },
95+
) {
8896
return getSuggestionPreview(suggestedValues);
8997
}
9098

9199
protected promptForMathValue(): Promise<string> {
92100
return Promise.resolve("calculation_result");
93101
}
94102

95-
protected getMacroValue(macroName: string) {
103+
protected getMacroValue(
104+
macroName: string,
105+
_context?: { label?: string },
106+
) {
96107
return getMacroPreview(macroName);
97108
}
98109

99110
protected async promptForVariable(
100111
variableName: string,
101-
context?: { type?: string; dateFormat?: string }
112+
context?: PromptContext
102113
): Promise<string> {
103114
return getVariablePromptExample(variableName);
104115
}

src/formatters/formatDisplayFormatter.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Formatter } from "./formatter";
1+
import { Formatter, type PromptContext } from "./formatter";
22
import type { App } from "obsidian";
33
import type QuickAdd from "../main";
44
import { SingleTemplateEngine } from "../engine/SingleTemplateEngine";
@@ -14,6 +14,7 @@ import {
1414
getCurrentFileNamePreview,
1515
DateFormatPreviewGenerator
1616
} from "./helpers/previewHelpers";
17+
import { getValueVariableBaseName } from "../utils/valueSyntax";
1718

1819
export class FormatDisplayFormatter extends Formatter {
1920
constructor(
@@ -74,7 +75,10 @@ export class FormatDisplayFormatter extends Formatter {
7475
}
7576

7677
protected getVariableValue(variableName: string): string {
77-
return getVariableExample(variableName);
78+
const stored = this.variables.get(variableName);
79+
if (typeof stored === "string") return stored;
80+
const baseName = getValueVariableBaseName(variableName);
81+
return getVariableExample(baseName);
7882
}
7983

8084
protected getCurrentFileLink(): string | null {
@@ -87,11 +91,18 @@ export class FormatDisplayFormatter extends Formatter {
8791
return getCurrentFileNamePreview(this.app.workspace.getActiveFile());
8892
}
8993

90-
protected suggestForValue(suggestedValues: string[], allowCustomInput = false) {
94+
protected suggestForValue(
95+
suggestedValues: string[],
96+
allowCustomInput = false,
97+
_context?: { placeholder?: string; variableKey?: string },
98+
) {
9199
return getSuggestionPreview(suggestedValues);
92100
}
93101

94-
protected getMacroValue(macroName: string) {
102+
protected getMacroValue(
103+
macroName: string,
104+
_context?: { label?: string },
105+
) {
95106
return getMacroPreview(macroName);
96107
}
97108

@@ -101,7 +112,7 @@ export class FormatDisplayFormatter extends Formatter {
101112

102113
protected promptForVariable(
103114
variableName: string,
104-
context?: { type?: string; dateFormat?: string; defaultValue?: string }
115+
context?: PromptContext
105116
): Promise<string> {
106117
return Promise.resolve(getVariablePromptExample(variableName));
107118
}

0 commit comments

Comments
 (0)