Skip to content

Commit d5fb866

Browse files
feat: Implement initial VSCode extension for Git Chronoscope, including CLI integration, UI, commands, and logging.
1 parent 0f2ceb7 commit d5fb866

File tree

12 files changed

+671
-0
lines changed

12 files changed

+671
-0
lines changed

vscode-extension/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
out/
3+
*.vsix
4+
.vscode-test/

vscode-extension/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Git Chronoscope VSCode Extension
2+
3+
Generate time-lapse visualizations of your Git repository directly in VSCode.
4+
5+
## Features
6+
7+
- **Generate Time-Lapse**: Create a video showing your repository's evolution
8+
- **File-Specific Time-Lapse**: Focus on a single file's history
9+
- **Sidebar Panel**: Easy access to generation controls
10+
- **Output Channel**: Detailed logging for debugging
11+
12+
## Commands
13+
14+
| Command | Description |
15+
|---------|-------------|
16+
| `Chronoscope: Generate Time-Lapse` | Generate for entire repository |
17+
| `Chronoscope: Generate Time-Lapse for Current File` | Generate for active file |
18+
| `Chronoscope: Show Output` | Show logging output |
19+
20+
## Configuration
21+
22+
| Setting | Default | Description |
23+
|---------|---------|-------------|
24+
| `chronoscope.pythonPath` | `python3` | Path to Python executable |
25+
| `chronoscope.outputFormat` | `mp4` | Default output format |
26+
| `chronoscope.fps` | `5` | Frames per second |
27+
28+
## Requirements
29+
30+
- Python 3.7+
31+
- git-chronoscope installed
32+
- FFmpeg (for video output)
33+
34+
## Installation
35+
36+
1. Open VSCode Extensions (Ctrl+Shift+X)
37+
2. Search for "Git Chronoscope"
38+
3. Click Install
39+
40+
## Development
41+
42+
```bash
43+
cd vscode-extension
44+
npm install
45+
npm run compile
46+
npm test
47+
```

vscode-extension/media/icon.svg

Lines changed: 4 additions & 0 deletions
Loading

