Skip to content

Commit 5361e6c

Browse files
authored
feat: add write to top of file switch for capture to active file (#986)
Adds a new option to insert capture content at the top of the active file (after frontmatter) instead of at the cursor position. This addresses the feature request in issue #248. Changes: - Add new 'activeFileTop' capture action type - Add 'activeFileWritePosition' field to capture choice types - Update UI to show separate 'At cursor' vs 'Top of file' options - Update engine and formatter to handle the new action - Add tests for the new functionality - Ensure backward compatibility for existing choices Closes #248
1 parent d63f8c9 commit 5361e6c

File tree

7 files changed

+90
-25
lines changed

7 files changed

+90
-25
lines changed

src/engine/CaptureChoiceEngine.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
7373
msg = `Captured to current line in ${fileName}`;
7474
break;
7575
case "prepend":
76+
case "activeFileTop":
7677
msg = `Captured to top of ${fileName}`;
7778
break;
7879
case "append":
@@ -127,10 +128,14 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
127128
const { file, newFileContent, captureContent } =
128129
await getFileAndAddContentFn(filePath, content);
129130

130-
const action = getCaptureAction(this.choice);
131+
const action = getCaptureAction(this.choice);
132+
const isEditorInsertionAction =
133+
action === "currentLine" ||
134+
action === "newLineAbove" ||
135+
action === "newLineBelow";
131136

132-
// Handle capture to active file with special actions
133-
if (action === "currentLine" || action === "newLineAbove" || action === "newLineBelow") {
137+
// Handle capture to active file with special actions
138+
if (isEditorInsertionAction) {
134139
// Parse Templater syntax in the capture content.
135140
// If Templater isn't installed, it just returns the capture content.
136141
const content = await templaterParseTemplate(
@@ -158,7 +163,6 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
158163

159164
// Show success notification
160165
if (this.plugin.settings.showCaptureNotification) {
161-
const action = getCaptureAction(this.choice);
162166
this.showSuccessNotice(file, {
163167
wasNewFile: !fileAlreadyExists,
164168
action,
@@ -203,15 +207,15 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
203207
}
204208

205209
/**
206-
* Gets a formatted file path to capture content to, either the active file or a specified location.
207-
* If capturing to a folder, suggests a file within the folder to capture the content to.
208-
*
209-
* @param {boolean} shouldCaptureToActiveFile - Determines if the content should be captured to the active file.
210-
* @returns {Promise<string>} A promise that resolves to the formatted file path where the content should be captured.
211-
*
212-
* @throws {Error} Throws an error if there's no active file when trying to capture to active file,
213-
* if the capture path is invalid, or if the target folder is empty.
214-
*/
210+
* Gets a formatted file path to capture content to, either the active file or a specified location.
211+
* If capturing to a folder, suggests a file within the folder to capture the content to.
212+
*
213+
* @param {boolean} shouldCaptureToActiveFile - Determines if the content should be captured to the active file.
214+
* @returns {Promise<string>} A promise that resolves to the formatted file path where the content should be captured.
215+
*
216+
* @throws {Error} Throws an error if there's no active file when trying to capture to active file,
217+
* if the capture path is invalid, or if the target folder is empty.
218+
*/
215219
private async getFormattedPathToCaptureTo(
216220
shouldCaptureToActiveFile: boolean,
217221
): Promise<string> {

src/engine/captureAction.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe("getCaptureAction", () => {
1010
command: false,
1111
captureTo: "",
1212
captureToActiveFile: false,
13+
activeFileWritePosition: "cursor",
1314
createFileIfItDoesntExist: { enabled: false, createWithTemplate: false, template: "" },
1415
format: { enabled: false, format: "" },
1516
prepend: false,
@@ -78,4 +79,30 @@ describe("getCaptureAction", () => {
7879
});
7980
expect(getCaptureAction(choice)).toBe("newLineBelow");
8081
});
82+
83+
it("returns 'activeFileTop' when capturing to active file with write position set to top", () => {
84+
const choice = createChoice({
85+
captureToActiveFile: true,
86+
activeFileWritePosition: "top",
87+
});
88+
expect(getCaptureAction(choice)).toBe("activeFileTop");
89+
});
90+
91+
it("returns 'currentLine' when capturing to active file with default cursor position", () => {
92+
const choice = createChoice({
93+
captureToActiveFile: true,
94+
activeFileWritePosition: "cursor",
95+
});
96+
expect(getCaptureAction(choice)).toBe("currentLine");
97+
});
98+
99+
it("prioritizes 'activeFileTop' over the default append action when eligible", () => {
100+
const choice = createChoice({
101+
captureToActiveFile: true,
102+
activeFileWritePosition: "top",
103+
prepend: false,
104+
insertAfter: { enabled: false, after: "", insertAtEnd: false, considerSubsections: false, createIfNotFound: false, createIfNotFoundLocation: "" },
105+
});
106+
expect(getCaptureAction(choice)).toBe("activeFileTop");
107+
});
81108
});

src/engine/captureAction.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type ICaptureChoice from "../types/choices/ICaptureChoice";
22

3-
export type CaptureAction = "append" | "prepend" | "insertAfter" | "currentLine" | "newLineAbove" | "newLineBelow";
3+
export type CaptureAction = "append" | "prepend" | "insertAfter" | "currentLine" | "newLineAbove" | "newLineBelow" | "activeFileTop";
44

55
/**
66
* Gets the capture action based on choice configuration.
@@ -12,6 +12,10 @@ export function getCaptureAction(choice: ICaptureChoice): CaptureAction {
1212
}
1313

1414
if (choice.captureToActiveFile && !choice.prepend && !choice.insertAfter.enabled) {
15+
if (choice.activeFileWritePosition === "top") {
16+
return "activeFileTop";
17+
}
18+
1519
return "currentLine";
1620
}
1721

src/formatters/captureChoiceFormatter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ export class CaptureChoiceFormatter extends CompleteFormatter {
6868
const shouldRunTemplater =
6969
choice.insertAfter.enabled ||
7070
choice.prepend ||
71-
!choice.captureToActiveFile;
71+
!choice.captureToActiveFile ||
72+
choice.activeFileWritePosition === "top";
7273
const formatted = await this.formatFileContent(input, shouldRunTemplater);
7374
return formatted;
7475
}

src/gui/ChoiceBuilder/captureChoiceBuilder.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,11 @@ export class CaptureChoiceBuilder extends ChoiceBuilder {
290290
private addWritePositionSetting() {
291291
const positionSetting: Setting = new Setting(this.contentEl);
292292
const isActiveFile = !!this.choice?.captureToActiveFile;
293+
294+
if (!this.choice.activeFileWritePosition) {
295+
this.choice.activeFileWritePosition = "cursor";
296+
}
297+
293298
positionSetting
294299
.setName("Write position")
295300
.setDesc(
@@ -298,7 +303,7 @@ export class CaptureChoiceBuilder extends ChoiceBuilder {
298303
: "Where to place the capture in the target file.",
299304
)
300305
.addDropdown((dropdown) => {
301-
const current: "top" | "after" | "bottom" | "newLineAbove" | "newLineBelow" =
306+
const current =
302307
this.choice.insertAfter?.enabled
303308
? "after"
304309
: this.choice.prepend
@@ -307,12 +312,14 @@ export class CaptureChoiceBuilder extends ChoiceBuilder {
307312
? this.choice.newLineCapture.direction === "above"
308313
? "newLineAbove"
309314
: "newLineBelow"
310-
: "top";
315+
: isActiveFile && this.choice.activeFileWritePosition === "top"
316+
? "activeTop"
317+
: "top";
311318

312319
dropdown.addOption("top", isActiveFile ? "At cursor" : "Top of file");
313320

314-
// Add new line options only when capturing to active file
315321
if (isActiveFile) {
322+
dropdown.addOption("activeTop", "Top of file (after frontmatter)");
316323
dropdown.addOption("newLineAbove", "New line above cursor");
317324
dropdown.addOption("newLineBelow", "New line below cursor");
318325
}
@@ -321,21 +328,35 @@ export class CaptureChoiceBuilder extends ChoiceBuilder {
321328
dropdown.addOption("bottom", "Bottom of file");
322329
dropdown.setValue(current);
323330
dropdown.onChange((value: string) => {
324-
const v = value as "top" | "after" | "bottom" | "newLineAbove" | "newLineBelow";
331+
const v = value as
332+
| "top"
333+
| "after"
334+
| "bottom"
335+
| "newLineAbove"
336+
| "newLineBelow"
337+
| "activeTop";
325338

326-
// Reset all options first
327339
this.choice.prepend = false;
328340
this.choice.insertAfter.enabled = false;
329341
if (!this.choice.newLineCapture) {
330342
this.choice.newLineCapture = { enabled: false, direction: "below" };
331343
}
332344
this.choice.newLineCapture.enabled = false;
345+
this.choice.activeFileWritePosition = "cursor";
333346

334347
if (v === "top") {
335348
this.reload();
336349
return;
337350
}
338351

352+
if (v === "activeTop") {
353+
if (isActiveFile) {
354+
this.choice.activeFileWritePosition = "top";
355+
}
356+
this.reload();
357+
return;
358+
}
359+
339360
if (v === "bottom") {
340361
this.choice.prepend = true;
341362
this.reload();

src/types/choices/CaptureChoice.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export class CaptureChoice extends Choice implements ICaptureChoice {
77
appendLink: boolean | AppendLinkOptions;
88
captureTo: string;
99
captureToActiveFile: boolean;
10+
activeFileWritePosition: "cursor" | "top";
1011
createFileIfItDoesntExist: {
1112
enabled: boolean;
1213
createWithTemplate: boolean;
@@ -41,6 +42,7 @@ export class CaptureChoice extends Choice implements ICaptureChoice {
4142
this.appendLink = false;
4243
this.captureTo = "";
4344
this.captureToActiveFile = false;
45+
this.activeFileWritePosition = "cursor";
4446
this.createFileIfItDoesntExist = {
4547
enabled: false,
4648
createWithTemplate: false,
@@ -71,6 +73,11 @@ export class CaptureChoice extends Choice implements ICaptureChoice {
7173
}
7274

7375
public static Load(choice: ICaptureChoice): CaptureChoice {
74-
return choice as CaptureChoice;
76+
const loaded = choice as CaptureChoice;
77+
// Ensure backward compatibility: default to "cursor" if not set
78+
if (!loaded.activeFileWritePosition) {
79+
loaded.activeFileWritePosition = "cursor";
80+
}
81+
return loaded;
7582
}
7683
}

src/types/choices/ICaptureChoice.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { OpenLocation, FileViewMode2 } from "../fileOpening";
55
export default interface ICaptureChoice extends IChoice {
66
captureTo: string;
77
captureToActiveFile: boolean;
8+
activeFileWritePosition?: "cursor" | "top";
89
createFileIfItDoesntExist: {
910
enabled: boolean;
1011
createWithTemplate: boolean;
@@ -14,10 +15,10 @@ export default interface ICaptureChoice extends IChoice {
1415
/** Capture to bottom of file (after current file content). */
1516
prepend: boolean;
1617
/**
17-
* Configure link appending behavior.
18-
* - boolean: Legacy format for backward compatibility (true = enabled with default placement)
19-
* - AppendLinkOptions: New format with configurable placement options
20-
*/
18+
* Configure link appending behavior.
19+
* - boolean: Legacy format for backward compatibility (true = enabled with default placement)
20+
* - AppendLinkOptions: New format with configurable placement options
21+
*/
2122
appendLink: boolean | AppendLinkOptions;
2223
task: boolean;
2324
insertAfter: {

0 commit comments

Comments
 (0)