Thanks to visit codestin.com
Credit goes to github.com

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
161 changes: 134 additions & 27 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3074,11 +3074,94 @@ function unwrapCommitMessageBody(body: string): string {
// Pattern to detect fenced code block markers
const FENCE_PATTERN = /^[ \t]*```/;

const getLeadingWhitespaceLength = (text: string): number => text.match(/^[ \t]*/)?.[0].length ?? 0;
const hasHardLineBreak = (text: string): boolean => / {2}$/.test(text);
const appendWithSpace = (base: string, addition: string): string => {
if (!addition) {
return base;
}
return base.length > 0 && !/\s$/.test(base) ? `${base} ${addition}` : `${base}${addition}`;
};

const lines = body.split('\n');
const result: string[] = [];
let i = 0;
let inFencedBlock = false;
let inListContext = false;
const listIndentStack: number[] = [];

const getNextNonBlankLineInfo = (
startIndex: number,
): { line: string; indent: number; isListItem: boolean } | undefined => {
for (let idx = startIndex; idx < lines.length; idx++) {
const candidate = lines[idx];
if (candidate.trim() === '') {
continue;
}
return {
line: candidate,
indent: getLeadingWhitespaceLength(candidate),
isListItem: LIST_ITEM_PATTERN.test(candidate),
};
}
return undefined;
};

const getActiveListIndent = (lineIndent: number): number | undefined => {
for (let idx = listIndentStack.length - 1; idx >= 0; idx--) {
const indentForLevel = listIndentStack[idx];
if (lineIndent >= indentForLevel + 2) {
listIndentStack.length = idx + 1;
return indentForLevel;
}
listIndentStack.pop();
}
return undefined;
};

const shouldJoinListContinuation = (lineIndex: number, activeIndent: number, baseLine: string): boolean => {
const currentLine = lines[lineIndex];
if (!currentLine) {
return false;
}

const trimmed = currentLine.trim();
if (!trimmed) {
return false;
}

if (hasHardLineBreak(baseLine) || hasHardLineBreak(currentLine)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's a little inconsistent that some regex-es are tested as "variable.test(...)" and some are wrapped into local functions.

return false;
}

if (LIST_ITEM_PATTERN.test(currentLine)) {
return false;
}

if (BLOCKQUOTE_PATTERN.test(currentLine) || FENCE_PATTERN.test(currentLine)) {
return false;
}

const currentIndent = getLeadingWhitespaceLength(currentLine);
if (currentIndent < activeIndent + 2) {
return false;
}

// Treat indented code blocks (4+ spaces beyond the bullet) as preserve-only.
if (currentIndent >= activeIndent + 4) {
return false;
}

const nextInfo = getNextNonBlankLineInfo(lineIndex + 1);
if (!nextInfo) {
return true;
}

if (nextInfo.isListItem && nextInfo.indent <= activeIndent) {
return false;
}

return true;
};

while (i < lines.length) {
const line = lines[i];
Expand All @@ -3087,7 +3170,7 @@ function unwrapCommitMessageBody(body: string): string {
if (line.trim() === '') {
result.push(line);
i++;
inListContext = false; // Reset list context on blank line
listIndentStack.length = 0;
continue;
}

Expand All @@ -3106,33 +3189,62 @@ function unwrapCommitMessageBody(body: string): string {
continue;
}

// Check if this line is a list item
const lineIndent = getLeadingWhitespaceLength(line);
const isListItem = LIST_ITEM_PATTERN.test(line);

// Check if this line is a blockquote
const isBlockquote = BLOCKQUOTE_PATTERN.test(line);
if (isListItem) {
while (listIndentStack.length && lineIndent < listIndentStack[listIndentStack.length - 1]) {
listIndentStack.pop();
}

// Check if this line is indented (4+ spaces) but NOT a list continuation
// List continuations have leading spaces but we're in list context
const leadingSpaces = line.match(/^[ \t]*/)?.[0].length || 0;
const isIndentedCode = leadingSpaces >= 4 && !inListContext;
if (!listIndentStack.length || lineIndent > listIndentStack[listIndentStack.length - 1]) {
listIndentStack.push(lineIndent);
} else {
listIndentStack[listIndentStack.length - 1] = lineIndent;
}

// Determine if this line should be preserved (not joined)
const shouldPreserveLine = isListItem || isBlockquote || isIndentedCode;
result.push(line);
i++;
continue;
}

const activeListIndent = getActiveListIndent(lineIndent);
const codeIndentThreshold = activeListIndent !== undefined ? activeListIndent + 4 : 4;
const isBlockquote = BLOCKQUOTE_PATTERN.test(line);
const isIndentedCode = lineIndent >= codeIndentThreshold;

if (shouldPreserveLine) {
if (isBlockquote || isIndentedCode) {
result.push(line);
i++;
// If this is a list item, we're now in list context
if (isListItem) {
inListContext = true;
}
continue;
}

// If we have leading spaces but we're in a list context, this is a list continuation
// We should preserve it to maintain list formatting
if (inListContext && leadingSpaces >= 2) {
if (activeListIndent !== undefined && lineIndent >= activeListIndent + 2) {
const baseIndex = result.length - 1;
if (baseIndex >= 0) {
let baseLine = result[baseIndex];
let appended = false;
let currentIndex = i;

while (
currentIndex < lines.length &&
shouldJoinListContinuation(currentIndex, activeListIndent, baseLine)
) {
const continuationText = lines[currentIndex].trim();
if (continuationText) {
baseLine = appendWithSpace(baseLine, continuationText);
appended = true;
}
currentIndex++;
}

if (appended) {
result[baseIndex] = baseLine;
i = currentIndex;
continue;
}
}

result.push(line);
i++;
continue;
Expand Down Expand Up @@ -3166,19 +3278,14 @@ function unwrapCommitMessageBody(body: string): string {
break;
}

// Check if next line is indented code (4+ spaces, not in list context)
const nextLeadingSpaces = nextLine.match(/^[ \t]*/)?.[0].length || 0;
const nextIsIndentedCode = nextLeadingSpaces >= 4 && !inListContext;
// Check if next line is indented code (4+ spaces, when not in a list context)
const nextLeadingSpaces = getLeadingWhitespaceLength(nextLine);
const nextIsIndentedCode = nextLeadingSpaces >= 4;

if (nextIsIndentedCode) {
break;
}

// If in list context and next line is indented, it's a list continuation
if (inListContext && nextLeadingSpaces >= 2) {
break;
}

// Join this line with a space
joinedLine += ' ' + nextLine;
i++;
Expand Down
8 changes: 8 additions & 0 deletions src/test/github/folderRepositoryManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,12 @@ describe('titleAndBodyFrom', function () {
assert.strictEqual(result?.title, 'title');
assert.strictEqual(result?.body, 'Wrapped paragraph across lines.\n\n- Item 1\n - Nested item\n More nested content\n- Item 2\n\nAnother wrapped paragraph here.');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: I think the nested lines should be merged.

Suggested change
assert.strictEqual(result?.body, 'Wrapped paragraph across lines.\n\n- Item 1\n - Nested item\n More nested content\n- Item 2\n\nAnother wrapped paragraph here.');
assert.strictEqual(result?.body, 'Wrapped paragraph across lines.\n\n- Item 1\n - Nested item More nested content\n- Item 2\n\nAnother wrapped paragraph here.');

});

it('handles nested lists', async function () {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexr00 Sorry, I wasn't quick enough to review this PR before merging, but I'd suggest adding a few more test cases, including some using numbered list items which are harder to implement than unordered lists because the numeric part can be variable-length. Also, the number of spaces is actually quite lenient in GitHub. Finally, code blocks and additional paragraphs are also supported in GH lists, so should probably have test cases for those.

I didn't review this PR code closely, but I did see a few 2's in the PR code, so wanted to make sure that you weren't assuming that the list indentation was always 2 spaces per level and/or that the list marker was always one character.

Here's some test cases, adapted from https://chatgpt.com/share/695c1f08-b928-8004-88c1-79417405b729. All the cases above should have their lines joined where there's a single \n. I apologize if some of these duplicate codepaths that were tested in other tests!

I suspect it'd be good to also make nested versions of these tests... I'll leave that as an exercise for copilot! :-)

  1. Basic numeric list
    continuation.
    Third line

  2. Additional spaces are
    OK for a continuation (unless it's 4 spaces which would be a code block).
    Third line

  • Additional spaces are
    OK for a continuation (unless it's 4 spaces which would be a code block).
    Third line
  1. Multi-digit numbers should also
    work for a continuation.
    Third line

  2. Multi-paragraph lists are also supported.

    Second paragraph in the same list item.
    Third line

  • Multi-paragraph lists are also supported.

    Second paragraph in the same list item.
    Third line

  1. Item with code:

    code line
    code line
    
  • Item with code:

    code line
    code line
    
  1. Fewer spaces are also OK
    for a list continuation (as long as there's at least one space)
  • Fewer spaces are also OK
    for a list continuation (as long as there's at least one space)
  1. Fewer spaces are also OK
    for a list continuation (as long as there's at least one space)
  • Fewer spaces are also OK
    for a list continuation (as long as there's at least one space)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the test cases!

const message = Promise.resolve('title\n\n* This is a list item with two lines\n that have a line break between them\n * This is a nested list item that also has\n two lines that should have been merged');

const result = await titleAndBodyFrom(message);
assert.strictEqual(result?.title, 'title');
assert.strictEqual(result?.body, '* This is a list item with two lines that have a line break between them\n * This is a nested list item that also has two lines that should have been merged');
});
});