vscode-extension/package.json

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"name": "git-chronoscope",
3+
"displayName": "Git Chronoscope",
4+
"description": "Generate time-lapse visualizations of your Git repository's evolution",
5+
"version": "0.1.0",
6+
"publisher": "git-chronoscope",
7+
"engines": {
8+
"vscode": "^1.74.0"
9+
},
10+
"categories": [
11+
"Visualization",
12+
"SCM Providers"
13+
],
14+
"activationEvents": [
15+
"onCommand:chronoscope.generate",
16+
"onCommand:chronoscope.generateFile",
17+
"onCommand:chronoscope.showOutput"
18+
],
19+
"main": "./out/extension.js",
20+
"contributes": {
21+
"commands": [
22+
{
23+
"command": "chronoscope.generate",
24+
"title": "Chronoscope: Generate Time-Lapse"
25+
},
26+
{
27+
"command": "chronoscope.generateFile",
28+
"title": "Chronoscope: Generate Time-Lapse for Current File"
29+
},
30+
{
31+
"command": "chronoscope.showOutput",
32+
"title": "Chronoscope: Show Output"
33+
}
34+
],
35+
"viewsContainers": {
36+
"activitybar": [
37+
{
38+
"id": "chronoscope",
39+
"title": "Git Chronoscope",
40+
"icon": "media/icon.svg"
41+
}
42+
]
43+
},
44+
"views": {
45+
"chronoscope": [
46+
{
47+
"type": "webview",
48+
"id": "chronoscope.sidebar",
49+
"name": "Configuration"
50+
}
51+
]
52+
},
53+
"configuration": {
54+
"title": "Git Chronoscope",
55+
"properties": {
56+
"chronoscope.pythonPath": {
57+
"type": "string",
58+
"default": "python3",
59+
"description": "Path to Python executable"
60+
},
61+
"chronoscope.outputFormat": {
62+
"type": "string",
63+
"enum": [
64+
"mp4",
65+
"gif",
66+
"html"
67+
],
68+
"default": "mp4",
69+
"description": "Default output format"
70+
},
71+
"chronoscope.fps": {
72+
"type": "number",
73+
"default": 5,
74+
"description": "Frames per second"
75+
}
76+
}
77+
}
78+
},
79+
"scripts": {
80+
"vscode:prepublish": "npm run compile",
81+
"compile": "tsc -p ./",
82+
"watch": "tsc -watch -p ./",
83+
"pretest": "npm run compile",
84+
"test": "node ./out/test/runTest.js",
85+
"lint": "eslint src --ext ts"
86+
},
87+
"devDependencies": {
88+
"@types/vscode": "^1.74.0",
89+
"@types/mocha": "^10.0.1",
90+
"@types/node": "^18.0.0",
91+
"@vscode/test-electron": "^2.3.0",
92+
"typescript": "^5.0.0",
93+
"mocha": "^10.2.0",
94+
"eslint": "^8.0.0"
95+
}
96+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as vscode from 'vscode';
2+
import * as cp from 'child_process';
3+
import * as path from 'path';
4+
import { Logger } from './logger';
5+
6+
/**
7+
* Wrapper for the git-chronoscope Python CLI.
8+
*/
9+
export class Chronoscope {
10+
private logger: Logger;
11+
12+
constructor() {
13+
this.logger = Logger.getInstance();
14+
}
15+
16+
/**
17+
* Get the configured Python path.
18+
*/
19+
private getPythonPath(): string {
20+
const config = vscode.workspace.getConfiguration('chronoscope');
21+
return config.get<string>('pythonPath', 'python3');
22+
}
23+
24+
/**
25+
* Get the chronoscope module path.
26+
*/
27+
private getModulePath(): string {
28+
// Assumes chronoscope is installed or in parent directory
29+
const extensionPath = vscode.extensions.getExtension('git-chronoscope.git-chronoscope')?.extensionPath;
30+
if (extensionPath) {
31+
return path.join(extensionPath, '..');
32+
}
33+
return '.';
34+
}
35+
36+
/**
37+
* Generate a time-lapse for a repository.
38+
*/
39+
async generate(
40+
repoPath: string,
41+
outputPath: string,
42+
options: GenerateOptions = {}
43+
): Promise<void> {
44+
const pythonPath = this.getPythonPath();
45+
const config = vscode.workspace.getConfiguration('chronoscope');
46+
47+
const args = [
48+
'-m', 'src.main',
49+
repoPath,
50+
outputPath
51+
];
52+
53+
// Add options
54+
if (options.fps || config.get<number>('fps')) {
55+
args.push('--fps', String(options.fps || config.get<number>('fps', 5)));
56+
}
57+
if (options.format || config.get<string>('outputFormat')) {
58+
args.push('--format', options.format || config.get<string>('outputFormat', 'mp4'));
59+
}
60+
if (options.include) {
61+
args.push('--include', options.include);
62+
}
63+
64+
this.logger.info(`Running: ${pythonPath} ${args.join(' ')}`);
65+
66+
return new Promise((resolve, reject) => {
67+
const cwd = this.getModulePath();
68+
const process = cp.spawn(pythonPath, args, { cwd });
69+
70+
process.stdout.on('data', (data) => {
71+
this.logger.info(data.toString().trim());
72+
});
73+
74+
process.stderr.on('data', (data) => {
75+
this.logger.error(data.toString().trim());
76+
});
77+
78+
process.on('close', (code) => {
79+
if (code === 0) {
80+
this.logger.info('Time-lapse generated successfully');
81+
resolve();
82+
} else {
83+
reject(new Error(`Process exited with code ${code}`));
84+
}
85+
});
86+
87+
process.on('error', (err) => {
88+
this.logger.error('Failed to start process', err);
89+
reject(err);
90+
});
91+
});
92+
}
93+
}
94+
95+
export interface GenerateOptions {
96+
fps?: number;
97+
format?: string;
98+
include?: string;
99+
}

