Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/docs/Choices/CaptureChoice.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ If you have a tag called `#people`, and you type `#people` in the _Capture To_ f
## Insert after

Insert After will allow you to insert the text after some line with the specified text.
If the matched line is followed by one or more blank lines (including whitespace-only
lines), QuickAdd inserts after those blank lines to preserve spacing under headings.

Example (Insert After `# H` with content `X`):

```markdown
# H

X
A
```

With Insert After, you can also enable `Insert at end of section` and `Consider subsections`.
You can see an explanation of these below.
Expand Down
129 changes: 129 additions & 0 deletions src/formatters/captureChoiceFormatter-frontmatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,132 @@ describe('CaptureChoiceFormatter frontmatter handling', () => {
expect(result).toBe(['---', 'tags: ["a"]', '---', 'Captured line', '# Template Body'].join('\n'));
});
});

describe('CaptureChoiceFormatter insert after blank lines', () => {
beforeEach(() => {
vi.resetAllMocks();
(global as any).navigator = {
clipboard: {
readText: vi.fn().mockResolvedValue(''),
},
};
});

const createFormatter = () => {
const app = createMockApp();
const plugin = {
settings: {
enableTemplatePropertyTypes: false,
globalVariables: {},
showCaptureNotification: false,
showInputCancellationNotification: true,
},
} as any;
const formatter = new CaptureChoiceFormatter(app, plugin);
const file = createTFile('Test.md');

return { formatter, file };
};

const createInsertAfterChoice = (after: string): ICaptureChoice =>
createChoice({
insertAfter: {
enabled: true,
after,
insertAtEnd: false,
considerSubsections: false,
createIfNotFound: false,
createIfNotFoundLocation: '',
},
});

it('skips one blank line after the match', async () => {
const { formatter, file } = createFormatter();
const choice = createInsertAfterChoice('# H');
const fileContent = ['# H', '', 'A'].join('\n');

const result = await formatter.formatContentWithFile(
'X\n',
choice,
fileContent,
file,
);

expect(result).toBe(['# H', '', 'X', 'A'].join('\n'));
});

it('skips multiple blank lines after the match', async () => {
const { formatter, file } = createFormatter();
const choice = createInsertAfterChoice('# H');
const fileContent = ['# H', '', '', 'A'].join('\n');

const result = await formatter.formatContentWithFile(
'X\n',
choice,
fileContent,
file,
);

expect(result).toBe(['# H', '', '', 'X', 'A'].join('\n'));
});

it('treats whitespace-only lines as blank', async () => {
const { formatter, file } = createFormatter();
const choice = createInsertAfterChoice('# H');
const fileContent = ['# H', ' \t', 'A'].join('\n');

const result = await formatter.formatContentWithFile(
'X\n',
choice,
fileContent,
file,
);

expect(result).toBe(['# H', ' \t', 'X', 'A'].join('\n'));
});

it('keeps behavior unchanged when no blank lines follow', async () => {
const { formatter, file } = createFormatter();
const choice = createInsertAfterChoice('# H');
const fileContent = ['# H', 'A'].join('\n');

const result = await formatter.formatContentWithFile(
'X\n',
choice,
fileContent,
file,
);

expect(result).toBe(['# H', 'X', 'A'].join('\n'));
});

it('keeps behavior unchanged when match is at EOF', async () => {
const { formatter, file } = createFormatter();
const choice = createInsertAfterChoice('# H');
const fileContent = '# H';

const result = await formatter.formatContentWithFile(
'X\n',
choice,
fileContent,
file,
);

expect(result).toBe('# H\nX\n');
});

it('handles CRLF content when skipping blank lines', async () => {
const { formatter, file } = createFormatter();
const choice = createInsertAfterChoice('# H');
const fileContent = '# H\r\n\r\nA';

const result = await formatter.formatContentWithFile(
'X\n',
choice,
fileContent,
file,
);

expect(result).toBe('# H\r\n\r\nX\nA');
});
});
31 changes: 31 additions & 0 deletions src/formatters/captureChoiceFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,31 @@ export class CaptureChoiceFormatter extends CompleteFormatter {
return partialIndex; // -1 if no match at all
}

private findInsertAfterPositionWithBlankLines(
lines: string[],
matchIndex: number,
body: string,
): number {
if (matchIndex < 0) return matchIndex;

// Ignore the trailing empty line that results from split("\n") when the
// file ends with a newline. This preserves existing EOF behavior.
const scanLimit = body.endsWith("\n")
? Math.max(lines.length - 1, 0)
: lines.length;
let position = matchIndex;

for (let i = matchIndex + 1; i < scanLimit; i++) {
if (lines[i].trim().length === 0) {
position = i;
continue;
}
break;
}

return position;
}

private async insertAfterHandler(formatted: string) {
// Use centralized location formatting for selector strings
const targetString: string = await this.formatLocationString(
Expand Down Expand Up @@ -231,6 +256,12 @@ export class CaptureChoiceFormatter extends CompleteFormatter {
);

targetPosition = endOfSectionIndex ?? fileContentLines.length - 1;
} else {
targetPosition = this.findInsertAfterPositionWithBlankLines(
fileContentLines,
targetPosition,
this.fileContent,
);
}

return this.insertTextAfterPositionInBody(
Expand Down
4 changes: 3 additions & 1 deletion src/gui/ChoiceBuilder/captureChoiceBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,9 @@ export class CaptureChoiceBuilder extends ChoiceBuilder {

private addInsertAfterFields() {
const descText =
"Insert capture after specified line. Accepts format syntax. Tip: use a heading (starts with #) to target a section.";
"Insert capture after specified line. Accepts format syntax. " +
"Tip: use a heading (starts with #) to target a section. " +
"If the matched line is followed by blank lines, QuickAdd inserts after them to preserve spacing.";

new Setting(this.contentEl)
.setName("Insert after")
Expand Down