From 12a00866d121a4c985b82f13639316461b627ce9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:26:31 +0000 Subject: [PATCH 1/3] Initial plan From b6cf72faaffdca37b897d5e3fad5d5ec8a0f8d12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:36:42 +0000 Subject: [PATCH 2/3] Improve commit line break unwrapping for lists - Fix list continuation to use content indent (not marker indent) - Support variable-length numbered list markers (1., 10., 100., etc.) - Allow flexible continuation spacing (1+ spaces, but not 4+ which is code) - Preserve multi-paragraph list formatting (blank lines within list items) - Track list context across blank lines for proper paragraph unwrapping - Add comprehensive test cases for all scenarios Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 141 +++++++++++++----- .../github/folderRepositoryManager.test.ts | 104 +++++++++++++ 2 files changed, 209 insertions(+), 36 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index dac84ca592..b144dcf728 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -3067,8 +3067,8 @@ function unwrapCommitMessageBody(body: string): string { return body; } - // Pattern to detect list item markers at the start of a line - const LIST_ITEM_PATTERN = /^[ \t]*([*+\-]|\d+\.)\s/; + // Pattern to detect list item markers at the start of a line and capture the marker + const LIST_ITEM_PATTERN = /^([ \t]*)([*+\-]|\d+\.)([ \t]+)/; // Pattern to detect blockquote markers const BLOCKQUOTE_PATTERN = /^[ \t]*>/; // Pattern to detect fenced code block markers @@ -3083,11 +3083,22 @@ function unwrapCommitMessageBody(body: string): string { return base.length > 0 && !/\s$/.test(base) ? `${base} ${addition}` : `${base}${addition}`; }; + // Get the content indent for a list item (position where actual content starts) + const getListItemContentIndent = (line: string): number => { + const match = line.match(LIST_ITEM_PATTERN); + if (!match) { + return 0; + } + // Content indent = leading whitespace + marker + space after marker + return match[1].length + match[2].length + match[3].length; + }; + const lines = body.split('\n'); const result: string[] = []; let i = 0; let inFencedBlock = false; - const listIndentStack: number[] = []; + // Stack stores { markerIndent, contentIndent } for each nesting level + const listStack: { markerIndent: number; contentIndent: number }[] = []; const getNextNonBlankLineInfo = ( startIndex: number, @@ -3106,19 +3117,23 @@ function unwrapCommitMessageBody(body: string): string { 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; + // Find the active list context for a given line indent + // Returns the content indent if the line is within an active list context + const getActiveListContentIndent = (lineIndent: number): number | undefined => { + for (let idx = listStack.length - 1; idx >= 0; idx--) { + const { markerIndent, contentIndent } = listStack[idx]; + // A line is part of a list item if it has at least 1 space indent + // (but less than contentIndent + 4 which would be a code block) + if (lineIndent >= 1 && lineIndent >= markerIndent) { + listStack.length = idx + 1; + return contentIndent; } - listIndentStack.pop(); + listStack.pop(); } return undefined; }; - const shouldJoinListContinuation = (lineIndex: number, activeIndent: number, baseLine: string): boolean => { + const shouldJoinListContinuation = (lineIndex: number, contentIndent: number, baseLine: string): boolean => { const currentLine = lines[lineIndex]; if (!currentLine) { return false; @@ -3142,12 +3157,13 @@ function unwrapCommitMessageBody(body: string): string { } const currentIndent = getLeadingWhitespaceLength(currentLine); - if (currentIndent < activeIndent + 2) { + // Need at least 1 space to be a continuation + if (currentIndent < 1) { return false; } - // Treat indented code blocks (4+ spaces beyond the bullet) as preserve-only. - if (currentIndent >= activeIndent + 4) { + // 4+ spaces beyond content indent is an indented code block + if (currentIndent >= contentIndent + 4) { return false; } @@ -3156,8 +3172,12 @@ function unwrapCommitMessageBody(body: string): string { return true; } - if (nextInfo.isListItem && nextInfo.indent <= activeIndent) { - return false; + // If next line is a list item at or before the current list level, don't join + if (nextInfo.isListItem) { + const currentListLevel = listStack.length > 0 ? listStack[listStack.length - 1].markerIndent : 0; + if (nextInfo.indent <= currentListLevel) { + return false; + } } return true; @@ -3166,11 +3186,11 @@ function unwrapCommitMessageBody(body: string): string { while (i < lines.length) { const line = lines[i]; - // Preserve blank lines + // Preserve blank lines but don't clear list context + // (multi-paragraph lists are allowed in GitHub markdown) if (line.trim() === '') { result.push(line); i++; - listIndentStack.length = 0; continue; } @@ -3190,26 +3210,25 @@ function unwrapCommitMessageBody(body: string): string { } const lineIndent = getLeadingWhitespaceLength(line); - const isListItem = LIST_ITEM_PATTERN.test(line); + const listItemMatch = line.match(LIST_ITEM_PATTERN); - if (isListItem) { - while (listIndentStack.length && lineIndent < listIndentStack[listIndentStack.length - 1]) { - listIndentStack.pop(); - } + if (listItemMatch) { + const markerIndent = listItemMatch[1].length; + const contentIndent = getListItemContentIndent(line); - if (!listIndentStack.length || lineIndent > listIndentStack[listIndentStack.length - 1]) { - listIndentStack.push(lineIndent); - } else { - listIndentStack[listIndentStack.length - 1] = lineIndent; + // Pop list levels that are at or beyond this indent + while (listStack.length && markerIndent <= listStack[listStack.length - 1].markerIndent) { + listStack.pop(); } + listStack.push({ markerIndent, contentIndent }); result.push(line); i++; continue; } - const activeListIndent = getActiveListIndent(lineIndent); - const codeIndentThreshold = activeListIndent !== undefined ? activeListIndent + 4 : 4; + const activeContentIndent = getActiveListContentIndent(lineIndent); + const codeIndentThreshold = activeContentIndent !== undefined ? activeContentIndent + 4 : 4; const isBlockquote = BLOCKQUOTE_PATTERN.test(line); const isIndentedCode = lineIndent >= codeIndentThreshold; @@ -3219,34 +3238,84 @@ function unwrapCommitMessageBody(body: string): string { continue; } - if (activeListIndent !== undefined && lineIndent >= activeListIndent + 2) { + // Handle list item continuations + if (activeContentIndent !== undefined && lineIndent >= 1) { const baseIndex = result.length - 1; - if (baseIndex >= 0) { - let baseLine = result[baseIndex]; + // Only try to join with previous line if it's not blank + // Multi-paragraph lists have blank lines that should be preserved + const baseLine = baseIndex >= 0 ? result[baseIndex] : ''; + const previousLineIsBlank = baseLine.trim() === ''; + + if (!previousLineIsBlank && baseIndex >= 0) { + let joinedLine = baseLine; let appended = false; let currentIndex = i; while ( currentIndex < lines.length && - shouldJoinListContinuation(currentIndex, activeListIndent, baseLine) + shouldJoinListContinuation(currentIndex, activeContentIndent, joinedLine) ) { const continuationText = lines[currentIndex].trim(); if (continuationText) { - baseLine = appendWithSpace(baseLine, continuationText); + joinedLine = appendWithSpace(joinedLine, continuationText); appended = true; } currentIndex++; } if (appended) { - result[baseIndex] = baseLine; + result[baseIndex] = joinedLine; i = currentIndex; continue; } } - result.push(line); + // For multi-paragraph continuations or standalone indented lines, + // preserve indentation but unwrap consecutive continuation lines + let joinedLine = line; i++; + + while (i < lines.length) { + const nextLine = lines[i]; + + if (nextLine.trim() === '') { + break; + } + + if (FENCE_PATTERN.test(nextLine)) { + break; + } + + if (LIST_ITEM_PATTERN.test(nextLine)) { + break; + } + + if (BLOCKQUOTE_PATTERN.test(nextLine)) { + break; + } + + const nextIndent = getLeadingWhitespaceLength(nextLine); + // Check for code block + if (nextIndent >= activeContentIndent + 4) { + break; + } + + // Must have at least 1 space to be a continuation + if (nextIndent < 1) { + break; + } + + // Check for hard line break + if (hasHardLineBreak(joinedLine)) { + break; + } + + // Join this line - preserve the original indentation for the first line + joinedLine = appendWithSpace(joinedLine, nextLine.trim()); + i++; + } + + result.push(joinedLine); continue; } diff --git a/src/test/github/folderRepositoryManager.test.ts b/src/test/github/folderRepositoryManager.test.ts index 6d6f6c3114..530ffaf9e2 100644 --- a/src/test/github/folderRepositoryManager.test.ts +++ b/src/test/github/folderRepositoryManager.test.ts @@ -230,4 +230,108 @@ describe('titleAndBodyFrom', function () { 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'); }); + + it('handles basic numeric list continuation', async function () { + const message = Promise.resolve('title\n\n1. Basic numeric list\n continuation.\n Third line'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '1. Basic numeric list continuation. Third line'); + }); + + it('handles additional spaces OK for continuation', async function () { + const message = Promise.resolve('title\n\n2. Additional spaces are\n OK for a continuation (unless it\'s 4 spaces which would be a code block).\n Third line'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '2. Additional spaces are OK for a continuation (unless it\'s 4 spaces which would be a code block). Third line'); + }); + + it('handles asterisk list with extra spaces', async function () { + const message = Promise.resolve('title\n\n* Additional spaces are\n OK for a continuation (unless it\'s 4 spaces which would be a code block).\n Third line'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '* Additional spaces are OK for a continuation (unless it\'s 4 spaces which would be a code block). Third line'); + }); + + it('handles multi-digit numbers (10.)', async function () { + const message = Promise.resolve('title\n\n10. Multi-digit numbers should also\n work for a continuation.\n Third line'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '10. Multi-digit numbers should also work for a continuation. Third line'); + }); + + it('handles multi-paragraph list - numbered', async function () { + const message = Promise.resolve('title\n\n11. Multi-paragraph lists are also supported.\n\n Second paragraph in the same list item.\n Third line'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '11. Multi-paragraph lists are also supported.\n\n Second paragraph in the same list item. Third line'); + }); + + it('handles multi-paragraph list - asterisk', async function () { + const message = Promise.resolve('title\n\n* Multi-paragraph lists are also supported.\n\n Second paragraph in the same list item.\n Third line'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '* Multi-paragraph lists are also supported.\n\n Second paragraph in the same list item. Third line'); + }); + + it('handles item with code block - numbered', async function () { + const message = Promise.resolve('title\n\n1. Item with code:\n\n ```\n code line\n code line\n ```'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '1. Item with code:\n\n ```\n code line\n code line\n ```'); + }); + + it('handles item with code block - asterisk', async function () { + const message = Promise.resolve('title\n\n* Item with code:\n\n ```\n code line\n code line\n ```'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '* Item with code:\n\n ```\n code line\n code line\n ```'); + }); + + it('handles fewer spaces OK - numbered (1 space)', async function () { + const message = Promise.resolve('title\n\n1. Fewer spaces are also OK\n for a list continuation (as long as there\'s at least one space)'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '1. Fewer spaces are also OK for a list continuation (as long as there\'s at least one space)'); + }); + + it('handles fewer spaces OK - asterisk (1 space)', async function () { + const message = Promise.resolve('title\n\n* Fewer spaces are also OK\n for a list continuation (as long as there\'s at least one space)'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '* Fewer spaces are also OK for a list continuation (as long as there\'s at least one space)'); + }); + + it('handles nested numbered lists', async function () { + const message = Promise.resolve('title\n\n1. First level item\n continuation of first level\n 1. Nested numbered item\n with continuation'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '1. First level item continuation of first level\n 1. Nested numbered item with continuation'); + }); + + it('handles nested multi-digit numbered lists', async function () { + const message = Promise.resolve('title\n\n10. First level item with\n multi-line content\n 10. Nested with multi-digit\n number and continuation'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '10. First level item with multi-line content\n 10. Nested with multi-digit number and continuation'); + }); + + it('handles nested multi-paragraph lists', async function () { + const message = Promise.resolve('title\n\n* Outer item\n\n Second paragraph of outer\n with continuation\n * Inner item\n\n Second paragraph of inner\n with continuation'); + + const result = await titleAndBodyFrom(message); + assert.strictEqual(result?.title, 'title'); + assert.strictEqual(result?.body, '* Outer item\n\n Second paragraph of outer with continuation\n * Inner item\n\n Second paragraph of inner with continuation'); + }); }); From f6b1a779c446b30005ed9c7baa431dea3dbe791b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:39:29 +0000 Subject: [PATCH 3/3] Fix plain text joining to trim whitespace Use appendWithSpace with trimmed text to avoid double spaces when joining lines with leading whitespace. Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/folderRepositoryManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index b144dcf728..84e7420615 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -3356,7 +3356,7 @@ function unwrapCommitMessageBody(body: string): string { } // Join this line with a space - joinedLine += ' ' + nextLine; + joinedLine = appendWithSpace(joinedLine, nextLine.trim()); i++; }