@@ -437,6 +437,7 @@ fn list_installed_plugins() -> Result<Vec<PluginDescriptor>> {
437
437
installed : true ,
438
438
compatibility : PluginCompatibility :: for_current ( & m) ,
439
439
manifest : m,
440
+ installed_version : None ,
440
441
} )
441
442
. collect ( ) ;
442
443
Ok ( descriptors)
@@ -458,6 +459,7 @@ async fn list_catalogue_plugins() -> Result<Vec<PluginDescriptor>> {
458
459
installed : m. is_installed_in ( store) ,
459
460
compatibility : PluginCompatibility :: for_current ( & m) ,
460
461
manifest : m,
462
+ installed_version : None ,
461
463
} )
462
464
. collect ( ) ;
463
465
Ok ( descriptors)
@@ -469,13 +471,81 @@ async fn list_catalogue_and_installed_plugins() -> Result<Vec<PluginDescriptor>>
469
471
Ok ( merge_plugin_lists ( catalogue, installed) )
470
472
}
471
473
474
+ fn summarise ( all_plugins : Vec < PluginDescriptor > ) -> Vec < PluginDescriptor > {
475
+ use itertools:: Itertools ;
476
+
477
+ let names_to_versions = all_plugins
478
+ . into_iter ( )
479
+ . into_group_map_by ( |pd| pd. name . clone ( ) ) ;
480
+ names_to_versions
481
+ . into_values ( )
482
+ . flat_map ( |versions| {
483
+ let ( latest, rest) = latest_and_rest ( versions) ;
484
+ let Some ( mut latest) = latest else {
485
+ // We can't parse things well enough to summarise: return all versions.
486
+ return rest;
487
+ } ;
488
+ if latest. installed {
489
+ // The installed is the latest: return it.
490
+ return vec ! [ latest] ;
491
+ }
492
+
493
+ let installed = rest. into_iter ( ) . find ( |pd| pd. installed ) ;
494
+ let Some ( installed) = installed else {
495
+ // No installed version: return the latest.
496
+ return vec ! [ latest] ;
497
+ } ;
498
+
499
+ // If we get here then there is an installed version which is not the latest.
500
+ // Mark the latest as installed (representing, in this case, that the plugin
501
+ // is installed, even though this version isn't), and record what version _is_
502
+ // installed.
503
+ latest. installed = true ;
504
+ latest. installed_version = Some ( installed. version ) ;
505
+ vec ! [ latest]
506
+ } )
507
+ . collect ( )
508
+ }
509
+
510
+ /// Given a list of plugin descriptors, this looks for the one with the latest version.
511
+ /// If it can determine a latest version, it returns a tuple where the first element is
512
+ /// the latest version, and the second is the remaining versions (order not preserved).
513
+ /// Otherwise it returns None and the original list.
514
+ fn latest_and_rest (
515
+ mut plugins : Vec < PluginDescriptor > ,
516
+ ) -> ( Option < PluginDescriptor > , Vec < PluginDescriptor > ) {
517
+ // `versions` is the parsed version of each plugin in the vector, in the same order.
518
+ // We rely on this 1-1 order-preserving behaviour as we are going to calculate
519
+ // an index from `versions` and use it to index into `plugins`.
520
+ let Ok ( versions) = plugins
521
+ . iter ( )
522
+ . map ( |pd| semver:: Version :: parse ( & pd. version ) )
523
+ . collect :: < Result < Vec < _ > , _ > > ( )
524
+ else {
525
+ return ( None , plugins) ;
526
+ } ;
527
+ let Some ( ( latest_index, _) ) = versions. iter ( ) . enumerate ( ) . max_by_key ( |( _, v) | * v) else {
528
+ return ( None , plugins) ;
529
+ } ;
530
+ let pd = plugins. swap_remove ( latest_index) ;
531
+ ( Some ( pd) , plugins)
532
+ }
533
+
472
534
/// List available or installed plugins.
473
535
#[ derive( Parser , Debug ) ]
474
536
pub struct List {
475
537
/// List only installed plugins.
476
- #[ clap( long = "installed" , takes_value = false ) ]
538
+ #[ clap( long = "installed" , takes_value = false , group = "which" ) ]
477
539
pub installed : bool ,
478
540
541
+ /// List all versions of plugins. This is the default behaviour.
542
+ #[ clap( long = "all" , takes_value = false , group = "which" ) ]
543
+ pub all : bool ,
544
+
545
+ /// List latest and installed versions of plugins.
546
+ #[ clap( long = "summary" , takes_value = false , group = "which" ) ]
547
+ pub summary : bool ,
548
+
479
549
/// Filter the list to plugins containing this string.
480
550
#[ clap( long = "filter" ) ]
481
551
pub filter : Option < String > ,
@@ -489,6 +559,10 @@ impl List {
489
559
list_catalogue_and_installed_plugins ( ) . await
490
560
} ?;
491
561
562
+ if self . summary {
563
+ plugins = summarise ( plugins) ;
564
+ }
565
+
492
566
plugins. sort_by ( |p, q| p. cmp ( q) ) ;
493
567
494
568
if let Some ( filter) = self . filter . as_ref ( ) {
@@ -504,7 +578,15 @@ impl List {
504
578
println ! ( "No plugins found" ) ;
505
579
} else {
506
580
for p in plugins {
507
- let installed = if p. installed { " [installed]" } else { "" } ;
581
+ let installed = if p. installed {
582
+ if let Some ( installed) = p. installed_version . as_ref ( ) {
583
+ format ! ( " [installed version: {installed}]" )
584
+ } else {
585
+ " [installed]" . to_string ( )
586
+ }
587
+ } else {
588
+ "" . to_string ( )
589
+ } ;
508
590
let compat = match & p. compatibility {
509
591
PluginCompatibility :: Compatible => String :: new ( ) ,
510
592
PluginCompatibility :: IncompatibleSpin ( v) => format ! ( " [requires Spin {v}]" ) ,
@@ -527,6 +609,8 @@ impl Search {
527
609
async fn run ( & self ) -> anyhow:: Result < ( ) > {
528
610
let list_cmd = List {
529
611
installed : false ,
612
+ all : true ,
613
+ summary : false ,
530
614
filter : self . filter . clone ( ) ,
531
615
} ;
532
616
@@ -563,6 +647,7 @@ struct PluginDescriptor {
563
647
compatibility : PluginCompatibility ,
564
648
installed : bool ,
565
649
manifest : PluginManifest ,
650
+ installed_version : Option < String > , // only in "latest" mode and if installed version is not latest
566
651
}
567
652
568
653
impl PluginDescriptor {
@@ -701,3 +786,61 @@ async fn try_install(
701
786
Ok ( false )
702
787
}
703
788
}
789
+
790
+ #[ cfg( test) ]
791
+ mod test {
792
+ use super :: * ;
793
+
794
+ fn dummy_descriptor ( version : & str ) -> PluginDescriptor {
795
+ use serde:: Deserialize ;
796
+ PluginDescriptor {
797
+ name : "dummy" . into ( ) ,
798
+ version : version. into ( ) ,
799
+ compatibility : PluginCompatibility :: Compatible ,
800
+ installed : false ,
801
+ manifest : PluginManifest :: deserialize ( serde_json:: json!( {
802
+ "name" : "dummy" ,
803
+ "version" : version,
804
+ "spinCompatibility" : ">= 0.1" ,
805
+ "license" : "dummy" ,
806
+ "packages" : [ ]
807
+ } ) )
808
+ . unwrap ( ) ,
809
+ installed_version : None ,
810
+ }
811
+ }
812
+
813
+ #[ test]
814
+ fn latest_and_rest_if_empty_returns_no_latest_rest_empty ( ) {
815
+ let ( latest, rest) = latest_and_rest ( vec ! [ ] ) ;
816
+ assert ! ( latest. is_none( ) ) ;
817
+ assert_eq ! ( 0 , rest. len( ) ) ;
818
+ }
819
+
820
+ #[ test]
821
+ fn latest_and_rest_if_invalid_ver_returns_no_latest_all_rest ( ) {
822
+ let ( latest, rest) = latest_and_rest ( vec ! [
823
+ dummy_descriptor( "1.2.3" ) ,
824
+ dummy_descriptor( "spork" ) ,
825
+ dummy_descriptor( "1.3.5" ) ,
826
+ ] ) ;
827
+ assert ! ( latest. is_none( ) ) ;
828
+ assert_eq ! ( 3 , rest. len( ) ) ;
829
+ }
830
+
831
+ #[ test]
832
+ fn latest_and_rest_if_valid_ver_returns_latest_and_rest ( ) {
833
+ let ( latest, rest) = latest_and_rest ( vec ! [
834
+ dummy_descriptor( "1.2.3" ) ,
835
+ dummy_descriptor( "2.4.6" ) ,
836
+ dummy_descriptor( "1.3.5" ) ,
837
+ ] ) ;
838
+ let latest = latest. expect ( "should have found a latest" ) ;
839
+ assert_eq ! ( "2.4.6" , latest. version) ;
840
+
841
+ assert_eq ! ( 2 , rest. len( ) ) ;
842
+ let rest_vers: std:: collections:: HashSet < _ > = rest. into_iter ( ) . map ( |p| p. version ) . collect ( ) ;
843
+ assert ! ( rest_vers. contains( "1.2.3" ) ) ;
844
+ assert ! ( rest_vers. contains( "1.3.5" ) ) ;
845
+ }
846
+ }
0 commit comments