@@ -95,24 +95,50 @@ export interface LaunchpadItemQuickPickItem extends QuickPickItemOfT<LaunchpadIt
95
95
type ConnectMoreIntegrationsItem = QuickPickItem & {
96
96
item : undefined ;
97
97
group : undefined ;
98
+ search : undefined ;
98
99
} ;
99
100
const connectMoreIntegrationsItem : ConnectMoreIntegrationsItem = {
100
101
label : 'Connect an Additional Integration...' ,
101
102
detail : 'Connect additional integrations to view their pull requests in Launchpad' ,
102
103
item : undefined ,
103
104
group : undefined ,
105
+ search : undefined ,
104
106
} ;
105
107
function isConnectMoreIntegrationsItem ( item : unknown ) : item is ConnectMoreIntegrationsItem {
106
108
return item === connectMoreIntegrationsItem ;
107
109
}
108
110
111
+ type ToggleSearchItem = QuickPickItem & {
112
+ item : undefined ;
113
+ group : undefined ;
114
+ search : boolean ;
115
+ } ;
116
+ const toggleSearchOnItem : ToggleSearchItem = {
117
+ label : 'Search for Other Pull Requests...' ,
118
+ item : undefined ,
119
+ group : undefined ,
120
+ search : true ,
121
+ alwaysShow : true ,
122
+ } ;
123
+ const toggleSearchOffItem : ToggleSearchItem = {
124
+ label : 'Stop Searching' ,
125
+ item : undefined ,
126
+ group : undefined ,
127
+ search : false ,
128
+ alwaysShow : true ,
129
+ } ;
130
+ function isToggleSearchItem ( item : unknown ) : item is ToggleSearchItem {
131
+ return item === toggleSearchOnItem || item === toggleSearchOffItem ;
132
+ }
133
+
109
134
interface Context {
110
135
result : LaunchpadCategorizedResult ;
111
136
112
137
title : string ;
113
138
collapsed : Map < LaunchpadGroup , boolean > ;
114
139
telemetryContext : LaunchpadTelemetryContext | undefined ;
115
140
connectedIntegrations : Map < IntegrationId , boolean > ;
141
+ isSearching : boolean ;
116
142
}
117
143
118
144
interface GroupedLaunchpadItem extends LaunchpadItem {
@@ -151,6 +177,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
151
177
private readonly updateItemsDebouncer = createAsyncDebouncer ( 500 ) ;
152
178
private readonly source : Source ;
153
179
private readonly telemetryContext : LaunchpadTelemetryContext | undefined ;
180
+ private savedSearch : string | undefined ;
154
181
155
182
constructor ( container : Container , args ?: LaunchpadCommandArgs ) {
156
183
super ( container , 'launchpad' , 'launchpad' , `GitLens Launchpad\u00a0\u00a0${ proBadge } ` , {
@@ -223,6 +250,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
223
250
collapsed : collapsed ,
224
251
telemetryContext : this . telemetryContext ,
225
252
connectedIntegrations : await this . container . launchpad . getConnectedIntegrations ( ) ,
253
+ isSearching : false ,
226
254
} ;
227
255
228
256
let opened = false ;
@@ -302,6 +330,12 @@ export class LaunchpadCommand extends QuickCommand<State> {
302
330
newlyConnected = Boolean ( connected ) ;
303
331
await updateContextItems ( this . container , context , { force : newlyConnected } ) ;
304
332
continue ;
333
+ } else if ( isToggleSearchItem ( result ) ) {
334
+ context . isSearching = result . search ;
335
+ if ( ! context . isSearching ) {
336
+ this . updateItemsDebouncer . cancel ( ) ;
337
+ }
338
+ continue ;
305
339
}
306
340
307
341
state . item = result ;
@@ -372,7 +406,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
372
406
state : StepState < State > ,
373
407
context : Context ,
374
408
{ picked, selectTopItem } : { picked ?: string ; selectTopItem ?: boolean } ,
375
- ) : StepResultGenerator < GroupedLaunchpadItem | ConnectMoreIntegrationsItem > {
409
+ ) : StepResultGenerator < GroupedLaunchpadItem | ConnectMoreIntegrationsItem | ToggleSearchItem > {
376
410
const hasDisconnectedIntegrations = [ ...context . connectedIntegrations . values ( ) ] . some ( c => ! c ) ;
377
411
378
412
const buildGroupHeading = (
@@ -466,6 +500,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
466
500
| LaunchpadItemQuickPickItem
467
501
| DirectiveQuickPickItem
468
502
| ConnectMoreIntegrationsItem
503
+ | ToggleSearchItem
469
504
) [ ] = [ ] ;
470
505
471
506
if ( items . length ) {
@@ -477,7 +512,11 @@ export class LaunchpadCommand extends QuickCommand<State> {
477
512
uiGroups . get ( 'blocked' ) ?. [ 0 ] ||
478
513
uiGroups . get ( 'follow-up' ) ?. [ 0 ] ||
479
514
uiGroups . get ( 'needs-review' ) ?. [ 0 ] ;
480
- for ( const [ ui , groupItems ] of uiGroups ) {
515
+ for ( let [ ui , groupItems ] of uiGroups ) {
516
+ if ( context . isSearching ) {
517
+ groupItems = groupItems . filter ( i => i . isSearched ) ;
518
+ }
519
+
481
520
if ( ! groupItems . length ) continue ;
482
521
483
522
if ( ! isSearching ) {
@@ -488,7 +527,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
488
527
}
489
528
490
529
groupedAndSorted . push (
491
- ...groupItems . map ( i => buildLaunchpadQuickPickItem ( i , ui , topItem , isSearching ) ) ,
530
+ ...groupItems . map ( i => buildLaunchpadQuickPickItem ( i , ui , topItem , context . isSearching ) ) ,
492
531
) ;
493
532
}
494
533
}
@@ -497,6 +536,8 @@ export class LaunchpadCommand extends QuickCommand<State> {
497
536
} ;
498
537
499
538
function getItemsAndPlaceholder ( isSearching ?: boolean ) {
539
+ const toggleSearchItem = context . isSearching ? toggleSearchOffItem : toggleSearchOnItem ;
540
+
500
541
if ( context . result . error != null ) {
501
542
return {
502
543
placeholder : `Unable to load items (${
@@ -513,34 +554,22 @@ export class LaunchpadCommand extends QuickCommand<State> {
513
554
if ( ! context . result . items . length ) {
514
555
return {
515
556
placeholder : 'All done! Take a vacation' ,
516
- items : [ createDirectiveQuickPickItem ( Directive . Cancel , undefined , { label : 'OK' } ) ] ,
557
+ items : [ toggleSearchItem ] ,
517
558
} ;
518
559
}
519
560
520
561
return {
521
- placeholder : 'Choose an item, type a term to search, or paste in a PR URL' ,
522
- items : getLaunchpadQuickPickItems ( context . result . items , isSearching ) ,
562
+ placeholder : context . isSearching
563
+ ? 'Type a term to search'
564
+ : 'Choose an item or paste in a pull request URL to search' ,
565
+ items : [ toggleSearchItem , ...getLaunchpadQuickPickItems ( context . result . items , isSearching ) ] ,
523
566
} ;
524
567
}
525
568
526
- const combineQuickpickItemsWithSearchResults = < T extends { item : { id : string } } | object > (
527
- arr : readonly T [ ] ,
528
- items : T [ ] ,
529
- ) => {
530
- const ids : Set < string > = new Set (
531
- arr . map ( i => 'item' in i && i . item ?. id ) . filter ( id => typeof id === 'string' ) ,
532
- ) ;
533
- const result = [ ...arr ] ;
534
- for ( const item of items ) {
535
- if ( 'item' in item && item . item ?. id && ! ids . has ( item . item . id ) ) {
536
- result . push ( item ) ;
537
- }
538
- }
539
- return result ;
540
- } ;
541
-
542
569
const updateItems = async (
543
- quickpick : QuickPick < LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem > ,
570
+ quickpick : QuickPick <
571
+ LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem | ToggleSearchItem
572
+ > ,
544
573
force ?: boolean ,
545
574
) => {
546
575
const search = quickpick . value ;
@@ -550,15 +579,15 @@ export class LaunchpadCommand extends QuickCommand<State> {
550
579
await updateContextItems (
551
580
this . container ,
552
581
context ,
553
- { force : force , search : search } ,
582
+ { force : force , search : context . isSearching ? search : undefined } ,
554
583
cancellationToken ,
555
584
) ;
556
585
if ( cancellationToken . isCancellationRequested ) {
557
586
return ;
558
587
}
559
588
const { items, placeholder } = getItemsAndPlaceholder ( Boolean ( search ) ) ;
560
589
quickpick . placeholder = placeholder ;
561
- quickpick . items = search ? combineQuickpickItemsWithSearchResults ( quickpick . items , items ) : items ;
590
+ quickpick . items = items ;
562
591
} ) ;
563
592
} finally {
564
593
quickpick . busy = false ;
@@ -568,7 +597,9 @@ export class LaunchpadCommand extends QuickCommand<State> {
568
597
// Should only be used for optimistic update of the list when some UI property (like pinned, snoozed) changed with an
569
598
// item. For all other cases, use updateItems.
570
599
const optimisticallyUpdateItems = (
571
- quickpick : QuickPick < LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem > ,
600
+ quickpick : QuickPick <
601
+ LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem | ToggleSearchItem
602
+ > ,
572
603
) => {
573
604
quickpick . items = getLaunchpadQuickPickItems (
574
605
context . result . items ,
@@ -577,7 +608,6 @@ export class LaunchpadCommand extends QuickCommand<State> {
577
608
} ;
578
609
579
610
const { items, placeholder } = getItemsAndPlaceholder ( ) ;
580
- const nonGroupedItems = items . filter ( i => ! isDirectiveQuickPickItem ( i ) ) ;
581
611
582
612
const step = createPickStep ( {
583
613
title : context . title ,
@@ -592,30 +622,72 @@ export class LaunchpadCommand extends QuickCommand<State> {
592
622
LaunchpadSettingsQuickInputButton ,
593
623
RefreshQuickInputButton ,
594
624
] ,
625
+ onDidActivate : quickpick => {
626
+ if ( this . savedSearch == null || this . savedSearch . length === 0 ) return ;
627
+ if ( context . isSearching ) {
628
+ quickpick . value = this . savedSearch ;
629
+ }
630
+
631
+ this . savedSearch = undefined ;
632
+ } ,
595
633
onDidChangeValue : async quickpick => {
596
634
const { value } = quickpick ;
597
- const hideGroups = Boolean ( value ?. length ) ;
598
- const consideredItems = hideGroups ? nonGroupedItems : items ;
635
+ this . savedSearch = value ;
599
636
600
- let updated = false ;
601
- for ( const item of consideredItems ) {
637
+ // Clear alwaysShow for items which we matched by PR number or other identity
638
+ for ( const item of quickpick . items . filter (
639
+ i => ! isDirectiveQuickPickItem ( i ) && ! isToggleSearchItem ( i ) ,
640
+ ) ) {
602
641
if ( item . alwaysShow ) {
603
642
item . alwaysShow = false ;
604
- updated = true ;
605
643
}
606
644
}
607
645
608
- // By doing the following we make sure we operate with the PRs that belong to Launchpad initially.
609
- // Also, when we re-create the array, we make sure that `alwaysShow` updates are applied.
610
- quickpick . items =
611
- updated && quickpick . items === consideredItems ? [ ...consideredItems ] : consideredItems ;
612
-
613
646
if ( ! value ?. length ) {
614
647
// Nothing to search
615
648
this . updateItemsDebouncer . cancel ( ) ;
649
+ // Restore original list if search mode was active and input was cleared
650
+ if ( context . isSearching ) {
651
+ context . isSearching = false ;
652
+ quickpick . busy = true ;
653
+ quickpick . items = [ ] ;
654
+ quickpick . placeholder = 'Restoring your pull requests...' ;
655
+ await updateItems ( quickpick ) ;
656
+ quickpick . busy = false ;
657
+ // Restore category/divider items if not in search mode and input was cleared
658
+ } else {
659
+ const { items, placeholder } = getItemsAndPlaceholder ( ) ;
660
+ quickpick . placeholder = placeholder ;
661
+ quickpick . items = items ;
662
+ }
663
+
664
+ return true ;
665
+ }
666
+
667
+ // In API search mode
668
+ if ( context . isSearching ) {
669
+ if ( quickpick . activeItems . length === 0 ) {
670
+ // Show just the option to toggle search off if nothing found
671
+ quickpick . items = [ toggleSearchOnItem ] ;
672
+ } else {
673
+ // Search the API and update the quickpick
674
+ await updateItems ( quickpick , true ) ;
675
+ }
676
+
677
+ return true ;
678
+ }
679
+
680
+ // Out of API search mode
681
+ // This effectively hides the category/divider items
682
+ quickpick . items = getLaunchpadQuickPickItems ( context . result . items , true ) ;
683
+
684
+ // Show just the option to toggle search on if nothing found
685
+ if ( quickpick . activeItems . length === 0 && ! isSupportedLaunchpadPullRequestUrl ( value ) ) {
686
+ quickpick . items = [ toggleSearchOnItem ] ;
616
687
return true ;
617
688
}
618
689
690
+ // Match on some special cases like owner/repo, PR number, URL, etc.
619
691
// TODO: This needs to be generalized to work outside of GitHub,
620
692
// The current idea is that we should iterate the connected integrations and apply their parsing.
621
693
// Probably we even want to build a map like this: { integrationId: identity }
@@ -625,7 +697,9 @@ export class LaunchpadCommand extends QuickCommand<State> {
625
697
626
698
if ( prUrlIdentity . prNumber != null ) {
627
699
// We can identify the PR number, so let's try to find it locally:
628
- const launchpadItems = quickpick . items . filter ( ( i ) : i is LaunchpadItemQuickPickItem => 'item' in i ) ;
700
+ const launchpadItems = quickpick . items . filter (
701
+ ( i ) : i is LaunchpadItemQuickPickItem => 'item' in i && i . item ?. id != null ,
702
+ ) ;
629
703
let item = launchpadItems . find ( i =>
630
704
// perform strict match first
631
705
doesPullRequestSatisfyRepositoryURLIdentity ( i . item , prUrlIdentity ) ,
@@ -640,14 +714,21 @@ export class LaunchpadCommand extends QuickCommand<State> {
640
714
// Force quickpick to update by changing the items object:
641
715
quickpick . items = [ ...quickpick . items ] ;
642
716
}
643
- // We have found an item that matches to the URL.
644
- // Now it will be displayed as the found item and we exit this function now without sending any requests to API:
717
+ // We have found an item that matches as a special case.
645
718
this . updateItemsDebouncer . cancel ( ) ;
646
719
return true ;
647
720
}
648
721
}
649
722
650
- await updateItems ( quickpick , true ) ;
723
+ // If a supported PR URL was entered but no existing items match outside of search mode, turn on search mode and search the API.
724
+ if ( isSupportedLaunchpadPullRequestUrl ( value ) ) {
725
+ context . isSearching = true ;
726
+ await updateItems ( quickpick , true ) ;
727
+ } else if ( quickpick . activeItems . length === 0 || quickpick . activeItems [ 0 ] === toggleSearchOnItem ) {
728
+ // Show just the option to toggle search on if nothing found
729
+ quickpick . items = [ ...quickpick . items , toggleSearchOnItem ] ;
730
+ }
731
+
651
732
return true ;
652
733
} ,
653
734
onDidClickButton : async ( quickpick , button ) => {
@@ -766,7 +847,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
766
847
return StepResultBreak ;
767
848
}
768
849
const element = selection [ 0 ] ;
769
- if ( isConnectMoreIntegrationsItem ( element ) ) {
850
+ if ( isConnectMoreIntegrationsItem ( element ) || isToggleSearchItem ( element ) ) {
770
851
return element ;
771
852
}
772
853
return { ...element . item , group : element . group } ;
@@ -1510,3 +1591,15 @@ function updateTelemetryContext(context: Context) {
1510
1591
function isLaunchpadTargetActionQuickPickItem ( item : any ) : item is QuickPickItemOfT < LaunchpadTargetAction > {
1511
1592
return item ?. item ?. action != null && item ?. item ?. target != null ;
1512
1593
}
1594
+
1595
+ function isGitHubPullRequestUrl ( search : string ) {
1596
+ return search . includes ( 'github.com' ) && search . includes ( '/pull/' ) ;
1597
+ }
1598
+
1599
+ function isGitLabPullRequestUrl ( search : string ) {
1600
+ return search . includes ( 'gitlab.com' ) && search . includes ( '/merge_requests/' ) ;
1601
+ }
1602
+
1603
+ function isSupportedLaunchpadPullRequestUrl ( search : string ) {
1604
+ return isGitHubPullRequestUrl ( search ) || isGitLabPullRequestUrl ( search ) ;
1605
+ }
0 commit comments