diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..e69de29
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6300be1..4d5d470 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,7 +7,7 @@ on:
permissions:
contents: write
-
+
jobs:
build-and-release:
runs-on: ubuntu-latest
diff --git a/extension.js b/extension.js
new file mode 100644
index 0000000..a39abc2
--- /dev/null
+++ b/extension.js
@@ -0,0 +1,273 @@
+// The module 'vscode' contains the VS Code extensibility API
+// Import the module and reference it with the alias vscode in your code below
+const vscode = require('vscode');
+const sharp = require('sharp');
+const fs = require('fs');
+const path = require('path');
+
+const config = vscode.workspace.getConfiguration('responsiveImageGenerator');
+
+// This method is called when your extension is activated
+// Your extension is activated the very first time the command is executed
+
+/**
+ * @param {vscode.ExtensionContext} context
+ */
+
+
+/**
+ * Activates the Responsive Image Generator extension.
+ * Registers commands and completion providers.
+ * @param {vscode.ExtensionContext} context - The extension context provided by VS Code.
+ */
+function activate(context) {
+ const extensionPackageJson = require(path.join(context.extensionPath, 'package.json'));
+
+ // Register the main command for generating responsive images
+ const disposable = vscode.commands.registerCommand('responsive-image-generator.generate', async function () {
+ try {
+ const result = await promptForAllInputs();
+ if (!result) return;
+ const { imageUris, outputDir, sizesToGenerate } = result;
+
+ // Show progress while processing images
+ await vscode.window.withProgress({
+ location: vscode.ProgressLocation.Notification,
+ title: 'Generating responsive images...',
+ cancellable: false
+ }, async (progress) => {
+ progress.report({ message: 'Processing images...' });
+ // Process all images and sizes in parallel
+ await Promise.all(
+ imageUris.map(imageUri => {
+ const itemName = path.basename(imageUri.fsPath, path.extname(imageUri.fsPath));
+ return processImage(imageUri.fsPath, outputDir, itemName, sizesToGenerate);
+ })
+ );
+ });
+
+ vscode.window.showInformationMessage('Responsive images generated successfully!');
+ } catch (err) {
+ vscode.window.showErrorMessage(`Error: ${err.message}`);
+ console.error(err);
+ }
+ });
+ context.subscriptions.push(disposable);
+
+ // Supported languages for completion provider
+ // Dynamically fetch supported languages from package.json activationEvents
+ let supportedLanguages = [];
+ if (extensionPackageJson.activationEvents) {
+ supportedLanguages = extensionPackageJson.activationEvents
+ .filter(event => event.startsWith('onLanguage:'))
+ .map(event => event.replace('onLanguage:', ''));
+ }
+
+ const searchWord = 'responsive';
+ const triggerCharacters = ['<', '>', '!'];
+
+ // Register completion provider for responsive image tag
+ const provider = vscode.languages.registerCompletionItemProvider(
+ supportedLanguages,
+ {
+ /**
+ * Provides completion items for responsive image tag.
+ * @param {vscode.TextDocument} document
+ * @param {vscode.Position} position
+ * @returns {vscode.CompletionItem[]|undefined}
+ */
+ provideCompletionItems(document, position) {
+ const linePrefix = document.lineAt(position).text.split(new RegExp(`[${triggerCharacters.join('')}]`)).at(1)?.trim() || '';
+ if (searchWord.includes(linePrefix) && linePrefix.length > 0) {
+ const completion = new vscode.CompletionItem('responsive_image_basic', vscode.CompletionItemKind.Snippet);
+ completion.command = {
+ command: 'responsive-image-generator.fillResponsiveTag',
+ title: 'Fill Responsive Image Tag',
+ arguments: [document, position.translate(0, -(linePrefix.length + 1))]
+ };
+ return [completion];
+ }
+ return undefined;
+ }
+ },
+ ...triggerCharacters
+ );
+ context.subscriptions.push(provider);
+
+ // Register command to fill responsive tag after completion is selected
+ context.subscriptions.push(vscode.commands.registerCommand('responsive-image-generator.fillResponsiveTag', async (document, triggerStart) => {
+ const result = await promptForAllInputs();
+ if (!result) return;
+ const { imageUris, outputDir, sizesToGenerate } = result;
+
+ // Generate srcset string
+ let srcsetParts = [];
+ for (const imageUri of imageUris) {
+ const itemName = path.basename(imageUri.fsPath, path.extname(imageUri.fsPath));
+ for (const size of sizesToGenerate) {
+ const outputFile = path.join(outputDir, `${itemName}_${size}${path.extname(imageUri.fsPath)}`);
+ try {
+ // Resize and save image
+ await sharp(imageUri.fsPath)
+ .resize(size)
+ .toFile(outputFile);
+ // Use relative paths if configured
+ if(config.get('useRelativePaths')) {
+ const root = config.get('staticAssetsRoot') || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
+ const relativeOutputFile = root ? `./${path.relative(root, outputFile)}` : outputFile;
+ srcsetParts.push(`${relativeOutputFile} ${size}w`);
+ } else {
+ srcsetParts.push(`${outputFile} ${size}w`);
+ }
+ } catch (err) {
+ vscode.window.showErrorMessage(`Error processing ${imageUri.fsPath} for size ${size}: ${err.message}`);
+ }
+ }
+ }
+ const srcset = srcsetParts.join(', ');
+
+ // Generate sizes attribute
+ const sizes = sizesToGenerate.map(size => `(max-width: ${size}px) ${size}px`).join(', ') + ', 100vw';
+
+ // Insert finished snippet, replacing the prefix
+ const editor = vscode.window.activeTextEditor;
+ if (editor) {
+ const snippet = new vscode.SnippetString(`
`);
+ const endPosition = triggerStart.translate(0, document.lineAt(triggerStart).text.length - triggerStart.character);
+ editor.edit(editBuilder => {
+ editBuilder.delete(new vscode.Range(triggerStart, endPosition));
+ }).then(() => {
+ editor.insertSnippet(snippet, triggerStart);
+ });
+ }
+ vscode.window.showInformationMessage('Add responsive image tag and generated images!');
+ }));
+}
+
+
+
+
+/**
+ * Prompts the user for all required inputs: image files, output directory, and sizes.
+ * Handles duplicate logic and error messaging for missing selections.
+ * @returns {Promise<{imageUris: vscode.Uri[], outputDir: string, sizesToGenerate: number[]} | undefined>} Object with all inputs, or undefined if cancelled.
+ */
+async function promptForAllInputs() {
+ // Prompt for image file(s)
+ const imageUris = await promptForImageFiles();
+ if (!imageUris || imageUris.length === 0) {
+ vscode.window.showWarningMessage('No image selected. Operation cancelled.');
+ return undefined;
+ }
+
+ // Prompt for output directory
+ const outputDir = await promptForOutputDirectory();
+ if (!outputDir) {
+ vscode.window.showWarningMessage('No output directory selected. Operation cancelled.');
+ return undefined;
+ }
+
+ // Prompt for sizes
+ const sizesToGenerate = await promptForSizes();
+ if (!sizesToGenerate || sizesToGenerate.length === 0) {
+ vscode.window.showWarningMessage('No sizes selected. Operation cancelled.');
+ return undefined;
+ }
+
+ // Create output directory if it doesn't exist
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ return { imageUris, outputDir, sizesToGenerate };
+}
+
+/**
+ * Prompts the user to select image files.
+ * @returns {Promise} Array of selected image URIs or undefined if cancelled.
+ */
+async function promptForImageFiles() {
+ return await vscode.window.showOpenDialog({
+ canSelectMany: true,
+ openLabel: 'Select Image(s)',
+ filters: {
+ 'Images': ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
+ }
+ });
+}
+
+/**
+ * Prompts the user to select an output directory, preferring 'wwwroot' or 'public' if available.
+ * @returns {Promise} Path to selected output directory or undefined if cancelled.
+ */
+async function promptForOutputDirectory() {
+ const workspaceFolders = vscode.workspace.workspaceFolders || [];
+ let staticContentFolder = workspaceFolders.find(folder => (folder.name.toLowerCase() === 'wwwroot') || (folder.name.toLowerCase() === 'public'));
+ let preselectUri = staticContentFolder ? staticContentFolder.uri : undefined;
+
+ const folder = await vscode.window.showWorkspaceFolderPick({
+ placeHolder: "Select output folder ('wwwroot' or 'public' will be preselected if available)",
+ });
+ if (folder) {
+ return folder.uri.fsPath;
+ } else if (preselectUri) {
+ return preselectUri.fsPath;
+ } else {
+ return undefined;
+ }
+}
+
+/**
+ * Prompts the user to select image sizes to generate.
+ * @returns {Promise} Array of selected sizes or undefined if cancelled.
+ */
+async function promptForSizes() {
+ const sizes = config.get('defaultSizes') || [320, 480, 768, 1024, 1280, 1600, 1920, 2560, 3840, 5120, 7680];
+ const selectedSizes = await vscode.window.showQuickPick(sizes.map(size => size.toString()), {
+ placeHolder: 'Select sizes to generate (you can select multiple)',
+ canPickMany: true
+ });
+ return selectedSizes ? selectedSizes.map(size => parseInt(size)) : undefined;
+}
+
+
+/**
+ * Processes an image: resizes and saves to output directory with item name and size.
+ * @param {string} imagePath - Path to the source image file.
+ * @param {string} outputDir - Directory to save resized images.
+ * @param {string} itemName - Base name for output files.
+ * @param {number[]} sizesToGenerate - Array of sizes to generate.
+ * @returns {Promise}
+ */
+async function processImage(imagePath, outputDir, itemName, sizesToGenerate) {
+ await Promise.all(
+ sizesToGenerate.map(async (size) => {
+ const outputFile = path.join(
+ outputDir,
+ `${itemName}_${size}${path.extname(imagePath)}`
+ );
+ try {
+ await sharp(imagePath)
+ .resize(size)
+ .toFile(outputFile);
+ console.log(`Generated ${outputFile}`);
+ } catch (err) {
+ vscode.window.showErrorMessage(`Error processing ${imagePath} for size ${size}: ${err.message}`);
+ }
+ })
+ );
+}
+
+// This method is called when your extension is deactivated
+
+
+/**
+ * Deactivates the extension.
+ * Called when the extension is deactivated.
+ */
+function deactivate() { }
+
+module.exports = {
+ activate,
+ deactivate
+}
\ No newline at end of file