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