@@ -437,6 +437,7 @@ fn list_installed_plugins() -> Result<Vec<PluginDescriptor>> {
437437 installed : true ,
438438 compatibility : PluginCompatibility :: for_current ( & m) ,
439439 manifest : m,
440+ installed_version : None ,
440441 } )
441442 . collect ( ) ;
442443 Ok ( descriptors)
@@ -458,6 +459,7 @@ async fn list_catalogue_plugins() -> Result<Vec<PluginDescriptor>> {
458459 installed : m. is_installed_in ( store) ,
459460 compatibility : PluginCompatibility :: for_current ( & m) ,
460461 manifest : m,
462+ installed_version : None ,
461463 } )
462464 . collect ( ) ;
463465 Ok ( descriptors)
@@ -469,13 +471,81 @@ async fn list_catalogue_and_installed_plugins() -> Result<Vec<PluginDescriptor>>
469471 Ok ( merge_plugin_lists ( catalogue, installed) )
470472}
471473
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+
472534/// List available or installed plugins.
473535#[ derive( Parser , Debug ) ]
474536pub struct List {
475537 /// List only installed plugins.
476- #[ clap( long = "installed" , takes_value = false ) ]
538+ #[ clap( long = "installed" , takes_value = false , group = "which" ) ]
477539 pub installed : bool ,
478540
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+
479549 /// Filter the list to plugins containing this string.
480550 #[ clap( long = "filter" ) ]
481551 pub filter : Option < String > ,
@@ -489,6 +559,10 @@ impl List {
489559 list_catalogue_and_installed_plugins ( ) . await
490560 } ?;
491561
562+ if self . summary {
563+ plugins = summarise ( plugins) ;
564+ }
565+
492566 plugins. sort_by ( |p, q| p. cmp ( q) ) ;
493567
494568 if let Some ( filter) = self . filter . as_ref ( ) {
@@ -504,7 +578,15 @@ impl List {
504578 println ! ( "No plugins found" ) ;
505579 } else {
506580 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+ } ;
508590 let compat = match & p. compatibility {
509591 PluginCompatibility :: Compatible => String :: new ( ) ,
510592 PluginCompatibility :: IncompatibleSpin ( v) => format ! ( " [requires Spin {v}]" ) ,
@@ -527,6 +609,8 @@ impl Search {
527609 async fn run ( & self ) -> anyhow:: Result < ( ) > {
528610 let list_cmd = List {
529611 installed : false ,
612+ all : true ,
613+ summary : false ,
530614 filter : self . filter . clone ( ) ,
531615 } ;
532616
@@ -563,6 +647,7 @@ struct PluginDescriptor {
563647 compatibility : PluginCompatibility ,
564648 installed : bool ,
565649 manifest : PluginManifest ,
650+ installed_version : Option < String > , // only in "latest" mode and if installed version is not latest
566651}
567652
568653impl PluginDescriptor {
@@ -701,3 +786,61 @@ async fn try_install(
701786 Ok ( false )
702787 }
703788}
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