@@ -95,24 +95,50 @@ export interface LaunchpadItemQuickPickItem extends QuickPickItemOfT<LaunchpadIt
9595type ConnectMoreIntegrationsItem = QuickPickItem & {
9696 item : undefined ;
9797 group : undefined ;
98+ search : undefined ;
9899} ;
99100const connectMoreIntegrationsItem : ConnectMoreIntegrationsItem = {
100101 label : 'Connect an Additional Integration...' ,
101102 detail : 'Connect additional integrations to view their pull requests in Launchpad' ,
102103 item : undefined ,
103104 group : undefined ,
105+ search : undefined ,
104106} ;
105107function isConnectMoreIntegrationsItem ( item : unknown ) : item is ConnectMoreIntegrationsItem {
106108 return item === connectMoreIntegrationsItem ;
107109}
108110
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+
109134interface Context {
110135 result : LaunchpadCategorizedResult ;
111136
112137 title : string ;
113138 collapsed : Map < LaunchpadGroup , boolean > ;
114139 telemetryContext : LaunchpadTelemetryContext | undefined ;
115140 connectedIntegrations : Map < IntegrationId , boolean > ;
141+ isSearching : boolean ;
116142}
117143
118144interface GroupedLaunchpadItem extends LaunchpadItem {
@@ -151,6 +177,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
151177 private readonly updateItemsDebouncer = createAsyncDebouncer ( 500 ) ;
152178 private readonly source : Source ;
153179 private readonly telemetryContext : LaunchpadTelemetryContext | undefined ;
180+ private savedSearch : string | undefined ;
154181
155182 constructor ( container : Container , args ?: LaunchpadCommandArgs ) {
156183 super ( container , 'launchpad' , 'launchpad' , `GitLens Launchpad\u00a0\u00a0${ proBadge } ` , {
@@ -223,6 +250,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
223250 collapsed : collapsed ,
224251 telemetryContext : this . telemetryContext ,
225252 connectedIntegrations : await this . container . launchpad . getConnectedIntegrations ( ) ,
253+ isSearching : false ,
226254 } ;
227255
228256 let opened = false ;
@@ -302,6 +330,12 @@ export class LaunchpadCommand extends QuickCommand<State> {
302330 newlyConnected = Boolean ( connected ) ;
303331 await updateContextItems ( this . container , context , { force : newlyConnected } ) ;
304332 continue ;
333+ } else if ( isToggleSearchItem ( result ) ) {
334+ context . isSearching = result . search ;
335+ if ( ! context . isSearching ) {
336+ this . updateItemsDebouncer . cancel ( ) ;
337+ }
338+ continue ;
305339 }
306340
307341 state . item = result ;
@@ -372,7 +406,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
372406 state : StepState < State > ,
373407 context : Context ,
374408 { picked, selectTopItem } : { picked ?: string ; selectTopItem ?: boolean } ,
375- ) : StepResultGenerator < GroupedLaunchpadItem | ConnectMoreIntegrationsItem > {
409+ ) : StepResultGenerator < GroupedLaunchpadItem | ConnectMoreIntegrationsItem | ToggleSearchItem > {
376410 const hasDisconnectedIntegrations = [ ...context . connectedIntegrations . values ( ) ] . some ( c => ! c ) ;
377411
378412 const buildGroupHeading = (
@@ -466,6 +500,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
466500 | LaunchpadItemQuickPickItem
467501 | DirectiveQuickPickItem
468502 | ConnectMoreIntegrationsItem
503+ | ToggleSearchItem
469504 ) [ ] = [ ] ;
470505
471506 if ( items . length ) {
@@ -477,7 +512,11 @@ export class LaunchpadCommand extends QuickCommand<State> {
477512 uiGroups . get ( 'blocked' ) ?. [ 0 ] ||
478513 uiGroups . get ( 'follow-up' ) ?. [ 0 ] ||
479514 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+
481520 if ( ! groupItems . length ) continue ;
482521
483522 if ( ! isSearching ) {
@@ -488,7 +527,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
488527 }
489528
490529 groupedAndSorted . push (
491- ...groupItems . map ( i => buildLaunchpadQuickPickItem ( i , ui , topItem , isSearching ) ) ,
530+ ...groupItems . map ( i => buildLaunchpadQuickPickItem ( i , ui , topItem , context . isSearching ) ) ,
492531 ) ;
493532 }
494533 }
@@ -497,6 +536,8 @@ export class LaunchpadCommand extends QuickCommand<State> {
497536 } ;
498537
499538 function getItemsAndPlaceholder ( isSearching ?: boolean ) {
539+ const toggleSearchItem = context . isSearching ? toggleSearchOffItem : toggleSearchOnItem ;
540+
500541 if ( context . result . error != null ) {
501542 return {
502543 placeholder : `Unable to load items (${
@@ -513,34 +554,22 @@ export class LaunchpadCommand extends QuickCommand<State> {
513554 if ( ! context . result . items . length ) {
514555 return {
515556 placeholder : 'All done! Take a vacation' ,
516- items : [ createDirectiveQuickPickItem ( Directive . Cancel , undefined , { label : 'OK' } ) ] ,
557+ items : [ toggleSearchItem ] ,
517558 } ;
518559 }
519560
520561 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 ) ] ,
523566 } ;
524567 }
525568
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-
542569 const updateItems = async (
543- quickpick : QuickPick < LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem > ,
570+ quickpick : QuickPick <
571+ LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem | ToggleSearchItem
572+ > ,
544573 force ?: boolean ,
545574 ) => {
546575 const search = quickpick . value ;
@@ -550,15 +579,15 @@ export class LaunchpadCommand extends QuickCommand<State> {
550579 await updateContextItems (
551580 this . container ,
552581 context ,
553- { force : force , search : search } ,
582+ { force : force , search : context . isSearching ? search : undefined } ,
554583 cancellationToken ,
555584 ) ;
556585 if ( cancellationToken . isCancellationRequested ) {
557586 return ;
558587 }
559588 const { items, placeholder } = getItemsAndPlaceholder ( Boolean ( search ) ) ;
560589 quickpick . placeholder = placeholder ;
561- quickpick . items = search ? combineQuickpickItemsWithSearchResults ( quickpick . items , items ) : items ;
590+ quickpick . items = items ;
562591 } ) ;
563592 } finally {
564593 quickpick . busy = false ;
@@ -568,7 +597,9 @@ export class LaunchpadCommand extends QuickCommand<State> {
568597 // Should only be used for optimistic update of the list when some UI property (like pinned, snoozed) changed with an
569598 // item. For all other cases, use updateItems.
570599 const optimisticallyUpdateItems = (
571- quickpick : QuickPick < LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem > ,
600+ quickpick : QuickPick <
601+ LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem | ToggleSearchItem
602+ > ,
572603 ) => {
573604 quickpick . items = getLaunchpadQuickPickItems (
574605 context . result . items ,
@@ -577,7 +608,6 @@ export class LaunchpadCommand extends QuickCommand<State> {
577608 } ;
578609
579610 const { items, placeholder } = getItemsAndPlaceholder ( ) ;
580- const nonGroupedItems = items . filter ( i => ! isDirectiveQuickPickItem ( i ) ) ;
581611
582612 const step = createPickStep ( {
583613 title : context . title ,
@@ -592,30 +622,72 @@ export class LaunchpadCommand extends QuickCommand<State> {
592622 LaunchpadSettingsQuickInputButton ,
593623 RefreshQuickInputButton ,
594624 ] ,
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+ } ,
595633 onDidChangeValue : async quickpick => {
596634 const { value } = quickpick ;
597- const hideGroups = Boolean ( value ?. length ) ;
598- const consideredItems = hideGroups ? nonGroupedItems : items ;
635+ this . savedSearch = value ;
599636
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+ ) ) {
602641 if ( item . alwaysShow ) {
603642 item . alwaysShow = false ;
604- updated = true ;
605643 }
606644 }
607645
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-
613646 if ( ! value ?. length ) {
614647 // Nothing to search
615648 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 ] ;
616687 return true ;
617688 }
618689
690+ // Match on some special cases like owner/repo, PR number, URL, etc.
619691 // TODO: This needs to be generalized to work outside of GitHub,
620692 // The current idea is that we should iterate the connected integrations and apply their parsing.
621693 // Probably we even want to build a map like this: { integrationId: identity }
@@ -625,7 +697,9 @@ export class LaunchpadCommand extends QuickCommand<State> {
625697
626698 if ( prUrlIdentity . prNumber != null ) {
627699 // 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+ ) ;
629703 let item = launchpadItems . find ( i =>
630704 // perform strict match first
631705 doesPullRequestSatisfyRepositoryURLIdentity ( i . item , prUrlIdentity ) ,
@@ -640,14 +714,21 @@ export class LaunchpadCommand extends QuickCommand<State> {
640714 // Force quickpick to update by changing the items object:
641715 quickpick . items = [ ...quickpick . items ] ;
642716 }
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.
645718 this . updateItemsDebouncer . cancel ( ) ;
646719 return true ;
647720 }
648721 }
649722
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+
651732 return true ;
652733 } ,
653734 onDidClickButton : async ( quickpick , button ) => {
@@ -766,7 +847,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
766847 return StepResultBreak ;
767848 }
768849 const element = selection [ 0 ] ;
769- if ( isConnectMoreIntegrationsItem ( element ) ) {
850+ if ( isConnectMoreIntegrationsItem ( element ) || isToggleSearchItem ( element ) ) {
770851 return element ;
771852 }
772853 return { ...element . item , group : element . group } ;
@@ -1510,3 +1591,15 @@ function updateTelemetryContext(context: Context) {
15101591function isLaunchpadTargetActionQuickPickItem ( item : any ) : item is QuickPickItemOfT < LaunchpadTargetAction > {
15111592 return item ?. item ?. action != null && item ?. item ?. target != null ;
15121593}
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