Skip to content

Commit a53f889

Browse files
authored
feat(capture): fully support capture into canvas cards (#1124)
* feat(capture): add targeted canvas node capture * chore(capture): apply review polish * fix(capture): abort .canvas capture without node id * fix(capture): prevent concurrent canvas overwrite * fix(capture): allow vault picker and clear stale canvas node ids * codex: address PR review feedback (#1124) * codex: fix PR #1124 CI regressions * codex: avoid duplicate capture path formatting (#1124) * codex: fix capture success notice placement (#1124) * codex: guard canvas file-card cursor fallback (#1124) * codex: restrict picker to capturable canvas nodes (#1124) * codex: remove redundant canvas destination source-path reset (#1124) * codex: fix picker button disable typing (#1124) * codex: address PR review feedback (#1124) * codex: address PR review feedback (#1124) * codex: fix canvas capture correctness regressions (#1124) * codex: harden stale canvas node config fallback (#1124) * codex: add canvas file-card regressions and remove dead helpers (#1124) * fix(capture): abort invalid insert-after and align canvas link behavior * test(capture): assert choice abort notices are always visible
1 parent f2daeed commit a53f889

19 files changed

Lines changed: 2721 additions & 104 deletions

docs/docs/Choices/CaptureChoice.md

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ If you have a tag called `#people`, and you type `#people` in the _Capture To_ f
4848
- _Create file if it doesn't exist_ will do as the name implies - you can also create the file from a template, if you specify the template (the input box will appear below the setting).
4949
- _Task_ will format your captured text as a task.
5050
- _Use editor selection as default value_ controls whether the current editor selection is used as `{{VALUE}}`. Choose **Follow global setting**, **Use selection**, or **Ignore selection** (global default lives in Settings > Input). This does not affect `{{SELECTED}}`.
51-
- _Write to bottom of file_ will put whatever you enter at the bottom of the file.
51+
- _Write position_ controls where Capture writes: top, bottom, after line, and active-file cursor modes.
5252
- _Append link_ will append a link to the file you have open in the file you're capturing to. You can choose between three modes:
53-
- **Enabled (requires active file)** – keeps the legacy behavior and throws an error if no note is focused
53+
- **Enabled (requires active file)** – keeps the legacy behavior and throws an error if no note is focused (except Canvas-triggered capture, where link insertion is skipped)
5454
- **Enabled (skip if no active file)** – inserts the link when possible and silently drops `{{LINKCURRENT}}` if nothing is open
5555
- **Disabled** – never append a link
5656

@@ -60,6 +60,78 @@ If you have a tag called `#people`, and you type `#people` in the _Capture To_ f
6060
- **End of line** - Places the link at the end of the current line
6161
- **New line** - Places the link on a new line below the cursor
6262

63+
## Canvas Capture Notes
64+
65+
QuickAdd supports two Canvas capture workflows:
66+
67+
- Capture to one selected card in the active Canvas view
68+
- Capture to a specific card in a specific `.canvas` file
69+
70+
### 1) Capture to selected card in active Canvas
71+
72+
This mode is enabled when **Capture to active file** is on and the active leaf
73+
is a Canvas.
74+
75+
Supported card targets:
76+
77+
- Text cards
78+
- File cards that point to markdown files
79+
80+
### 2) Capture to specific card in specific `.canvas` file
81+
82+
This mode is enabled when **Capture to active file** is off, the capture path
83+
resolves to a `.canvas` file, and **Target canvas node** is set.
84+
85+
When the capture path is a `.canvas` file, QuickAdd shows a node picker that
86+
helps you choose a node id directly from that board.
87+
88+
### Write position support in Canvas
89+
90+
- Text cards support: **Top of file**, **Bottom of file**, **After line...**
91+
- File cards (markdown targets) support: **Top of file**, **Bottom of file**, **After line...**
92+
- Canvas does not support cursor-based modes: **At cursor**, **New line above cursor**, **New line below cursor**
93+
94+
If **Capture to active file** is enabled and you leave the default write
95+
position at **At cursor**, capture will abort in Canvas until you switch to a
96+
supported mode.
97+
98+
Canvas capture requires exactly one selected card in selected-card mode. If the
99+
selection is missing, multi-select, or unsupported, QuickAdd aborts with a
100+
notice instead of writing to the wrong place.
101+
102+
When append-link is set to **Enabled (requires active file)** and capture runs
103+
from a Canvas card without a focused Markdown editor, the capture still writes
104+
and link insertion is skipped.
105+
106+
A dedicated Canvas walkthrough page will return in a future update.
107+
108+
### Canvas Capture FAQ
109+
110+
**Why did my capture abort in Canvas?**
111+
112+
Most often one of these is true:
113+
114+
- No card is selected
115+
- More than one card is selected
116+
- The selected card type is unsupported
117+
- The selected write mode is cursor-based
118+
119+
**Can I target a specific card in a Canvas file?**
120+
121+
Yes. Set capture path to a `.canvas` file and choose a **Target canvas node**.
122+
123+
**Does "At cursor" work in Canvas cards?**
124+
125+
No. Use top, bottom, or insert-after placement.
126+
127+
**Can I capture to a file card that points to a Canvas file?**
128+
129+
No. File-card capture supports markdown targets only.
130+
131+
**Can I still create new Canvas files from templates?**
132+
133+
Yes. Template choices support `.canvas` templates.
134+
63135
## Insert after
64136

65137
Insert After will allow you to insert the text after some line with the specified text.

src/constants.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ export const DATE_VARIABLE_REGEX = new RegExp(
9696
);
9797
export const LINK_TO_CURRENT_FILE_REGEX = new RegExp(/{{LINKCURRENT}}/i);
9898
export const FILE_NAME_OF_CURRENT_FILE_REGEX = new RegExp(/{{FILENAMECURRENT}}/i);
99-
export const MARKDOWN_FILE_EXTENSION_REGEX = new RegExp(/\.md$/);
100-
export const CANVAS_FILE_EXTENSION_REGEX = new RegExp(/\.canvas$/);
101-
export const BASE_FILE_EXTENSION_REGEX = new RegExp(/\.base$/);
99+
export const MARKDOWN_FILE_EXTENSION_REGEX = new RegExp(/\.md$/i);
100+
export const CANVAS_FILE_EXTENSION_REGEX = new RegExp(/\.canvas$/i);
101+
export const BASE_FILE_EXTENSION_REGEX = new RegExp(/\.base$/i);
102102
export const JAVASCRIPT_FILE_EXTENSION_REGEX = new RegExp(/\.js$/);
103103
export const MACRO_REGEX = new RegExp(/{{MACRO:([^\n\r}]*)}}/i);
104104
export const TEMPLATE_REGEX = new RegExp(

src/engine/CaptureChoiceEngine.notice.test.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import { CaptureChoiceEngine } from "./CaptureChoiceEngine";
106106
import type { IChoiceExecutor } from "../IChoiceExecutor";
107107
import type ICaptureChoice from "../types/choices/ICaptureChoice";
108108
import { MacroAbortError } from "../errors/MacroAbortError";
109+
import { ChoiceAbortError } from "../errors/ChoiceAbortError";
109110
import { settingsStore } from "../settingsStore";
110111

111112
const defaultSettingsState = structuredClone(settingsStore.getState());
@@ -153,7 +154,7 @@ const createCaptureChoice = (): ICaptureChoice => ({
153154
},
154155
});
155156

156-
const createEngine = (abortMessage: string) => {
157+
const createEngine = (abortError: Error) => {
157158
const app = {
158159
vault: {
159160
adapter: {
@@ -185,7 +186,7 @@ const createEngine = (abortMessage: string) => {
185186
);
186187

187188
(engine as any).getFormattedPathToCaptureTo = vi.fn(async () => {
188-
throw new MacroAbortError(abortMessage);
189+
throw abortError;
189190
});
190191

191192
return engine;
@@ -202,7 +203,7 @@ describe("CaptureChoiceEngine cancellation notices", () => {
202203
...settingsStore.getState(),
203204
showInputCancellationNotification: true,
204205
});
205-
const engine = createEngine("Input cancelled by user");
206+
const engine = createEngine(new MacroAbortError("Input cancelled by user"));
206207

207208
await engine.run();
208209

@@ -218,7 +219,7 @@ describe("CaptureChoiceEngine cancellation notices", () => {
218219
showInputCancellationNotification: false,
219220
});
220221

221-
const engine = createEngine("Input cancelled by user");
222+
const engine = createEngine(new MacroAbortError("Input cancelled by user"));
222223

223224
await engine.run();
224225

@@ -231,7 +232,7 @@ describe("CaptureChoiceEngine cancellation notices", () => {
231232
showInputCancellationNotification: false,
232233
});
233234

234-
const engine = createEngine("Target file missing");
235+
const engine = createEngine(new MacroAbortError("Target file missing"));
235236

236237
await engine.run();
237238

@@ -241,6 +242,24 @@ describe("CaptureChoiceEngine cancellation notices", () => {
241242
);
242243
});
243244

245+
it("shows notices for choice abort errors even when input cancellation notifications are disabled", async () => {
246+
settingsStore.setState({
247+
...settingsStore.getState(),
248+
showInputCancellationNotification: false,
249+
});
250+
251+
const engine = createEngine(
252+
new ChoiceAbortError("Insert-after target not found: '# Missing'."),
253+
);
254+
255+
await engine.run();
256+
257+
expect(noticeClass.instances).toHaveLength(1);
258+
expect(noticeClass.instances[0]?.message).toContain(
259+
"Capture execution aborted: Insert-after target not found: '# Missing'.",
260+
);
261+
});
262+
244263
it("shows a notice when the target file is missing and create is disabled", async () => {
245264
settingsStore.setState({
246265
...settingsStore.getState(),

0 commit comments

Comments
 (0)