mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-20 02:08:47 +00:00
Support folding in git COMMIT_MSG files (#272356)
This commit is contained in:
@@ -79,6 +79,10 @@ const extensions = [
|
|||||||
workspaceFolder: `extensions/vscode-api-tests/testworkspace.code-workspace`,
|
workspaceFolder: `extensions/vscode-api-tests/testworkspace.code-workspace`,
|
||||||
mocha: { timeout: 60_000 },
|
mocha: { timeout: 60_000 },
|
||||||
files: 'extensions/vscode-api-tests/out/workspace-tests/**/*.test.js',
|
files: 'extensions/vscode-api-tests/out/workspace-tests/**/*.test.js',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'git-base',
|
||||||
|
mocha: { timeout: 60_000 }
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,20 @@
|
|||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* 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 { registerAPICommands } from './api/api1';
|
||||||
import { GitBaseExtensionImpl } from './api/extension';
|
import { GitBaseExtensionImpl } from './api/extension';
|
||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
|
import { GitCommitFoldingProvider } from './foldingProvider';
|
||||||
|
|
||||||
export function activate(context: ExtensionContext): GitBaseExtensionImpl {
|
export function activate(context: ExtensionContext): GitBaseExtensionImpl {
|
||||||
const apiImpl = new GitBaseExtensionImpl(new Model());
|
const apiImpl = new GitBaseExtensionImpl(new Model());
|
||||||
context.subscriptions.push(registerAPICommands(apiImpl));
|
context.subscriptions.push(registerAPICommands(apiImpl));
|
||||||
|
|
||||||
|
// Register folding provider for git-commit language
|
||||||
|
context.subscriptions.push(
|
||||||
|
languages.registerFoldingRangeProvider('git-commit', new GitCommitFoldingProvider())
|
||||||
|
);
|
||||||
|
|
||||||
return apiImpl;
|
return apiImpl;
|
||||||
}
|
}
|
||||||
|
|||||||
92
extensions/git-base/src/foldingProvider.ts
Normal file
92
extensions/git-base/src/foldingProvider.ts
Normal file
@@ -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<vscode.FoldingRange[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
258
extensions/git-base/src/test/foldingProvider.test.ts
Normal file
258
extensions/git-base/src/test/foldingProvider.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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%
|
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%
|
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.
|
||||||
echo ### Ipynb tests
|
echo ### Ipynb tests
|
||||||
call npm run test-extension -- -l ipynb
|
call npm run test-extension -- -l ipynb
|
||||||
|
|||||||
@@ -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
|
"$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
|
kill_app
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "### Git Base tests"
|
||||||
|
echo
|
||||||
|
npm run test-extension -- -l git-base
|
||||||
|
kill_app
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "### Ipynb tests"
|
echo "### Ipynb tests"
|
||||||
echo
|
echo
|
||||||
|
|||||||
Reference in New Issue
Block a user