Skip to content

Commit 8597905

Browse files
authored
fix(field-suggestions): opt-in inline values from fenced code blocks (#1128)
* fix(field-suggestions): opt-in inline code-block values * codex: address PR review feedback (#1128) * codex: address PR review feedback (#1128)
1 parent b8ec56c commit 8597905

11 files changed

+319
-27
lines changed

docs/docs/FormatSyntax.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ title: Format syntax
2424
| `{{TEMPLATE:<TEMPLATEPATH>}}` | Include templates in your `format`. Supports Templater syntax. |
2525
| `{{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. |
2626
| `{{MVALUE}}` | Math modal for writing LaTeX. Use CTRL + Enter to submit. |
27-
| `{{FIELD:<FIELDNAME>}}` | Suggest the values of `FIELDNAME` anywhere `{{FIELD:FIELDNAME}}` is used. Fields are YAML fields, and the values represent any value this field has in your vault. If there exists no such field or value, you are instead prompted to enter one.<br/><br/>**Enhanced Filtering Options:**<br/>• `{{FIELD:fieldname\|folder:path/to/folder}}` - Only suggest values from files in specific folder<br/>• `{{FIELD:fieldname\|tag:tagname}}` - Only suggest values from files with specific tag<br/>• `{{FIELD:fieldname\|inline:true}}` - Include Dataview inline fields (fieldname:: value)<br/>• `{{FIELD:fieldname\|exclude-folder:templates}}` - Exclude values from files in specific folder<br/>• `{{FIELD:fieldname\|exclude-tag:deprecated}}` - Exclude values from files with specific tag<br/>• `{{FIELD:fieldname\|exclude-file:example.md}}` - Exclude values from specific file<br/>• `{{FIELD:fieldname\|default:Status - To Do}}` - Prepend a default suggestion; the modal placeholder shows it and pressing Enter accepts it.<br/>• `{{FIELD:fieldname\|default:Draft\|default-empty:true}}` - Only add the default when no matching values are found.<br/>• `{{FIELD:fieldname\|default:Draft\|default-always:true}}` - Keep the default first even if other suggestions exist.<br/>• Combine filters: `{{FIELD:fieldname\|folder:daily\|tag:work\|exclude-folder:templates\|inline:true}}`<br/>• Multiple exclusions: `{{FIELD:fieldname\|exclude-folder:templates\|exclude-folder:archive}}`<br/><br/>This is currently in beta, and the syntax can change—leave your thoughts [here](https://github.com/chhoumann/quickadd/issues/337). |
27+
| `{{FIELD:<FIELDNAME>}}` | Suggest the values of `FIELDNAME` anywhere `{{FIELD:FIELDNAME}}` is used. Fields are YAML fields, and the values represent any value this field has in your vault. If there exists no such field or value, you are instead prompted to enter one.<br/><br/>**Enhanced Filtering Options:**<br/>• `{{FIELD:fieldname\|folder:path/to/folder}}` - Only suggest values from files in specific folder<br/>• `{{FIELD:fieldname\|tag:tagname}}` - Only suggest values from files with specific tag<br/>• `{{FIELD:fieldname\|inline:true}}` - Include Dataview inline fields (fieldname:: value)<br/>• `{{FIELD:fieldname\|inline:true\|inline-code-blocks:ad-note}}` - Include inline fields inside specific fenced code blocks (opt-in)<br/>• `{{FIELD:fieldname\|exclude-folder:templates}}` - Exclude values from files in specific folder<br/>• `{{FIELD:fieldname\|exclude-tag:deprecated}}` - Exclude values from files with specific tag<br/>• `{{FIELD:fieldname\|exclude-file:example.md}}` - Exclude values from specific file<br/>• `{{FIELD:fieldname\|default:Status - To Do}}` - Prepend a default suggestion; the modal placeholder shows it and pressing Enter accepts it.<br/>• `{{FIELD:fieldname\|default:Draft\|default-empty:true}}` - Only add the default when no matching values are found.<br/>• `{{FIELD:fieldname\|default:Draft\|default-always:true}}` - Keep the default first even if other suggestions exist.<br/>• Combine filters: `{{FIELD:fieldname\|folder:daily\|tag:work\|exclude-folder:templates\|inline:true\|inline-code-blocks:ad-note}}`<br/>• Multiple exclusions: `{{FIELD:fieldname\|exclude-folder:templates\|exclude-folder:archive}}`<br/><br/>This is currently in beta, and the syntax can change—leave your thoughts [here](https://github.com/chhoumann/quickadd/issues/337). |
2828
| `{{selected}}` | The selected text in the current editor. Will be empty if no active editor exists. |
2929
| `{{CLIPBOARD}}` | The current clipboard content. Will be empty if clipboard access fails due to permissions or security restrictions. |
3030
| `{{RANDOM:<length>}}` | Generates a random alphanumeric string of the specified length (1-100). Useful for creating unique identifiers, block references, or temporary codes. Example: `{{RANDOM:6}}` generates something like `3YusT5`. |

docs/docs/QuickAddAPI.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,7 @@ Retrieves all unique values for a specific field across your vault.
732732
- `folder`: Only search in specific folder (e.g., "daily/notes")
733733
- `tags`: Only search in files with specific tags (array)
734734
- `includeInline`: Include Dataview inline fields (default: false)
735+
- `includeInlineCodeBlocks`: Include inline fields inside specific fenced code block types when `includeInline` is true (e.g., `["ad-note"]`)
735736
736737
**Returns:** Promise resolving to sorted array of unique field values
737738
@@ -774,6 +775,18 @@ const clients = await quickAddApi.fieldSuggestions.getFieldValues(
774775
);
775776
```
776777
778+
Include inline fields in specific code block types:
779+
```javascript
780+
const ids = await quickAddApi.fieldSuggestions.getFieldValues(
781+
"Id",
782+
{
783+
folder: "work/projects",
784+
includeInline: true,
785+
includeInlineCodeBlocks: ["ad-note"]
786+
}
787+
);
788+
```
789+
777790
### `clearCache(fieldName?: string): void`
778791
Clears the field suggestions cache for better performance.
779792

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const FORMAT_SYNTAX: string[] = [
4242
"{{field:<fieldname>|folder:<path>}}",
4343
"{{field:<fieldname>|tag:<tagname>}}",
4444
"{{field:<fieldname>|inline:true}}",
45+
"{{field:<fieldname>|inline:true|inline-code-blocks:ad-note}}",
4546
LINKCURRENT_SYNTAX,
4647
FILENAMECURRENT_SYNTAX,
4748
"{{macro:<macroname>}}",
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import type { App, TFile } from "obsidian";
3+
import type { IChoiceExecutor } from "./IChoiceExecutor";
4+
import type QuickAdd from "./main";
5+
import { QuickAddApi } from "./quickAddApi";
6+
7+
vi.mock("./quickAddSettingsTab", () => ({
8+
DEFAULT_SETTINGS: {},
9+
QuickAddSettingsTab: class {},
10+
}));
11+
12+
vi.mock("./formatters/completeFormatter", () => ({
13+
CompleteFormatter: class CompleteFormatterMock {},
14+
}));
15+
16+
vi.mock("obsidian-dataview", () => ({
17+
getAPI: vi.fn(),
18+
}));
19+
20+
const INLINE_CONTENT = `
21+
Id:: 343434
22+
23+
\`\`\`ad-note
24+
Id:: 121212
25+
\`\`\`
26+
27+
\`\`\`js
28+
Id:: 999999
29+
\`\`\`
30+
`;
31+
32+
function createApp(content: string): App {
33+
const file = { path: "QuickAdd-Issue-998/repro.md" } as TFile;
34+
return {
35+
vault: {
36+
getMarkdownFiles: () => [file],
37+
read: vi.fn(async () => content),
38+
},
39+
metadataCache: {
40+
getFileCache: vi.fn(() => ({ frontmatter: {} })),
41+
},
42+
} as unknown as App;
43+
}
44+
45+
describe("QuickAddApi.fieldSuggestions.getFieldValues", () => {
46+
let variables: Map<string, unknown>;
47+
let choiceExecutor: IChoiceExecutor;
48+
let plugin: QuickAdd;
49+
50+
beforeEach(() => {
51+
variables = new Map<string, unknown>();
52+
choiceExecutor = {
53+
execute: vi.fn(),
54+
variables,
55+
} as unknown as IChoiceExecutor;
56+
plugin = {} as QuickAdd;
57+
});
58+
59+
it("keeps code-block values excluded by default when includeInline is true", async () => {
60+
const app = createApp(INLINE_CONTENT);
61+
const api = QuickAddApi.GetApi(app, plugin, choiceExecutor);
62+
63+
const result = await api.fieldSuggestions.getFieldValues("Id", {
64+
includeInline: true,
65+
});
66+
67+
expect(result).toEqual(["343434"]);
68+
});
69+
70+
it("includes allowlisted code-block values when includeInlineCodeBlocks is provided", async () => {
71+
const app = createApp(INLINE_CONTENT);
72+
const api = QuickAddApi.GetApi(app, plugin, choiceExecutor);
73+
74+
const result = await api.fieldSuggestions.getFieldValues("Id", {
75+
includeInline: true,
76+
includeInlineCodeBlocks: ["ad-note"],
77+
});
78+
79+
expect(result).toEqual(["121212", "343434"]);
80+
});
81+
82+
it("does not scan inline values when includeInline is false", async () => {
83+
const app = createApp(INLINE_CONTENT);
84+
const api = QuickAddApi.GetApi(app, plugin, choiceExecutor);
85+
86+
const result = await api.fieldSuggestions.getFieldValues("Id", {
87+
includeInline: false,
88+
includeInlineCodeBlocks: ["ad-note"],
89+
});
90+
91+
expect(result).toEqual([]);
92+
});
93+
});

src/quickAddApi.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,12 +536,17 @@ export class QuickAddApi {
536536
folder?: string;
537537
tags?: string[];
538538
includeInline?: boolean;
539+
includeInlineCodeBlocks?: string[];
539540
},
540541
) => {
542+
const inlineCodeBlocks = options?.includeInlineCodeBlocks
543+
?.map((value) => value.trim().toLowerCase())
544+
.filter((value) => value.length > 0);
541545
const filters = {
542546
folder: options?.folder,
543547
tags: options?.tags,
544548
inline: options?.includeInline ?? false,
549+
inlineCodeBlocks,
545550
};
546551

547552
// Get all markdown files and apply filters
@@ -578,6 +583,9 @@ export class QuickAddApi {
578583
const inlineValues = InlineFieldParser.getFieldValues(
579584
content,
580585
fieldName,
586+
{
587+
includeCodeBlocks: inlineCodeBlocks,
588+
},
581589
);
582590
inlineValues.forEach((v) => values.add(v));
583591
}

src/utils/FieldSuggestionParser.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ describe("FieldSuggestionParser", () => {
4949
});
5050
});
5151

52+
it("should parse inline code block allowlist filter", () => {
53+
const result = FieldSuggestionParser.parse(
54+
"fieldname|inline:true|inline-code-blocks:ad-note, dataview",
55+
);
56+
expect(result).toEqual({
57+
fieldName: "fieldname",
58+
filters: {
59+
inline: true,
60+
inlineCodeBlocks: ["ad-note", "dataview"],
61+
},
62+
});
63+
});
64+
5265
it("should parse field name with multiple filters", () => {
5366
const result = FieldSuggestionParser.parse(
5467
"fieldname|folder:daily|tag:work|inline:true",
@@ -182,4 +195,4 @@ describe("FieldSuggestionParser", () => {
182195
});
183196
});
184197
});
185-
});
198+
});

