@@ -2,7 +2,7 @@ import { exec } from 'node:child_process';
22import { commands , env , type ExtensionContext , QuickPickItemKind , Uri , window , workspace } from 'vscode' ;
33
44import { detectPackageManager } from './detectPackageManager' ;
5- import { DirectoryEntry } from './types' ;
5+ import { DirectoryEntry , NpmRegistryData } from './types' ;
66import {
77 deduplicateSearchTokens ,
88 ENTRY_OPTION ,
@@ -11,12 +11,14 @@ import {
1111 getCompatibilityList ,
1212 getEntryTypeLabel ,
1313 getPlatformsList ,
14+ invertObject ,
1415 KEYWORD_REGEX ,
1516 numberFormatter ,
1617 openListWithSearch ,
1718 STRINGS ,
1819 VALID_KEYWORDS_MAP ,
19- ValidKeyword
20+ ValidKeyword ,
21+ VERSIONS_OPTION
2022} from './utils' ;
2123
2224export async function activate ( context : ExtensionContext ) {
@@ -90,6 +92,10 @@ export async function activate(context: ExtensionContext) {
9092 label : ENTRY_OPTION . INSTALL ,
9193 description : `with ${ preferredManager } ${ selectedEntry . dev ? ' as devDependency' : '' } `
9294 } ,
95+ workspacePath && {
96+ label : ENTRY_OPTION . INSTALL_SPECIFIC_VERSION ,
97+ description : `with ${ preferredManager } ${ selectedEntry . dev ? ' as devDependency' : '' } `
98+ } ,
9399 { label : `open URLs` , kind : QuickPickItemKind . Separator } ,
94100 {
95101 label : ENTRY_OPTION . VISIT_REPO ,
@@ -144,11 +150,15 @@ export async function activate(context: ExtensionContext) {
144150 { label : ENTRY_OPTION . GO_BACK }
145151 ] . filter ( ( option ) => ! ! option && typeof option === 'object' ) ;
146152
153+ function setupAndShowEntryPicker ( ) {
154+ optionPick . title = `Actions for "${ selectedEntry . label } " ${ getEntryTypeLabel ( selectedEntry ) } ` ;
155+ optionPick . placeholder = 'Select an action' ;
156+ optionPick . items = possibleActions ;
157+ optionPick . show ( ) ;
158+ }
159+
147160 const optionPick = window . createQuickPick ( ) ;
148- optionPick . title = `Actions for "${ selectedEntry . label } " ${ getEntryTypeLabel ( selectedEntry ) } ` ;
149- optionPick . placeholder = 'Select an action' ;
150- optionPick . items = possibleActions ;
151- optionPick . show ( ) ;
161+ setupAndShowEntryPicker ( ) ;
152162
153163 optionPick . onDidAccept ( async ( ) => {
154164 const selectedAction = optionPick . selectedItems [ 0 ] ;
@@ -169,6 +179,71 @@ export async function activate(context: ExtensionContext) {
169179 } ) ;
170180 break ;
171181 }
182+ case ENTRY_OPTION . INSTALL_SPECIFIC_VERSION : {
183+ const versionPick = window . createQuickPick ( ) ;
184+ versionPick . title = `Select "${ selectedEntry . label } " package version to install` ;
185+ versionPick . placeholder = 'Loading versions...' ;
186+ versionPick . show ( ) ;
187+
188+ const apiUrl = new URL ( `https://registry.npmjs.org/${ selectedEntry . npmPkg } ` ) ;
189+ const response = await fetch ( apiUrl . href ) ;
190+
191+ if ( ! response . ok ) {
192+ window . showErrorMessage ( `Cannot fetch package versions from npm registry` ) ;
193+ }
194+ const data = ( await response . json ( ) ) as NpmRegistryData ;
195+ const tags = invertObject ( data [ 'dist-tags' ] ) ;
196+
197+ if ( 'versions' in data ) {
198+ const versions = Object . values ( data . versions ) . map ( ( item : NpmRegistryData [ 'versions' ] [ number ] ) => ( {
199+ label : item . version ,
200+ description : item . version in tags ? tags [ item . version ] : '' ,
201+ alwaysShow : true
202+ } ) ) ;
203+
204+ versionPick . placeholder = 'Select a version' ;
205+ versionPick . items = [
206+ ...versions . reverse ( ) ,
207+ { label : '' , kind : QuickPickItemKind . Separator } ,
208+ { label : VERSIONS_OPTION . CANCEL }
209+ ] ;
210+
211+ versionPick . onDidAccept ( async ( ) => {
212+ const selectedVersion = versionPick . selectedItems [ 0 ] ;
213+
214+ if ( selectedVersion . label === VERSIONS_OPTION . CANCEL ) {
215+ versionPick . hide ( ) ;
216+ setupAndShowEntryPicker ( ) ;
217+ return ;
218+ }
219+
220+ exec (
221+ getCommandToRun ( selectedEntry , preferredManager , selectedVersion . label ) ,
222+ { cwd : workspacePath } ,
223+ ( error , stout ) => {
224+ if ( error ) {
225+ window . showErrorMessage (
226+ `An error occurred while trying to install the \`${ selectedEntry . npmPkg } @${ selectedVersion . label } \` package: ${ error . message } `
227+ ) ;
228+ versionPick . hide ( ) ;
229+ setupAndShowEntryPicker ( ) ;
230+ return ;
231+ }
232+ window . showInformationMessage (
233+ `\`${ selectedEntry . npmPkg } @${ selectedVersion . label } \` package has been installed${ selectedEntry . dev ? ' as `devDependency`' : '' } in current workspace using \`${ preferredManager } \`: ${ stout } `
234+ ) ;
235+ versionPick . hide ( ) ;
236+ }
237+ ) ;
238+ } ) ;
239+ } else {
240+ window . showErrorMessage ( `Incompatible response from npm registry` ) ;
241+ versionPick . hide ( ) ;
242+ setupAndShowEntryPicker ( ) ;
243+ }
244+
245+ break ;
246+ }
172247 case ENTRY_OPTION . VISIT_HOMEPAGE : {
173248 if ( selectedEntry . github . urls . homepage ) {
174249 env . openExternal ( Uri . parse ( selectedEntry . github . urls . homepage ) ) ;
0 commit comments