diff --git a/.vscode-test.js b/.vscode-test.js index 2e49c90126b..4c093d0e2b3 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -79,6 +79,10 @@ const extensions = [ workspaceFolder: `extensions/vscode-api-tests/testworkspace.code-workspace`, mocha: { timeout: 60_000 }, files: 'extensions/vscode-api-tests/out/workspace-tests/**/*.test.js', + }, + { + label: 'git-base', + mocha: { timeout: 60_000 } } ]; diff --git a/extensions/git-base/src/extension.ts b/extensions/git-base/src/extension.ts index 17ffb89f82d..453d8f7850f 100644 --- a/extensions/git-base/src/extension.ts +++ b/extensions/git-base/src/extension.ts @@ -3,14 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext } from 'vscode'; +import { ExtensionContext, languages } from 'vscode'; import { registerAPICommands } from './api/api1'; import { GitBaseExtensionImpl } from './api/extension'; import { Model } from './model'; +import { GitCommitFoldingProvider } from './foldingProvider'; export function activate(context: ExtensionContext): GitBaseExtensionImpl { const apiImpl = new GitBaseExtensionImpl(new Model()); context.subscriptions.push(registerAPICommands(apiImpl)); + // Register folding provider for git-commit language + context.subscriptions.push( + languages.registerFoldingRangeProvider('git-commit', new GitCommitFoldingProvider()) + ); + return apiImpl; } diff --git a/extensions/git-base/src/foldingProvider.ts b/extensions/git-base/src/foldingProvider.ts new file mode 100644 index 00000000000..b1c1cc45171 --- /dev/null +++ b/extensions/git-base/src/foldingProvider.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export class GitCommitFoldingProvider implements vscode.FoldingRangeProvider { + + provideFoldingRanges( + document: vscode.TextDocument, + _context: vscode.FoldingContext, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + const ranges: vscode.FoldingRange[] = []; + + let commentBlockStart: number | undefined; + let currentDiffStart: number | undefined; + + for (let i = 0; i < document.lineCount; i++) { + const line = document.lineAt(i); + const lineText = line.text; + + // Check for comment lines (lines starting with #) + if (lineText.startsWith('#')) { + // Close any active diff block when we encounter a comment + if (currentDiffStart !== undefined) { + // Only create fold if there are at least 2 lines + if (i - currentDiffStart > 1) { + ranges.push(new vscode.FoldingRange(currentDiffStart, i - 1)); + } + currentDiffStart = undefined; + } + + if (commentBlockStart === undefined) { + commentBlockStart = i; + } + } else { + // End of comment block + if (commentBlockStart !== undefined) { + // Only create fold if there are at least 2 lines + if (i - commentBlockStart > 1) { + ranges.push(new vscode.FoldingRange( + commentBlockStart, + i - 1, + vscode.FoldingRangeKind.Comment + )); + } + commentBlockStart = undefined; + } + } + + // Check for diff sections (lines starting with "diff --git") + if (lineText.startsWith('diff --git ')) { + // If there's a previous diff block, close it + if (currentDiffStart !== undefined) { + // Only create fold if there are at least 2 lines + if (i - currentDiffStart > 1) { + ranges.push(new vscode.FoldingRange(currentDiffStart, i - 1)); + } + } + // Start new diff block + currentDiffStart = i; + } + } + + // Handle end-of-document cases + + // If comment block extends to end of document + if (commentBlockStart !== undefined) { + if (document.lineCount - commentBlockStart > 1) { + ranges.push(new vscode.FoldingRange( + commentBlockStart, + document.lineCount - 1, + vscode.FoldingRangeKind.Comment + )); + } + } + + // If diff block extends to end of document + if (currentDiffStart !== undefined) { + if (document.lineCount - currentDiffStart > 1) { + ranges.push(new vscode.FoldingRange( + currentDiffStart, + document.lineCount - 1 + )); + } + } + + return ranges; + } +} diff --git a/extensions/git-base/src/test/foldingProvider.test.ts b/extensions/git-base/src/test/foldingProvider.test.ts new file mode 100644 index 00000000000..69f7d35bf18 --- /dev/null +++ b/extensions/git-base/src/test/foldingProvider.test.ts @@ -0,0 +1,258 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { GitCommitFoldingProvider } from '../foldingProvider'; + +suite('GitCommitFoldingProvider', () => { + + function createMockDocument(content: string): vscode.TextDocument { + const lines = content.split('\n'); + return { + lineCount: lines.length, + lineAt: (index: number) => ({ + text: lines[index] || '', + lineNumber: index + }), + } as vscode.TextDocument; + } + + const mockContext: vscode.FoldingContext = {} as vscode.FoldingContext; + const mockToken: vscode.CancellationToken = { isCancellationRequested: false } as vscode.CancellationToken; + + test('empty document returns no folding ranges', () => { + const provider = new GitCommitFoldingProvider(); + const doc = createMockDocument(''); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken); + + assert.strictEqual(Array.isArray(ranges) ? ranges.length : 0, 0); + }); + + test('single line document returns no folding ranges', () => { + const provider = new GitCommitFoldingProvider(); + const doc = createMockDocument('commit message'); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken); + + assert.strictEqual(Array.isArray(ranges) ? ranges.length : 0, 0); + }); + + test('single comment line returns no folding ranges', () => { + const provider = new GitCommitFoldingProvider(); + const doc = createMockDocument('# Comment'); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken); + + assert.strictEqual(Array.isArray(ranges) ? ranges.length : 0, 0); + }); + + test('two comment lines create one folding range', () => { + const provider = new GitCommitFoldingProvider(); + const content = '# Comment 1\n# Comment 2'; + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 1); + assert.strictEqual(ranges[0].start, 0); + assert.strictEqual(ranges[0].end, 1); + assert.strictEqual(ranges[0].kind, vscode.FoldingRangeKind.Comment); + }); + + test('multiple comment lines create one folding range', () => { + const provider = new GitCommitFoldingProvider(); + const content = '# Comment 1\n# Comment 2\n# Comment 3\n# Comment 4'; + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 1); + assert.strictEqual(ranges[0].start, 0); + assert.strictEqual(ranges[0].end, 3); + assert.strictEqual(ranges[0].kind, vscode.FoldingRangeKind.Comment); + }); + + test('comment block followed by content', () => { + const provider = new GitCommitFoldingProvider(); + const content = '# Comment 1\n# Comment 2\nCommit message'; + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 1); + assert.strictEqual(ranges[0].start, 0); + assert.strictEqual(ranges[0].end, 1); + assert.strictEqual(ranges[0].kind, vscode.FoldingRangeKind.Comment); + }); + + test('comment block at end of document', () => { + const provider = new GitCommitFoldingProvider(); + const content = 'Commit message\n\n# Comment 1\n# Comment 2'; + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 1); + assert.strictEqual(ranges[0].start, 2); + assert.strictEqual(ranges[0].end, 3); + assert.strictEqual(ranges[0].kind, vscode.FoldingRangeKind.Comment); + }); + + test('multiple separated comment blocks', () => { + const provider = new GitCommitFoldingProvider(); + const content = '# Comment 1\n# Comment 2\n\nCommit message\n\n# Comment 3\n# Comment 4'; + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 2); + assert.strictEqual(ranges[0].start, 0); + assert.strictEqual(ranges[0].end, 1); + assert.strictEqual(ranges[0].kind, vscode.FoldingRangeKind.Comment); + assert.strictEqual(ranges[1].start, 5); + assert.strictEqual(ranges[1].end, 6); + assert.strictEqual(ranges[1].kind, vscode.FoldingRangeKind.Comment); + }); + + test('single diff line returns no folding ranges', () => { + const provider = new GitCommitFoldingProvider(); + const doc = createMockDocument('diff --git a/file.txt b/file.txt'); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken); + + assert.strictEqual(Array.isArray(ranges) ? ranges.length : 0, 0); + }); + + test('diff block with content creates folding range', () => { + const provider = new GitCommitFoldingProvider(); + const content = 'diff --git a/file.txt b/file.txt\nindex 1234..5678\n--- a/file.txt\n+++ b/file.txt'; + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 1); + assert.strictEqual(ranges[0].start, 0); + assert.strictEqual(ranges[0].end, 3); + assert.strictEqual(ranges[0].kind, undefined); // Diff blocks don't have a specific kind + }); + + test('multiple diff blocks', () => { + const provider = new GitCommitFoldingProvider(); + const content = [ + 'diff --git a/file1.txt b/file1.txt', + '--- a/file1.txt', + '+++ b/file1.txt', + 'diff --git a/file2.txt b/file2.txt', + '--- a/file2.txt', + '+++ b/file2.txt' + ].join('\n'); + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 2); + assert.strictEqual(ranges[0].start, 0); + assert.strictEqual(ranges[0].end, 2); + assert.strictEqual(ranges[1].start, 3); + assert.strictEqual(ranges[1].end, 5); + }); + + test('diff block at end of document', () => { + const provider = new GitCommitFoldingProvider(); + const content = [ + 'Commit message', + '', + 'diff --git a/file.txt b/file.txt', + '--- a/file.txt', + '+++ b/file.txt' + ].join('\n'); + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 1); + assert.strictEqual(ranges[0].start, 2); + assert.strictEqual(ranges[0].end, 4); + }); + + test('realistic git commit message with comments and verbose diff', () => { + const provider = new GitCommitFoldingProvider(); + const content = [ + 'Add folding support for git commit messages', + '', + '# Please enter the commit message for your changes. Lines starting', + '# with \'#\' will be ignored, and an empty message aborts the commit.', + '#', + '# On branch main', + '# Changes to be committed:', + '#\tmodified: extension.ts', + '#\tnew file: foldingProvider.ts', + '#', + '# ------------------------ >8 ------------------------', + '# Do not modify or remove the line above.', + '# Everything below it will be ignored.', + 'diff --git a/extensions/git-base/src/extension.ts b/extensions/git-base/src/extension.ts', + 'index 17ffb89..453d8f7 100644', + '--- a/extensions/git-base/src/extension.ts', + '+++ b/extensions/git-base/src/extension.ts', + '@@ -3,14 +3,20 @@', + ' * Licensed under the MIT License.', + '-import { ExtensionContext } from \'vscode\';', + '+import { ExtensionContext, languages } from \'vscode\';', + 'diff --git a/extensions/git-base/src/foldingProvider.ts b/extensions/git-base/src/foldingProvider.ts', + 'new file mode 100644', + 'index 0000000..2c4a9c3', + '--- /dev/null', + '+++ b/extensions/git-base/src/foldingProvider.ts' + ].join('\n'); + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + // Should have one comment block and two diff blocks + assert.strictEqual(ranges.length, 3); + + // Comment block (lines 2-12) + assert.strictEqual(ranges[0].start, 2); + assert.strictEqual(ranges[0].end, 12); + assert.strictEqual(ranges[0].kind, vscode.FoldingRangeKind.Comment); + + // First diff block (lines 13-20) + assert.strictEqual(ranges[1].start, 13); + assert.strictEqual(ranges[1].end, 20); + assert.strictEqual(ranges[1].kind, undefined); + + // Second diff block (lines 21-25) + assert.strictEqual(ranges[2].start, 21); + assert.strictEqual(ranges[2].end, 25); + assert.strictEqual(ranges[2].kind, undefined); + }); + + test('mixed comment and diff content', () => { + const provider = new GitCommitFoldingProvider(); + const content = [ + 'Fix bug in parser', + '', + '# Comment 1', + '# Comment 2', + '', + 'diff --git a/file.txt b/file.txt', + '--- a/file.txt', + '+++ b/file.txt', + '', + '# Comment 3', + '# Comment 4' + ].join('\n'); + const doc = createMockDocument(content); + const ranges = provider.provideFoldingRanges(doc, mockContext, mockToken) as vscode.FoldingRange[]; + + assert.strictEqual(ranges.length, 3); + + // First comment block + assert.strictEqual(ranges[0].start, 2); + assert.strictEqual(ranges[0].end, 3); + assert.strictEqual(ranges[0].kind, vscode.FoldingRangeKind.Comment); + + // Diff block + assert.strictEqual(ranges[1].start, 5); + assert.strictEqual(ranges[1].end, 8); + assert.strictEqual(ranges[1].kind, undefined); + + // Second comment block + assert.strictEqual(ranges[2].start, 9); + assert.strictEqual(ranges[2].end, 10); + assert.strictEqual(ranges[2].kind, vscode.FoldingRangeKind.Comment); + }); +}); diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 50c14ff8d3f..2be5bfef0a0 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -80,6 +80,11 @@ mkdir %GITWORKSPACE% call "%INTEGRATION_TEST_ELECTRON_PATH%" %GITWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\git --extensionTestsPath=%~dp0\..\extensions\git\out\test %API_TESTS_EXTRA_ARGS% if %errorlevel% neq 0 exit /b %errorlevel% +echo. +echo ### Git Base tests +call npm run test-extension -- -l git-base +if %errorlevel% neq 0 exit /b %errorlevel% + echo. echo ### Ipynb tests call npm run test-extension -- -l ipynb diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 3e26bb17a17..e3c391004f8 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -97,6 +97,12 @@ echo "$INTEGRATION_TEST_ELECTRON_PATH" $(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test $API_TESTS_EXTRA_ARGS kill_app +echo +echo "### Git Base tests" +echo +npm run test-extension -- -l git-base +kill_app + echo echo "### Ipynb tests" echo