vscode-extension/src/commands.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import * as vscode from 'vscode';
2+
import * as path from 'path';
3+
import { Chronoscope } from './chronoscope';
4+
import { Logger } from './logger';
5+
6+
const logger = Logger.getInstance();
7+
const chronoscope = new Chronoscope();
8+
9+
/**
10+
* Command: Generate time-lapse for the workspace.
11+
*/
12+
export async function generateTimeLapse(): Promise<void> {
13+
const workspaceFolders = vscode.workspace.workspaceFolders;
14+
15+
if (!workspaceFolders || workspaceFolders.length === 0) {
16+
vscode.window.showErrorMessage('No workspace folder open');
17+
return;
18+
}
19+
20+
const repoPath = workspaceFolders[0].uri.fsPath;
21+
logger.info(`Generating time-lapse for: ${repoPath}`);
22+
23+
// Ask for output location
24+
const outputUri = await vscode.window.showSaveDialog({
25+
defaultUri: vscode.Uri.file(path.join(repoPath, 'timelapse.mp4')),
26+
filters: {
27+
'Video': ['mp4'],
28+
'GIF': ['gif'],
29+
'HTML': ['html']
30+
},
31+
title: 'Save Time-Lapse'
32+
});
33+
34+
if (!outputUri) {
35+
return;
36+
}
37+
38+
const outputPath = outputUri.fsPath;
39+
const format = path.extname(outputPath).slice(1);
40+
41+
try {
42+
await vscode.window.withProgress({
43+
location: vscode.ProgressLocation.Notification,
44+
title: 'Generating Time-Lapse',
45+
cancellable: false
46+
}, async (progress) => {
47+
progress.report({ message: 'Processing commits...' });
48+
await chronoscope.generate(repoPath, outputPath, { format });
49+
return;
50+
});
51+
52+
vscode.window.showInformationMessage(`Time-lapse saved to ${outputPath}`);
53+
} catch (error) {
54+
const err = error as Error;
55+
logger.error('Failed to generate time-lapse', err);
56+
vscode.window.showErrorMessage(`Failed to generate time-lapse: ${err.message}`);
57+
}
58+
}
59+
60+
/**
61+
* Command: Generate time-lapse for the current file.
62+
*/
63+
export async function generateFileTimeLapse(): Promise<void> {
64+
const editor = vscode.window.activeTextEditor;
65+
66+
if (!editor) {
67+
vscode.window.showErrorMessage('No file open');
68+
return;
69+
}
70+
71+
const filePath = editor.document.uri.fsPath;
72+
const workspaceFolders = vscode.workspace.workspaceFolders;
73+
74+
if (!workspaceFolders || workspaceFolders.length === 0) {
75+
vscode.window.showErrorMessage('No workspace folder open');
76+
return;
77+
}
78+
79+
const repoPath = workspaceFolders[0].uri.fsPath;
80+
const relativePath = path.relative(repoPath, filePath);
81+
82+
logger.info(`Generating time-lapse for file: ${relativePath}`);
83+
84+
// Ask for output location
85+
const baseName = path.basename(filePath, path.extname(filePath));
86+
const outputUri = await vscode.window.showSaveDialog({
87+
defaultUri: vscode.Uri.file(path.join(repoPath, `${baseName}_timelapse.mp4`)),
88+
filters: {
89+
'Video': ['mp4'],
90+
'GIF': ['gif']
91+
},
92+
title: 'Save Time-Lapse'
93+
});
94+
95+
if (!outputUri) {
96+
return;
97+
}
98+
99+
const outputPath = outputUri.fsPath;
100+
const format = path.extname(outputPath).slice(1);
101+
102+
try {
103+
await vscode.window.withProgress({
104+
location: vscode.ProgressLocation.Notification,
105+
title: `Generating Time-Lapse for ${relativePath}`,
106+
cancellable: false
107+
}, async (progress) => {
108+
progress.report({ message: 'Processing commits...' });
109+
await chronoscope.generate(repoPath, outputPath, {
110+
format,
111+
include: relativePath
112+
});
113+
return;
114+
});
115+
116+
vscode.window.showInformationMessage(`Time-lapse saved to ${outputPath}`);
117+
} catch (error) {
118+
const err = error as Error;
119+
logger.error('Failed to generate file time-lapse', err);
120+
vscode.window.showErrorMessage(`Failed to generate time-lapse: ${err.message}`);
121+
}
122+
}
123+
124+
/**
125+
* Command: Show output channel.
126+
*/
127+
export function showOutput(): void {
128+
logger.show();
129+
}

0 commit comments

Comments
 (0)