src/utils/FieldSuggestionParser.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface FieldFilter {
44
folder?: string;
55
tags?: string[];
66
inline?: boolean;
7+
inlineCodeBlocks?: string[];
78
defaultValue?: string;
89
defaultEmpty?: boolean;
910
defaultAlways?: boolean;
@@ -54,6 +55,17 @@ export class FieldSuggestionParser {
5455
case "inline":
5556
filters.inline = filterValue.toLowerCase() === "true";
5657
break;
58+
case "inline-code-blocks":
59+
if (!filters.inlineCodeBlocks) {
60+
filters.inlineCodeBlocks = [];
61+
}
62+
filters.inlineCodeBlocks.push(
63+
...filterValue
64+
.split(",")
65+
.map((value) => value.trim().toLowerCase())
66+
.filter((value) => value.length > 0),
67+
);
68+
break;
5769
case "default":
5870
filters.defaultValue = filterValue;
5971
break;

src/utils/FieldValueCollector.issue671.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,34 @@ describe("Issue #671 - {{FIELD:tags}} suggestions", () => {
2929
expect.arrayContaining(["ai/technology", "cook/hoven"]),
3030
);
3131
});
32+
33+
it("includes inline fields inside allowlisted code blocks only when configured", async () => {
34+
const app = new App();
35+
const file = { path: "folder/note.md" } as any;
36+
37+
app.vault.getMarkdownFiles = () => [file];
38+
app.metadataCache.getFileCache = () => ({ frontmatter: {} } as any);
39+
app.vault.read = vi.fn(async () => `
40+
Id:: 343434
41+
\`\`\`ad-note
42+
Id:: 121212
43+
\`\`\`
44+
\`\`\`js
45+
Id:: 999999
46+
\`\`\`
47+
`);
48+
49+
const withoutCodeBlockAllowlist = await collectFieldValuesProcessed(
50+
app,
51+
"Id",
52+
{ inline: true },
53+
);
54+
const withCodeBlockAllowlist = await collectFieldValuesProcessed(app, "Id", {
55+
inline: true,
56+
inlineCodeBlocks: ["ad-note"],
57+
});
58+
59+
expect(withoutCodeBlockAllowlist).toEqual(["343434"]);
60+
expect(withCodeBlockAllowlist).toEqual(["121212", "343434"]);
61+
});
3262
});

src/utils/FieldValueCollector.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export function generateFieldCacheKey(filters: FieldFilter): string {
1111
if (filters.folder) parts.push(`folder:${filters.folder}`);
1212
if (filters.tags) parts.push(`tags:${filters.tags.join(",")}`);
1313
if (filters.inline) parts.push("inline:true");
14+
if (filters.inlineCodeBlocks?.length) {
15+
parts.push(`inline-code-blocks:${filters.inlineCodeBlocks.join(",")}`);
16+
}
1417
if (filters.caseSensitive) parts.push("case-sensitive:true");
1518
if (filters.excludeFolders)
1619
parts.push(`exclude-folders:${filters.excludeFolders.join(",")}`);
@@ -243,6 +246,9 @@ async function collectFieldValuesManually(
243246
const inlineValues = InlineFieldParser.getFieldValues(
244247
content,
245248
fieldName,
249+
{
250+
includeCodeBlocks: filters.inlineCodeBlocks,
251+
},
246252
);
247253
inlineValues.forEach((s) => {
248254
const t = String(s).trim();

0 commit comments

Comments
 (0)