Skip to content

Commit 600cca8

Browse files
committed
fix: add permissions section to release workflow and create copilot instructions file
1 parent 723d374 commit 600cca8

File tree

3 files changed

+276
-0
lines changed

3 files changed

+276
-0
lines changed

.github/copilot-instructions.md

Whitespace-only changes.

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ on:
55
tags:
66
- 'v*' # Triggers on version tags like v1.0.0
77

8+
permissions:
9+
contents: write
10+
811
jobs:
912
build-and-release:
1013
runs-on: ubuntu-latest

extension.js

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// The module 'vscode' contains the VS Code extensibility API
2+
// Import the module and reference it with the alias vscode in your code below
3+
const vscode = require('vscode');
4+
const sharp = require('sharp');
5+
const fs = require('fs');
6+
const path = require('path');
7+
8+
const config = vscode.workspace.getConfiguration('responsiveImageGenerator');
9+
10+
// This method is called when your extension is activated
11+
// Your extension is activated the very first time the command is executed
12+
13+
/**
14+
* @param {vscode.ExtensionContext} context
15+
*/
16+
17+
18+
/**
19+
* Activates the Responsive Image Generator extension.
20+
* Registers commands and completion providers.
21+
* @param {vscode.ExtensionContext} context - The extension context provided by VS Code.
22+
*/
23+
function activate(context) {
24+
const extensionPackageJson = require(path.join(context.extensionPath, 'package.json'));
25+
26+
// Register the main command for generating responsive images
27+
const disposable = vscode.commands.registerCommand('responsive-image-generator.generate', async function () {
28+
try {
29+
const result = await promptForAllInputs();
30+
if (!result) return;
31+
const { imageUris, outputDir, sizesToGenerate } = result;
32+
33+
// Show progress while processing images
34+
await vscode.window.withProgress({
35+
location: vscode.ProgressLocation.Notification,
36+
title: 'Generating responsive images...',
37+
cancellable: false
38+
}, async (progress) => {
39+
progress.report({ message: 'Processing images...' });
40+
// Process all images and sizes in parallel
41+
await Promise.all(
42+
imageUris.map(imageUri => {
43+
const itemName = path.basename(imageUri.fsPath, path.extname(imageUri.fsPath));
44+
return processImage(imageUri.fsPath, outputDir, itemName, sizesToGenerate);
45+
})
46+
);
47+
});
48+
49+
vscode.window.showInformationMessage('Responsive images generated successfully!');
50+
} catch (err) {
51+
vscode.window.showErrorMessage(`Error: ${err.message}`);
52+
console.error(err);
53+
}
54+
});
55+
context.subscriptions.push(disposable);
56+
57+
// Supported languages for completion provider
58+
// Dynamically fetch supported languages from package.json activationEvents
59+
let supportedLanguages = [];
60+
if (extensionPackageJson.activationEvents) {
61+
supportedLanguages = extensionPackageJson.activationEvents
62+
.filter(event => event.startsWith('onLanguage:'))
63+
.map(event => event.replace('onLanguage:', ''));
64+
}
65+
66+
const searchWord = 'responsive';
67+
const triggerCharacters = ['<', '>', '!'];
68+
69+
// Register completion provider for responsive image tag
70+
const provider = vscode.languages.registerCompletionItemProvider(
71+
supportedLanguages,
72+
{
73+
/**
74+
* Provides completion items for responsive image tag.
75+
* @param {vscode.TextDocument} document
76+
* @param {vscode.Position} position
77+
* @returns {vscode.CompletionItem[]|undefined}
78+
*/
79+
provideCompletionItems(document, position) {
80+
const linePrefix = document.lineAt(position).text.split(new RegExp(`[${triggerCharacters.join('')}]`)).at(1)?.trim() || '';
81+
if (searchWord.includes(linePrefix) && linePrefix.length > 0) {
82+
const completion = new vscode.CompletionItem('responsive_image_basic', vscode.CompletionItemKind.Snippet);
83+
completion.command = {
84+
command: 'responsive-image-generator.fillResponsiveTag',
85+
title: 'Fill Responsive Image Tag',
86+
arguments: [document, position.translate(0, -(linePrefix.length + 1))]
87+
};
88+
return [completion];
89+
}
90+
return undefined;
91+
}
92+
},
93+
...triggerCharacters
94+
);
95+
context.subscriptions.push(provider);
96+
97+
// Register command to fill responsive tag after completion is selected
98+
context.subscriptions.push(vscode.commands.registerCommand('responsive-image-generator.fillResponsiveTag', async (document, triggerStart) => {
99+
const result = await promptForAllInputs();
100+
if (!result) return;
101+
const { imageUris, outputDir, sizesToGenerate } = result;
102+
103+
// Generate srcset string
104+
let srcsetParts = [];
105+
for (const imageUri of imageUris) {
106+
const itemName = path.basename(imageUri.fsPath, path.extname(imageUri.fsPath));
107+
for (const size of sizesToGenerate) {
108+
const outputFile = path.join(outputDir, `${itemName}_${size}${path.extname(imageUri.fsPath)}`);
109+
try {
110+
// Resize and save image
111+
await sharp(imageUri.fsPath)
112+
.resize(size)
113+
.toFile(outputFile);
114+
// Use relative paths if configured
115+
if(config.get('useRelativePaths')) {
116+
const root = config.get('staticAssetsRoot') || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
117+
const relativeOutputFile = root ? `./${path.relative(root, outputFile)}` : outputFile;
118+
srcsetParts.push(`${relativeOutputFile} ${size}w`);
119+
} else {
120+
srcsetParts.push(`${outputFile} ${size}w`);
121+
}
122+
} catch (err) {
123+
vscode.window.showErrorMessage(`Error processing ${imageUri.fsPath} for size ${size}: ${err.message}`);
124+
}
125+
}
126+
}
127+
const srcset = srcsetParts.join(', ');
128+
129+
// Generate sizes attribute
130+
const sizes = sizesToGenerate.map(size => `(max-width: ${size}px) ${size}px`).join(', ') + ', 100vw';
131+
132+
// Insert finished snippet, replacing the prefix
133+
const editor = vscode.window.activeTextEditor;
134+
if (editor) {
135+
const snippet = new vscode.SnippetString(`<img src="${srcsetParts[0]?.split(' ')[0] || ''}" srcset="${srcset}" sizes="${sizes}" alt="$1">`);
136+
const endPosition = triggerStart.translate(0, document.lineAt(triggerStart).text.length - triggerStart.character);
137+
editor.edit(editBuilder => {
138+
editBuilder.delete(new vscode.Range(triggerStart, endPosition));
139+
}).then(() => {
140+
editor.insertSnippet(snippet, triggerStart);
141+
});
142+
}
143+
vscode.window.showInformationMessage('Add responsive image tag and generated images!');
144+
}));
145+
}
146+
147+
148+
149+
150+
/**
151+
* Prompts the user for all required inputs: image files, output directory, and sizes.
152+
* Handles duplicate logic and error messaging for missing selections.
153+
* @returns {Promise<{imageUris: vscode.Uri[], outputDir: string, sizesToGenerate: number[]} | undefined>} Object with all inputs, or undefined if cancelled.
154+
*/
155+
async function promptForAllInputs() {
156+
// Prompt for image file(s)
157+
const imageUris = await promptForImageFiles();
158+
if (!imageUris || imageUris.length === 0) {
159+
vscode.window.showWarningMessage('No image selected. Operation cancelled.');
160+
return undefined;
161+
}
162+
163+
// Prompt for output directory
164+
const outputDir = await promptForOutputDirectory();
165+
if (!outputDir) {
166+
vscode.window.showWarningMessage('No output directory selected. Operation cancelled.');
167+
return undefined;
168+
}
169+
170+
// Prompt for sizes
171+
const sizesToGenerate = await promptForSizes();
172+
if (!sizesToGenerate || sizesToGenerate.length === 0) {
173+
vscode.window.showWarningMessage('No sizes selected. Operation cancelled.');
174+
return undefined;
175+
}
176+
177+
// Create output directory if it doesn't exist
178+
if (!fs.existsSync(outputDir)) {
179+
fs.mkdirSync(outputDir, { recursive: true });
180+
}
181+
182+
return { imageUris, outputDir, sizesToGenerate };
183+
}
184+
185+
/**
186+
* Prompts the user to select image files.
187+
* @returns {Promise<vscode.Uri[]|undefined>} Array of selected image URIs or undefined if cancelled.
188+
*/
189+
async function promptForImageFiles() {
190+
return await vscode.window.showOpenDialog({
191+
canSelectMany: true,
192+
openLabel: 'Select Image(s)',
193+
filters: {
194+
'Images': ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
195+
}
196+
});
197+
}
198+
199+
/**
200+
* Prompts the user to select an output directory, preferring 'wwwroot' or 'public' if available.
201+
* @returns {Promise<string|undefined>} Path to selected output directory or undefined if cancelled.
202+
*/
203+
async function promptForOutputDirectory() {
204+
const workspaceFolders = vscode.workspace.workspaceFolders || [];
205+
let staticContentFolder = workspaceFolders.find(folder => (folder.name.toLowerCase() === 'wwwroot') || (folder.name.toLowerCase() === 'public'));
206+
let preselectUri = staticContentFolder ? staticContentFolder.uri : undefined;
207+
208+
const folder = await vscode.window.showWorkspaceFolderPick({
209+
placeHolder: "Select output folder ('wwwroot' or 'public' will be preselected if available)",
210+
});
211+
if (folder) {
212+
return folder.uri.fsPath;
213+
} else if (preselectUri) {
214+
return preselectUri.fsPath;
215+
} else {
216+
return undefined;
217+
}
218+
}
219+
220+
/**
221+
* Prompts the user to select image sizes to generate.
222+
* @returns {Promise<number[]|undefined>} Array of selected sizes or undefined if cancelled.
223+
*/
224+
async function promptForSizes() {
225+
const sizes = config.get('defaultSizes') || [320, 480, 768, 1024, 1280, 1600, 1920, 2560, 3840, 5120, 7680];
226+
const selectedSizes = await vscode.window.showQuickPick(sizes.map(size => size.toString()), {
227+
placeHolder: 'Select sizes to generate (you can select multiple)',
228+
canPickMany: true
229+
});
230+
return selectedSizes ? selectedSizes.map(size => parseInt(size)) : undefined;
231+
}
232+
233+
234+
/**
235+
* Processes an image: resizes and saves to output directory with item name and size.
236+
* @param {string} imagePath - Path to the source image file.
237+
* @param {string} outputDir - Directory to save resized images.
238+
* @param {string} itemName - Base name for output files.
239+
* @param {number[]} sizesToGenerate - Array of sizes to generate.
240+
* @returns {Promise<void>}
241+
*/
242+
async function processImage(imagePath, outputDir, itemName, sizesToGenerate) {
243+
await Promise.all(
244+
sizesToGenerate.map(async (size) => {
245+
const outputFile = path.join(
246+
outputDir,
247+
`${itemName}_${size}${path.extname(imagePath)}`
248+
);
249+
try {
250+
await sharp(imagePath)
251+
.resize(size)
252+
.toFile(outputFile);
253+
console.log(`Generated ${outputFile}`);
254+
} catch (err) {
255+
vscode.window.showErrorMessage(`Error processing ${imagePath} for size ${size}: ${err.message}`);
256+
}
257+
})
258+
);
259+
}
260+
261+
// This method is called when your extension is deactivated
262+
263+
264+
/**
265+
* Deactivates the extension.
266+
* Called when the extension is deactivated.
267+
*/
268+
function deactivate() { }
269+
270+
module.exports = {
271+
activate,
272+
deactivate
273+
}

0 commit comments

Comments
 (0)