Skip to content

support extensions contributing actions to the search results info bar

Administrator requested to merge contribute-to-search-page into master

Created by: sqs

This lets extensions and other encapsulated code add buttons or information-display actions right above the search results. Example use cases:

  • "Replace" for codemod
  • "Statistics" for language statistics

Sample screenshot (this PR just adds the capability to add things like the Replace button, it doesn't actually add that button):

image

Example code to add that (you can also do this in an extension):

diff --git a/web/src/Layout.tsx b/web/src/Layout.tsx
index 7c419c624..99931fd55 100644
--- a/web/src/Layout.tsx
+++ b/web/src/Layout.tsx
@@ -1,5 +1,5 @@
 import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
-import React, { Suspense } from 'react'
+import React, { Suspense, useEffect } from 'react'
 import { Redirect, Route, RouteComponentProps, Switch } from 'react-router'
 import { Observable } from 'rxjs'
 import { ActivationProps } from '../../shared/src/components/activation/Activation'
@@ -41,6 +41,7 @@ import { UserAreaHeaderNavItem } from './user/area/UserAreaHeader'
 import { UserSettingsAreaRoute } from './user/settings/UserSettingsArea'
 import { UserSettingsSidebarItems } from './user/settings/UserSettingsSidebar'
 import { parseBrowserRepoURL } from './util/url'
+import { registerCodemodSearchContributions } from './enterprise/codemod/contributions/search'
 
 export interface LayoutProps
     extends RouteComponentProps<any>,
@@ -101,6 +102,11 @@ export const Layout: React.FunctionComponent<LayoutProps> = props => {
     const needsSiteInit = window.context.showOnboarding
     const isSiteInit = props.location.pathname === '/site-admin/init'
 
+    useEffect(() => {
+        const sub = registerCodemodSearchContributions(props)
+        return () => sub.unsubscribe()
+    }, [])
+
     useScrollToLocationHash(props.location)
     // Remove trailing slash (which is never valid in any of our URLs).
     if (props.location.pathname !== '/' && props.location.pathname.endsWith('/')) {
diff --git a/web/src/enterprise/codemod/contributions/search.ts b/web/src/enterprise/codemod/contributions/search.ts
new file mode 100644
index 000000000..d0b9b1ab0
--- /dev/null
+++ b/web/src/enterprise/codemod/contributions/search.ts
@@ -0,0 +1,61 @@
+import H from 'history'
+import { Subscription, Unsubscribable } from 'rxjs'
+import { parseContributionExpressions } from '../../../../../shared/src/api/client/services/contribution'
+import { ContributableMenu } from '../../../../../shared/src/api/protocol'
+import { ExtensionsControllerProps } from '../../../../../shared/src/extensions/controller'
+
+export const CODEMOD_PANEL_VIEW_ID = 'codemod'
+
+export function registerCodemodSearchContributions({
+    history,
+    extensionsController,
+}: {
+    history: H.History
+} & ExtensionsControllerProps<'services'>): Unsubscribable {
+    const subscriptions = new Subscription()
+
+    const REPLACE_ID = 'codemod.search.replace'
+    subscriptions.add(
+        extensionsController.services.commands.registerCommand({
+            command: REPLACE_ID,
+            run: () => {
+                const text = prompt('Enter replacement text:')
+                if (text !== null) {
+                    const params = new URLSearchParams(history.location.search)
+                    history.push({
+                        // TODO!(sqs):why is this commented out/necessary?
+                        //
+                        // ...TabsWithURLViewStatePersistence.urlForTabID(history.location, CODEMOD_PANEL_VIEW_ID),
+                        search: `${params}`,
+                    })
+                }
+                return Promise.resolve()
+            },
+        })
+    )
+    subscriptions.add(
+        extensionsController.services.contribution.registerContributions({
+            contributions: parseContributionExpressions({
+                actions: [
+                    {
+                        id: REPLACE_ID,
+                        title: 'Replace',
+                        category: 'Codemod',
+                        command: REPLACE_ID,
+                        actionItem: {
+                            label: 'Replace...',
+                            // TODO!(sqs): icon theme color doesn't update
+                            iconURL:
+                                "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath fill='%23a2b0cd' d='M11,6C12.38,6 13.63,6.56 14.54,7.46L12,10H18V4L15.95,6.05C14.68,4.78 12.93,4 11,4C7.47,4 4.57,6.61 4.08,10H6.1C6.56,7.72 8.58,6 11,6M16.64,15.14C17.3,14.24 17.76,13.17 17.92,12H15.9C15.44,14.28 13.42,16 11,16C9.62,16 8.37,15.44 7.46,14.54L10,12H4V18L6.05,15.95C7.32,17.22 9.07,18 11,18C12.55,18 14,17.5 15.14,16.64L20,21.5L21.5,20L16.64,15.14Z' /%3E%3C/svg%3E",
+                        },
+                    },
+                ],
+                menus: {
+                    [ContributableMenu.SearchResultsToolbar]: [{ action: REPLACE_ID }],
+                },
+            }),
+        })
+    )
+
+    return subscriptions
+}

Merge request reports

Loading