remove enableLegacyExtensions and all now-unreachable code (#47517)

This is part of the extension API deprecation
(https://docs.google.com/document/d/10vtoe-kpNvVZ8Etrx34bSCoTaCCHxX8o3ncCmuErPZo/edit).

There are a lot more refactors to do after this, but this removes the
admin-facing `enableLegacyExtensions` setting and some of the root-level
instantiation and usage of the extensions controller API.

## Test plan

Check that hovers still work on dotcom.
This commit is contained in:
Quinn Slack 2023-02-25 10:28:43 -08:00 committed by GitHub
parent 7e395819c4
commit e99c4145f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 173 additions and 3679 deletions

View File

@ -100,6 +100,7 @@ All notable changes to Sourcegraph are documented in this file.
- The Code insights "run over all repositories" mode has been replaced with search-powered repositories filed syntax. [#45687](https://github.com/sourcegraph/sourcegraph/pull/45687)
- The settings `search.repositoryGroups`, `codeInsightsGqlApi`, `codeInsightsAllRepos`, `experimentalFeatures.copyQueryButton`,, `experimentalFeatures.showRepogroupHomepage`, `experimentalFeatures.showOnboardingTour`, `experimentalFeatures.showSearchContextManagement` and `codeIntelligence.autoIndexRepositoryGroups` have been removed as they were deprecated and unsued. [#47481](https://github.com/sourcegraph/sourcegraph/pull/47481)
- The site config `enableLegacyExtensions` setting was removed. It is no longer possible to enable legacy Sourcegraph extension API functionality in this version.
## 4.4.2

View File

@ -102,11 +102,6 @@ describe('GitHub', () => {
siteAdmin: false,
},
}),
EnableLegacyExtensions: () => ({
site: {
enableLegacyExtensions: true,
},
}),
})
// Ensure that the same assets are requested in all environments.
@ -155,10 +150,7 @@ describe('GitHub', () => {
// it('shows hover tooltips when hovering a token and respects "Enable single click to go to definition" setting', async () => {
// mockUrls(['https://github.com/*path/find-definition'])
// const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking({
// pollyServer: testContext.server,
// sourcegraphBaseUrl: driver.sourcegraphBaseUrl,
// })
// const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking()
// const userSettings: Settings = {
// extensions: extensionSettings,
@ -316,16 +308,14 @@ describe('GitHub', () => {
// })
describe('Pull request pages', () => {
describe('Files Changed view', () => {
// TODO(sqs): skipped because these have not been reimplemented after the extension API deprecation
describe.skip('Files Changed view', () => {
// For each pull request test, set up a mock extension that verifies that the correct
// file and revision info reach extensions.
beforeEach(() => {
mockUrls(['https://github.com/*path/find-definition'])
const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking({
pollyServer: testContext.server,
sourcegraphBaseUrl: driver.sourcegraphBaseUrl,
})
const { mockExtension, extensionSettings } = setupExtensionMocking()
const userSettings: Settings = {
extensions: extensionSettings,
@ -350,7 +340,6 @@ describe('GitHub', () => {
merged: { contents: JSON.stringify(userSettings), messages: [] },
},
}),
Extensions,
ResolveRev: ({ revision }) => ({
repository: {
mirrorInfo: { cloned: true },
@ -639,7 +628,8 @@ describe('GitHub', () => {
})
})
describe('Commit view', () => {
// TODO(sqs): skipped because these have not been reimplemented after the extension API deprecation
describe.skip('Commit view', () => {
beforeEach(() => {
mockUrls([
'https://github.com/*path/find-definition',
@ -647,10 +637,7 @@ describe('GitHub', () => {
'https://github.com/commits/badges',
])
const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking({
pollyServer: testContext.server,
sourcegraphBaseUrl: driver.sourcegraphBaseUrl,
})
const { mockExtension, extensionSettings } = setupExtensionMocking()
const userSettings: Settings = {
extensions: extensionSettings,
@ -691,7 +678,6 @@ describe('GitHub', () => {
},
},
}),
Extensions,
})
// Serve a mock extension with a simple hover provider

View File

@ -91,11 +91,6 @@ describe('GitLab', () => {
hasCodeIntelligence: true,
},
}),
EnableLegacyExtensions: () => ({
site: {
enableLegacyExtensions: true,
},
}),
})
// Ensure that the same assets are requested in all environments.
@ -146,11 +141,9 @@ describe('GitLab', () => {
})
})
it('shows hover tooltips when hovering a token', async () => {
const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking({
pollyServer: testContext.server,
sourcegraphBaseUrl: driver.sourcegraphBaseUrl,
})
// TODO(sqs): skipped because these have not been reimplemented after the extension API deprecation
it.skip('shows hover tooltips when hovering a token', async () => {
const { mockExtension, extensionSettings } = setupExtensionMocking()
const userSettings: Settings = {
extensions: extensionSettings,
@ -175,7 +168,6 @@ describe('GitLab', () => {
merged: { contents: JSON.stringify(userSettings), messages: [] },
},
}),
Extensions,
})
// Serve a mock extension with a simple hover provider

View File

@ -1,5 +1,5 @@
import { combineLatest, ReplaySubject, of } from 'rxjs'
import { map, switchMap } from 'rxjs/operators'
import { combineLatest, ReplaySubject } from 'rxjs'
import { map } from 'rxjs/operators'
import { asError } from '@sourcegraph/common'
import { isHTTPAuthError } from '@sourcegraph/http-client'
@ -8,13 +8,11 @@ import { mutateSettings, updateSettings } from '@sourcegraph/shared/src/settings
import { EMPTY_SETTINGS_CASCADE, gqlToCascade, SettingsSubject } from '@sourcegraph/shared/src/settings/settings'
import { toPrettyBlobURL } from '@sourcegraph/shared/src/util/url'
import { background } from '../../browser-extension/web-extension-api/runtime'
import { createGraphQLHelpers } from '../backend/requestGraphQl'
import { CodeHost } from '../code-hosts/shared/codeHost'
import { isInPage } from '../context'
import { createExtensionHost } from './extensionHost'
import { getInlineExtensions, shouldUseInlineExtensions } from './inlineExtensionsService'
import { getInlineExtensions } from './inlineExtensionsService'
import { editClientSettings, fetchViewerSettings, mergeCascades, storageSettingsCascade } from './settings'
export interface SourcegraphIntegrationURLs {
@ -57,8 +55,6 @@ export function createPlatformContext(
}>(1)
const { requestGraphQL, getBrowserGraphQLClient } = createGraphQLHelpers(sourcegraphURL, isExtension)
const shouldUseInlineExtensionsObservable = shouldUseInlineExtensions(requestGraphQL)
const context: BrowserPlatformContext = {
/**
* The active settings cascade.
@ -120,31 +116,6 @@ export function createPlatformContext(
requestGraphQL,
getGraphQLClient: getBrowserGraphQLClient,
createExtensionHost: () => createExtensionHost({ assetsURL }),
getScriptURLForExtension: () => {
if (isInPage) {
return undefined
}
return bundleURLs =>
shouldUseInlineExtensionsObservable.toPromise().then(shouldUseInlineExtensions => {
if (shouldUseInlineExtensions) {
// inline extensions have fixed scriptURLs
return bundleURLs
}
// We need to import the extension's JavaScript file (in importScripts in the Web Worker) from a blob:
// URI, not its original http:/https: URL, because Chrome extensions are not allowed to be published
// with a CSP that allowlists https://* in script-src (see
// https://developer.chrome.com/extensions/contentSecurityPolicy#relaxing-remote-script). (Firefox
// add-ons have an even stricter restriction.)
return Promise.allSettled(bundleURLs.map(bundleURL => background.createBlobURL(bundleURL))).then(
results =>
results.map(result =>
result.status === 'rejected' ? asError(result.reason) : result.value
)
)
})
},
urlToFile: ({ rawRepoName, ...target }, context) => {
// We don't always resolve the rawRepoName, e.g. if there are multiple definitions.
// Construct URL to file on code host, if possible.
@ -156,10 +127,7 @@ export function createPlatformContext(
},
sourcegraphURL,
clientApplication: 'other',
getStaticExtensions: () =>
shouldUseInlineExtensionsObservable.pipe(
switchMap(shouldUseInline => (shouldUseInline ? getInlineExtensions() : of(undefined)))
),
getStaticExtensions: () => getInlineExtensions(),
}
return context
}

View File

@ -1,60 +1,10 @@
import { Observable, from } from 'rxjs'
import { map } from 'rxjs/operators'
import { checkOk, isErrorGraphQLResult, gql } from '@sourcegraph/http-client'
import { checkOk } from '@sourcegraph/http-client'
import { ExecutableExtension } from '@sourcegraph/shared/src/api/extension/activation'
import { ExtensionManifest } from '@sourcegraph/shared/src/extensions/extensionManifest'
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import extensions from '../../../code-intel-extensions.json'
import { EnableLegacyExtensionsResult } from '../../graphql-operations'
const DEFAULT_ENABLE_LEGACY_EXTENSIONS = false
/**
* Determine which extensions should be loaded:
* - inline (bundled with the browser extension)
* - or from the extensions registry (if `enableLegacyExtensions` experimental feature value is set to `true`).
*/
export const shouldUseInlineExtensions = (requestGraphQL: PlatformContext['requestGraphQL']): Observable<boolean> =>
requestGraphQL<EnableLegacyExtensionsResult>({
request: gql`
query EnableLegacyExtensions {
site {
enableLegacyExtensions
}
}
`,
variables: {},
mightContainPrivateInfo: false,
}).pipe(
map(result => {
if (isErrorGraphQLResult(result)) {
// EnableLegacyExtensions query resolver may not be implemented on older versions.
// Return `true` by default.
return true
}
try {
const enableLegacyExtensions = result.data.site.enableLegacyExtensions
return typeof enableLegacyExtensions === 'undefined'
? DEFAULT_ENABLE_LEGACY_EXTENSIONS
: enableLegacyExtensions
} catch {
return DEFAULT_ENABLE_LEGACY_EXTENSIONS
}
}),
map(enableLegacyExtensions => {
// TODO: The Phabricator native extension is currently the only runtime that runs the
// browser extension code but does not use bundled extensions yet. We will fix this
// when we update the browser extensions to use the new code intel APIs (#42104).
if (window.SOURCEGRAPH_PHABRICATOR_EXTENSION === true) {
return false
}
return !enableLegacyExtensions
})
)
/**
* Get the manifest URL and script URL for a Sourcegraph extension which is inline (bundled with the browser add-on).

View File

@ -44,13 +44,7 @@ export async function createExtensionHostClientConnection(
initData: Omit<InitData, 'initialSettings'>,
platformContext: Pick<
PlatformContext,
| 'settings'
| 'updateSettings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'telemetryService'
| 'getScriptURLForExtension'
| 'clientApplication'
'settings' | 'updateSettings' | 'getGraphQLClient' | 'requestGraphQL' | 'telemetryService' | 'clientApplication'
>
): Promise<{
subscription: Unsubscribable

View File

@ -1,90 +0,0 @@
import { isEqual, once } from 'lodash'
import { combineLatest, from, Observable, throwError } from 'rxjs'
import { catchError, distinctUntilChanged, map, publishReplay, refCount, shareReplay, switchMap } from 'rxjs/operators'
import { asError } from '@sourcegraph/common'
import { ConfiguredExtension, extensionIDsFromSettings, isExtensionEnabled } from '../../extensions/extension'
import { areExtensionsSame } from '../../extensions/extensions'
import { queryConfiguredRegistryExtensions } from '../../extensions/helpers'
import { PlatformContext } from '../../platform/context'
import { isSettingsValid } from '../../settings/settings'
/**
* @returns An observable that emits the list of extensions configured in the viewer's final settings upon
* subscription and each time it changes.
*/
function viewerConfiguredExtensions({
settings,
getGraphQLClient,
}: Pick<PlatformContext, 'settings' | 'getGraphQLClient'>): Observable<ConfiguredExtension[]> {
return from(settings).pipe(
map(settings => extensionIDsFromSettings(settings)),
distinctUntilChanged((a, b) => isEqual(a, b)),
switchMap(extensionIDs => queryConfiguredRegistryExtensions({ getGraphQLClient }, extensionIDs)),
catchError(error => throwError(asError(error))),
// TODO: Restore reference counter after refactoring contributions service
// to not unsubscribe from existing entries when new entries are registered,
// in order to ensure that the source is unsubscribed from.
shareReplay(1)
)
}
/**
* List of extensions migrated to the core workflow.
*/
export const MIGRATED_TO_CORE_WORKFLOW_EXTENSION_IDS = new Set([
'sourcegraph/git-extras',
'sourcegraph/search-export',
'sourcegraph/open-in-editor',
'sourcegraph/open-in-vscode',
'dymka/open-in-webstorm',
'sourcegraph/open-in-atom',
])
/**
* Returns an Observable of extensions enabled for the user.
* Wrapped with the `once` function from lodash.
*/
export const getEnabledExtensions = once(
(
context: Pick<
PlatformContext,
'settings' | 'getGraphQLClient' | 'getScriptURLForExtension' | 'clientApplication'
>
): Observable<ConfiguredExtension[]> =>
combineLatest([viewerConfiguredExtensions(context), context.settings]).pipe(
map(([configuredExtensions, settings]) => {
const enableGoImportsSearchQueryTransform =
isSettingsValid(settings) &&
settings.final.experimentalFeatures?.enableGoImportsSearchQueryTransform
return configuredExtensions.filter(extension => {
const extensionsAsCoreFeatureMigratedExtension = MIGRATED_TO_CORE_WORKFLOW_EXTENSION_IDS.has(
extension.id
)
// Ignore extensions migrated to the core workflow if the experimental feature is enabled
if (context.clientApplication === 'sourcegraph' && extensionsAsCoreFeatureMigratedExtension) {
return false
}
// Go import search query transform is enabled by default but can be disabled by the setting
const enableGoImportsSearchQueryTransformMigratedExtension =
(enableGoImportsSearchQueryTransform === undefined || enableGoImportsSearchQueryTransform) &&
extension.id === 'go-imports-search'
// Ignore loading the go-imports-search extension when the migrated go imports search is enabled
if (
context.clientApplication === 'sourcegraph' &&
enableGoImportsSearchQueryTransformMigratedExtension
) {
return false
}
return isExtensionEnabled(settings.final, extension.id)
})
}),
distinctUntilChanged((a, b) => areExtensionsSame(a, b)),
publishReplay(1),
refCount()
)
)

View File

@ -22,18 +22,12 @@ describe('MainThreadAPI', () => {
const platformContext: Pick<
PlatformContext,
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'getScriptURLForExtension'
| 'clientApplication'
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
> = {
settings: EMPTY,
getGraphQLClient,
updateSettings: () => Promise.resolve(),
requestGraphQL,
getScriptURLForExtension: () => undefined,
clientApplication: 'other',
}
@ -61,18 +55,12 @@ describe('MainThreadAPI', () => {
const platformContext: Pick<
PlatformContext,
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'getScriptURLForExtension'
| 'clientApplication'
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
> = {
settings: EMPTY,
getGraphQLClient,
updateSettings: () => Promise.resolve(),
requestGraphQL,
getScriptURLForExtension: () => undefined,
clientApplication: 'other',
}
@ -93,12 +81,7 @@ describe('MainThreadAPI', () => {
}
const platformContext: Pick<
PlatformContext,
| 'updateSettings'
| 'settings'
| 'requestGraphQL'
| 'getGraphQLClient'
| 'getScriptURLForExtension'
| 'clientApplication'
'updateSettings' | 'settings' | 'requestGraphQL' | 'getGraphQLClient' | 'clientApplication'
> = {
settings: of({
subjects: [
@ -131,7 +114,6 @@ describe('MainThreadAPI', () => {
updateSettings,
getGraphQLClient,
requestGraphQL: () => EMPTY,
getScriptURLForExtension: () => undefined,
clientApplication: 'other',
}
@ -161,18 +143,12 @@ describe('MainThreadAPI', () => {
const platformContext: Pick<
PlatformContext,
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'getScriptURLForExtension'
| 'clientApplication'
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
> = {
getGraphQLClient,
settings: of(...values),
updateSettings: () => Promise.resolve(),
requestGraphQL: () => EMPTY,
getScriptURLForExtension: () => undefined,
clientApplication: 'other',
}
@ -193,18 +169,12 @@ describe('MainThreadAPI', () => {
const values = new Subject<SettingsCascade<{ a: string }>>()
const platformContext: Pick<
PlatformContext,
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'getScriptURLForExtension'
| 'clientApplication'
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
> = {
settings: values.asObservable(),
updateSettings: () => Promise.resolve(),
getGraphQLClient,
requestGraphQL: () => EMPTY,
getScriptURLForExtension: () => undefined,
clientApplication: 'other',
}
const passedToExtensionHost: SettingsCascade<object>[] = []

View File

@ -13,7 +13,6 @@ import { proxySubscribable } from '../extension/api/common'
import { NotificationType, PlainNotification } from '../extension/extensionHostApi'
import { ProxySubscription } from './api/common'
import { getEnabledExtensions } from './enabledExtensions'
import { updateSettings } from './services/settings'
/** A registered command in the command registry. */
@ -65,7 +64,6 @@ export const initMainThreadAPI = (
| 'requestGraphQL'
| 'showMessage'
| 'showInputBox'
| 'getScriptURLForExtension'
| 'getStaticExtensions'
| 'telemetryService'
| 'clientApplication'
@ -149,14 +147,6 @@ export const initMainThreadAPI = (
showInputBox: options =>
platformContext.showInputBox ? platformContext.showInputBox(options) : defaultShowInputBox(options),
getScriptURLForExtension: () => {
const getScriptURL = platformContext.getScriptURLForExtension()
if (!getScriptURL) {
return undefined
}
return proxy(getScriptURL)
},
getEnabledExtensions: () => {
if (platformContext.getStaticExtensions) {
return proxySubscribable(
@ -164,15 +154,13 @@ export const initMainThreadAPI = (
.getStaticExtensions()
.pipe(
switchMap(staticExtensions =>
staticExtensions
? of(staticExtensions).pipe(publishReplay(1), refCount())
: getEnabledExtensions(platformContext)
staticExtensions ? of(staticExtensions).pipe(publishReplay(1), refCount()) : of([])
)
)
)
}
return proxySubscribable(getEnabledExtensions(platformContext))
return proxySubscribable(of([]))
},
logEvent: (eventName, eventProperties) => platformContext.telemetryService?.log(eventName, eventProperties),
logExtensionMessage: (...data) => logger.log(...data),

View File

@ -1,29 +1,18 @@
// For search-related extension API features, such as query transformers
import { Remote } from 'comlink'
import { from, Observable, of, TimeoutError } from 'rxjs'
import { catchError, filter, first, switchMap, timeout } from 'rxjs/operators'
import { Observable, of } from 'rxjs'
import { logger } from '@sourcegraph/common'
import { FlatExtensionHostAPI } from '../contract'
import { SharedEventLogger } from '../sharedEventLogger'
import { wrapRemoteObservable } from './api/common'
const TRANSFORM_QUERY_TIMEOUT = 3000
/**
* Executes search query transformers contributed by Sourcegraph extensions.
*/
export function transformSearchQuery({
query,
extensionHostAPIPromise,
enableGoImportsSearchQueryTransform,
eventLogger,
}: {
query: string
extensionHostAPIPromise: null | Promise<Remote<FlatExtensionHostAPI>>
enableGoImportsSearchQueryTransform: undefined | boolean
eventLogger: SharedEventLogger
}): Observable<string> {
@ -33,37 +22,7 @@ export function transformSearchQuery({
query = goImportsTransform(query, eventLogger)
}
if (extensionHostAPIPromise === null) {
return of(query)
}
return from(extensionHostAPIPromise).pipe(
switchMap(extensionHostAPI =>
// Since we won't re-compute on subsequent extension activation, ensure that
// at least the initial set of extensions, which should include always-activated
// query-transforming extensions, have been loaded to ensure that the initial
// search query is transformed
wrapRemoteObservable(extensionHostAPI.haveInitialExtensionsLoaded()).pipe(
filter(haveLoaded => haveLoaded),
first(), // Ensure that it only emits once
switchMap(() =>
wrapRemoteObservable(extensionHostAPI.transformSearchQuery(query)).pipe(
first() // Ensure that it only emits once
)
)
)
),
// Timeout: if this is hanging due to any sort of extension bug, it may not result in a thrown error,
// but will degrade search UX.
// Wait up to 5 seconds and log to console for users to debug slow query transformer extensions
timeout(TRANSFORM_QUERY_TIMEOUT),
catchError(error => {
if (error instanceof TimeoutError) {
logger.error(`Extension query transformers took more than ${TRANSFORM_QUERY_TIMEOUT}ms`)
}
return of(query)
})
)
return of(query)
}
function goImportsTransform(query: string, eventLogger: SharedEventLogger): string {

View File

@ -4,7 +4,7 @@ import { DocumentHighlight } from 'sourcegraph'
import { Contributions, Evaluated, Raw, TextDocumentPositionParameters, HoverMerged } from '@sourcegraph/client-api'
import { MaybeLoadingResult } from '@sourcegraph/codeintellify'
import { DeepReplace, ErrorLike } from '@sourcegraph/common'
import { DeepReplace } from '@sourcegraph/common'
import * as clientType from '@sourcegraph/extension-api-types'
import { GraphQLResult } from '@sourcegraph/http-client'
@ -196,10 +196,6 @@ export interface MainThreadAPI {
showMessage: (message: string) => Promise<void>
showInputBox: (options?: InputBoxOptions) => Promise<string | undefined>
getScriptURLForExtension: () =>
| undefined
| (((bundleURLs: string[]) => Promise<(string | ErrorLike)[]>) & ProxyMarked)
getEnabledExtensions: () => ProxySubscribable<(ConfiguredExtension | ExecutableExtension)[]>
/**

View File

@ -1,10 +1,10 @@
import { Remote } from 'comlink'
import { BehaviorSubject, combineLatest, from, Observable, Subscription } from 'rxjs'
import { BehaviorSubject, combineLatest, from, Observable, of, Subscription } from 'rxjs'
import { catchError, concatMap, distinctUntilChanged, first, map, switchMap, tap } from 'rxjs/operators'
import sourcegraph from 'sourcegraph'
import { Contributions } from '@sourcegraph/client-api'
import { asError, ErrorLike, isErrorLike, hashCode, memoizeObservable, logger } from '@sourcegraph/common'
import { asError, isErrorLike, hashCode, logger } from '@sourcegraph/common'
import { ConfiguredExtension, getScriptURLFromExtensionManifest, splitExtensionID } from '../../extensions/extension'
import { areExtensionsSame, getEnabledExtensionsForSubject } from '../../extensions/extensions'
@ -62,7 +62,7 @@ const DEPRECATED_EXTENSION_IDS = new Set(['sourcegraph/code-stats-insights', 'so
export function activateExtensions(
state: Pick<ExtensionHostState, 'activeExtensions' | 'contributions' | 'haveInitialExtensionsLoaded' | 'settings'>,
mainAPI: Remote<Pick<MainThreadAPI, 'getScriptURLForExtension' | 'logEvent'>>,
mainAPI: Remote<Pick<MainThreadAPI, 'logEvent'>>,
createExtensionAPI: (extensionID: string) => typeof sourcegraph,
mainThreadAPIInitializations: Observable<boolean>,
/**
@ -76,31 +76,12 @@ export function activateExtensions(
* */
deactivate = deactivateExtension
): Subscription {
const getScriptURLs = memoizeObservable(
() =>
mainThreadAPIInitializations.pipe(
first(initialized => initialized),
switchMap(() =>
from(mainAPI.getScriptURLForExtension()).pipe(
map(getScriptURL => {
function getBundleURLs(urls: string[]): Promise<(string | ErrorLike)[]> {
return getScriptURL ? getScriptURL(urls) : Promise.resolve(urls)
}
return getBundleURLs
})
)
)
),
() => 'getScriptURL'
)
const previouslyActivatedExtensions = new Set<string>()
const extensionContributions = new Map<string, Contributions>()
const contributionsToAdd = new Map<string, Contributions>()
const extensionsSubscription = combineLatest([state.activeExtensions, getScriptURLs(null)])
const extensionsSubscription = combineLatest([state.activeExtensions])
.pipe(
concatMap(([activeExtensions, getScriptURLs]) => {
concatMap(([activeExtensions]) => {
const toDeactivate = new Set<string>()
const toActivate = new Map<string, ConfiguredExtension | ExecutableExtension>()
const activeExtensionIDs = new Set<string>()
@ -126,32 +107,24 @@ export function activateExtensions(
}
}
return from(
getScriptURLs(
[...toActivate.values()].map(extension => {
if ('scriptURL' in extension) {
// This is already an executable extension (inline extension)
return extension.scriptURL
}
const scriptURLs = [...toActivate.values()].map(extension => {
if ('scriptURL' in extension) {
// This is already an executable extension (inline extension)
return extension.scriptURL
}
return getScriptURLFromExtensionManifest(extension)
})
).then(scriptURLs => {
// TODO: (not urgent) add scriptURL cache
return getScriptURLFromExtensionManifest(extension)
})
const executableExtensionsToActivate: ExecutableExtension[] = [...toActivate.values()]
.map((extension, index) => ({
id: extension.id,
manifest: extension.manifest,
scriptURL: scriptURLs[index],
}))
.filter(
(extension): extension is ExecutableExtension => typeof extension.scriptURL === 'string'
)
const executableExtensionsToActivate: ExecutableExtension[] = [...toActivate.values()]
.map((extension, index) => ({
id: extension.id,
manifest: extension.manifest,
scriptURL: scriptURLs[index],
}))
.filter((extension): extension is ExecutableExtension => typeof extension.scriptURL === 'string')
return { toActivate: executableExtensionsToActivate, toDeactivate }
})
).pipe(
return of({ toActivate: executableExtensionsToActivate, toDeactivate }).pipe(
tap(({ toActivate }) => {
for (const extension of toActivate) {
if (

View File

@ -16,8 +16,7 @@ describe('Extension activation', () => {
it('logs events for activated extensions', async () => {
const logEvent = sinon.spy()
const mockMain = pretendRemote<Pick<MainThreadAPI, 'getScriptURLForExtension' | 'logEvent'>>({
getScriptURLForExtension: () => undefined,
const mockMain = pretendRemote<Pick<MainThreadAPI, 'logEvent'>>({
logEvent,
})

View File

@ -1,4 +1,3 @@
import { proxy } from 'comlink'
import { BehaviorSubject } from 'rxjs'
import { SettingsCascade } from '../../../settings/settings'
@ -24,7 +23,6 @@ describe('ExtensionHost: Configuration', () => {
sourcegraphURL: 'https://example.com/',
},
pretendRemote<ClientAPI>({
getScriptURLForExtension: proxy(() => undefined),
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
})
)
@ -44,7 +42,6 @@ describe('ExtensionHost: Configuration', () => {
sourcegraphURL: 'https://example.com/',
},
pretendRemote<ClientAPI>({
getScriptURLForExtension: proxy(() => undefined),
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
})
)
@ -62,7 +59,6 @@ describe('ExtensionHost: Configuration', () => {
sourcegraphURL: 'https://example.com/',
},
pretendRemote<ClientAPI>({
getScriptURLForExtension: proxy(() => undefined),
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
})
)
@ -82,7 +78,6 @@ describe('ExtensionHost: Configuration', () => {
sourcegraphURL: 'https://example.com/',
},
pretendRemote<ClientAPI>({
getScriptURLForExtension: proxy(() => undefined),
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
})
)
@ -105,7 +100,6 @@ describe('ExtensionHost: Configuration', () => {
sourcegraphURL: 'https://example.com/',
},
pretendRemote<ClientAPI>({
getScriptURLForExtension: proxy(() => undefined),
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
applySettingsEdit: edit =>
Promise.resolve().then(() => {

View File

@ -15,7 +15,6 @@ describe('getDocumentHighlights from ExtensionHost API, it aims to have more e2e
// integration(ish) tests for scenarios not covered by providers tests
const noopMain = pretendRemote<ClientAPI>({
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
getScriptURLForExtension: () => undefined,
})
const initialSettings: SettingsCascade<object> = {
subjects: [],

View File

@ -16,7 +16,6 @@ describe('getHover from ExtensionHost API, it aims to have more e2e feel', () =>
// integration(ish) tests for scenarios not covered by providers tests
const noopMain = pretendRemote<ClientAPI>({
getEnabledExtensions: () => pretendProxySubscribable(of([])),
getScriptURLForExtension: () => undefined,
})
const initialSettings: SettingsCascade<object> = {
subjects: [],

View File

@ -11,7 +11,6 @@ import { initializeExtensionHostTest } from './test-helpers'
const noopMain = pretendRemote<ClientAPI>({
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
getScriptURLForExtension: () => undefined,
logExtensionMessage: (...data) => logger.log(...data),
})

View File

@ -10,7 +10,6 @@ import { initializeExtensionHostTest } from './test-helpers'
const noopMain = pretendRemote<ClientAPI>({
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
getScriptURLForExtension: () => undefined,
})
const initialSettings: SettingsCascade<object> = { subjects: [], final: {} }

View File

@ -5,13 +5,6 @@ import { TypedTypePolicies } from '../graphql-operations'
// Defines how the Apollo cache interacts with our GraphQL schema.
// See https://www.apollographql.com/docs/react/caching/cache-configuration/#typepolicy-fields
const typePolicies: TypedTypePolicies = {
ExtensionRegistry: {
// Replace existing `ExtensionRegistry` with the incoming value.
// Required because of the missing `id` on the `ExtensionRegistry` field.
merge(existing, incoming) {
return incoming
},
},
Person: {
// Replace existing `Person` with the incoming value.
// Required because of the missing `id` on the `Person` field.

View File

@ -1,66 +0,0 @@
import { from, Subscription } from 'rxjs'
import { switchMap } from 'rxjs/operators'
import type { InitData } from '../api/extension/extensionHost'
import { syncPromiseSubscription } from '../api/util'
import type { PlatformContext } from '../platform/context'
import type { Controller } from './controller'
/**
* Creates the controller, which handles all communication between the client application and extensions.
*
* There should only be a single controller for the entire client application. The controller's model represents
* all of the client application state that the client needs to know.
*
* The implementation (`createExtensionHostClientConnection`) is lazy loaded to avoid adding bytes when
* the extension system is disabled
*/
export function createController(
context: Pick<
PlatformContext,
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'showMessage'
| 'showInputBox'
| 'getScriptURLForExtension'
| 'getStaticExtensions'
| 'telemetryService'
| 'clientApplication'
| 'sourcegraphURL'
| 'createExtensionHost'
>
): Controller {
const subscriptions = new Subscription()
const initData: Omit<InitData, 'initialSettings'> = {
sourcegraphURL: context.sourcegraphURL,
clientApplication: context.clientApplication,
}
const extensionHostClientPromise = import('../api/client/connection').then(module =>
module.createExtensionHostClientConnection(context.createExtensionHost(), initData, context)
)
subscriptions.add(() => extensionHostClientPromise.then(({ subscription }) => subscription.unsubscribe()))
// TODO: Debug helpers, logging
return {
executeCommand: (parameters, suppressNotificationOnError) =>
extensionHostClientPromise.then(({ exposedToClient }) =>
exposedToClient.executeCommand(parameters, suppressNotificationOnError)
),
commandErrors: from(extensionHostClientPromise).pipe(
switchMap(({ exposedToClient }) => exposedToClient.commandErrors)
),
registerCommand: entryToRegister =>
syncPromiseSubscription(
extensionHostClientPromise.then(({ exposedToClient }) =>
exposedToClient.registerCommand(entryToRegister)
)
),
extHostAPI: extensionHostClientPromise.then(({ api }) => api),
unsubscribe: () => subscriptions.unsubscribe(),
}
}

View File

@ -26,7 +26,6 @@ export function createController(
| 'requestGraphQL'
| 'showMessage'
| 'showInputBox'
| 'getScriptURLForExtension'
| 'getStaticExtensions'
| 'telemetryService'
| 'clientApplication'

View File

@ -8,7 +8,7 @@ import { ExtensionManifest } from './extensionManifest'
* The default fields in the {@link ConfiguredExtension} manifest (i.e., the default value of the
* `K` type parameter).
*/
export const CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS = ['contributes', 'activationEvents', 'url'] as const
const CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS = ['contributes', 'activationEvents', 'url'] as const
export type ConfiguredExtensionManifestDefaultFields = typeof CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS[number]
/**

View File

@ -1,32 +0,0 @@
import { createGraphQLClientGetter } from '../testing/apollo/createGraphQLClientGetter'
import { ConfiguredExtension, ConfiguredExtensionManifestDefaultFields } from './extension'
import { ExtensionManifest } from './extensionManifest'
import { queryConfiguredRegistryExtensions } from './helpers'
const TEST_MANIFEST: Pick<ExtensionManifest, ConfiguredExtensionManifestDefaultFields | 'publisher'> = {
publisher: 'a',
url: 'https://example.com',
activationEvents: [],
}
describe('queryConfiguredRegistryExtensions', () => {
it('gets extensions from GraphQL servers supporting extensions(extensionIDs)', done => {
const extensionsMock = {
data: {
extensionRegistry: {
extensions: {
nodes: [{ extensionID: 'a/b', manifest: { jsonFields: TEST_MANIFEST } }],
},
},
},
}
const getGraphQLClient = createGraphQLClientGetter({ watchQueryMocks: [extensionsMock] })
queryConfiguredRegistryExtensions({ getGraphQLClient }, ['a/b']).subscribe(data => {
expect(data).toEqual([{ id: 'a/b', manifest: TEST_MANIFEST }] as ConfiguredExtension[])
done()
})
})
})

View File

@ -1,87 +0,0 @@
import { Observable, of } from 'rxjs'
import { map } from 'rxjs/operators'
import { createAggregateError } from '@sourcegraph/common'
import { fromObservableQueryPromise, getDocumentNode, gql } from '@sourcegraph/http-client'
import { ExtensionsResult, ExtensionsVariables } from '../graphql-operations'
import { PlatformContext } from '../platform/context'
import {
ConfiguredExtension,
ConfiguredExtensionManifestDefaultFields,
CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS,
} from './extension'
import { ExtensionManifest } from './extensionManifest'
const ExtensionsQuery = gql`
query Extensions($first: Int!, $extensionIDs: [String!]!, $extensionManifestFields: [String!]!) {
extensionRegistry {
extensions(first: $first, extensionIDs: $extensionIDs) {
nodes {
id
extensionID
manifest {
jsonFields(fields: $extensionManifestFields)
}
}
}
}
}
`
/**
* Query the GraphQL API for registry metadata about the extensions given in {@link extensionIDs}.
*
* @returns An observable that emits once with the results.
*/
export function queryConfiguredRegistryExtensions(
// TODO(tj): can copy this over to extension host, just replace platformContext.requestGraphQL
// with mainThreadAPI.requestGraphQL
{ getGraphQLClient }: Pick<PlatformContext, 'getGraphQLClient'>,
extensionIDs: string[]
): Observable<ConfiguredExtension[]> {
if (extensionIDs.length === 0) {
return of([])
}
const queryObservablePromise = getGraphQLClient().then(client =>
client.watchQuery<ExtensionsResult, ExtensionsVariables>({
query: getDocumentNode(ExtensionsQuery),
variables: {
first: extensionIDs.length,
extensionIDs,
// Spread operator is required to avoid Typescript type error
// because of `readonly` type of `CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS`.
extensionManifestFields: [...CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS],
},
})
)
return fromObservableQueryPromise(queryObservablePromise).pipe(
map(({ data, errors }) => {
if (!data?.extensionRegistry?.extensions?.nodes) {
throw createAggregateError(errors)
}
const { nodes } = data.extensionRegistry.extensions
return nodes
.filter(({ extensionID }) => extensionIDs.includes(extensionID))
.map(({ extensionID, manifest }) => {
const getManifest = (value: typeof manifest): ConfiguredExtension['manifest'] => {
if (!value) {
return value
}
return value.jsonFields as Pick<ExtensionManifest, ConfiguredExtensionManifestDefaultFields>
}
return {
id: extensionID,
manifest: getManifest(manifest),
}
})
})
)
}

View File

@ -488,47 +488,45 @@ export function registerHoverContributions({
subscriptions.add(syncRemoteSubscription(referencesContributionPromise))
let implementationsContributionPromise: Promise<unknown> = Promise.resolve()
if (window.context?.enableLegacyExtensions === false) {
const promise = extensionHostAPI.registerContributions({
actions: [
...languageSpecs.map(spec => ({
actionItem: { label: 'Find implementations' },
command: 'open',
commandArguments: [
"${get(context, 'implementations_" +
spec.languageID +
"') && get(context, 'panel.url') && sub(get(context, 'panel.url'), 'panelID', 'implementations_" +
spec.languageID +
"') || 'noop'}",
],
id: 'findImplementations_' + spec.languageID,
title: 'Find implementations',
})),
],
menus: {
hover: languageSpecs.map(spec => ({
action: 'findImplementations_' + spec.languageID,
when:
"resource.language == '" +
const promise = extensionHostAPI.registerContributions({
actions: [
...languageSpecs.map(spec => ({
actionItem: { label: 'Find implementations' },
command: 'open',
commandArguments: [
"${get(context, 'implementations_" +
spec.languageID +
// eslint-disable-next-line no-template-curly-in-string
"' && get(context, `implementations_${resource.language}`) && (goToDefinition.showLoading || goToDefinition.url || goToDefinition.error)",
})),
},
})
implementationsContributionPromise = promise
subscriptions.add(syncRemoteSubscription(promise))
for (const spec of languageSpecs) {
if (spec.textDocumentImplemenationSupport) {
extensionHostAPI
.updateContext({
[`implementations_${spec.languageID}`]: true,
})
.then(
() => {},
() => {}
)
}
"') && get(context, 'panel.url') && sub(get(context, 'panel.url'), 'panelID', 'implementations_" +
spec.languageID +
"') || 'noop'}",
],
id: 'findImplementations_' + spec.languageID,
title: 'Find implementations',
})),
],
menus: {
hover: languageSpecs.map(spec => ({
action: 'findImplementations_' + spec.languageID,
when:
"resource.language == '" +
spec.languageID +
// eslint-disable-next-line no-template-curly-in-string
"' && get(context, `implementations_${resource.language}`) && (goToDefinition.showLoading || goToDefinition.url || goToDefinition.error)",
})),
},
})
implementationsContributionPromise = promise
subscriptions.add(syncRemoteSubscription(promise))
for (const spec of languageSpecs) {
if (spec.textDocumentImplemenationSupport) {
extensionHostAPI
.updateContext({
[`implementations_${spec.languageID}`]: true,
})
.then(
() => {},
() => {}
)
}
}

View File

@ -5,7 +5,7 @@ import { from, Subject, Subscription } from 'rxjs'
import { catchError, distinctUntilChanged, map, scan, switchMap } from 'rxjs/operators'
import { renderMarkdown } from '@sourcegraph/common'
import { Alert, AlertProps } from '@sourcegraph/wildcard'
import { Alert } from '@sourcegraph/wildcard'
import type { NotificationType, Progress } from '../codeintel/legacy-extensions/api'
@ -17,21 +17,11 @@ export interface UnbrandedNotificationItemStyleProps {
notificationItemClassNames: Record<NotificationType, string>
}
export interface BrandedNotificationItemStyleProps {
notificationItemVariants: Record<NotificationType, AlertProps['variant']>
}
/**
* Note, we do not export this type because it is not intended to be used directly.
* Consumers should use either `UnbrandedNotificationItemStyleProps` or `BrandedNotificationItemStyleProps` when configuring this component.
*/
type NotificationItemStyleProps = UnbrandedNotificationItemStyleProps | BrandedNotificationItemStyleProps
export interface NotificationItemProps {
notification: Notification
onDismiss: (notification: Notification) => void
className?: string
notificationItemStyleProps: NotificationItemStyleProps
notificationItemStyleProps: UnbrandedNotificationItemStyleProps
}
interface NotificationItemState {
@ -93,18 +83,12 @@ export class NotificationItem extends React.PureComponent<NotificationItemProps,
const baseAlertClassName = classNames(styles.sourcegraphNotificationItem, this.props.className)
const { notificationItemStyleProps } = this.props
const alertProps =
'notificationItemVariants' in notificationItemStyleProps
? {
variant: notificationItemStyleProps.notificationItemVariants[this.props.notification.type],
className: baseAlertClassName,
}
: {
className: classNames(
baseAlertClassName,
notificationItemStyleProps.notificationItemClassNames[this.props.notification.type]
),
}
const alertProps = {
className: classNames(
baseAlertClassName,
notificationItemStyleProps.notificationItemClassNames[this.props.notification.type]
),
}
return (
<Alert {...alertProps}>

View File

@ -3,7 +3,7 @@ import { isObject } from 'lodash'
import { Observable, Subscribable, Subscription } from 'rxjs'
import { DiffPart } from '@sourcegraph/codeintellify'
import { ErrorLike, hasProperty } from '@sourcegraph/common'
import { hasProperty } from '@sourcegraph/common'
import { GraphQLClient, GraphQLResult } from '@sourcegraph/http-client'
import { SettingsEdit } from '../api/client/services/settings'
@ -130,22 +130,6 @@ export interface PlatformContext {
*/
createExtensionHost: () => Promise<ClosableEndpointPair>
/**
* Returns the script URL suitable for passing to importScripts for an extension's bundle.
*
* This is necessary because some platforms (such as Chrome extensions) use a script-src CSP
* that would prevent loading bundles from arbitrary URLs, which requires us to pass blob: URIs
* to importScripts.
*
* @param bundleURL The URL to the JavaScript bundle file specified in the extension manifest.
* @returns A script URL suitable for passing to importScripts, typically either the original
* https:// URL for the extension's bundle or a blob: URI for it.
*
* TODO(tj): If this doesn't return a getScriptURLForExtension function, the original bundleURL will be used.
* Also, make getScriptURL batched to minimize round trips between extension host and client application
*/
getScriptURLForExtension: () => undefined | ((bundleURL: string[]) => Promise<(string | ErrorLike)[]>)
/**
* Constructs the URL (possibly relative or absolute) to the file with the specified options.
*

View File

@ -1,20 +1,4 @@
import { PollyServer } from '@pollyjs/core'
import type { ExtensionContext } from '../../codeintel/legacy-extensions/api'
import { ExtensionManifest } from '../../extensions/extensionManifest'
import { ExtensionsResult, SharedGraphQlOperations } from '../../graphql-operations'
import { Settings } from '../../settings/settings'
interface ExtensionMockingInit {
/**
* The polly server object, used to intercept extension bundle requests.
*/
pollyServer: PollyServer
/**
* The base Sourcegraph URL for the test instance, used to construst bundle URL.
*/
sourcegraphBaseUrl: string
}
interface ExtensionMockingUtils {
/**
@ -23,75 +7,17 @@ interface ExtensionMockingUtils {
* and exports an `activate` function, just like any other Sourcegraph extension.
*/
mockExtension: ({ id, bundle }: { id: string; bundle: () => void }) => void
/**
* Use this as the `Extension` override for `TestContext#overrideGraphQL`.
*/
Extensions: SharedGraphQlOperations['Extensions']
/**
* Merge/replace your mock settings `extensions` property with this object.
*/
extensionSettings: Settings['extensions']
}
interface ExtensionsResultMock {
extensionRegistry: ExtensionsResult['extensionRegistry'] & {
__typename: 'ExtensionRegistry'
}
extensionSettings: {}
}
/**
* Set up Sourcegraph extension mocking for an integration test.
*/
export function setupExtensionMocking({
pollyServer,
sourcegraphBaseUrl,
}: ExtensionMockingInit): ExtensionMockingUtils {
let internalID = 0
const extensionSettings: Settings['extensions'] = {}
const extensionsResult: ExtensionsResultMock = {
extensionRegistry: {
__typename: 'ExtensionRegistry',
extensions: {
nodes: [],
},
},
}
return {
mockExtension: ({ id, bundle }) => {
internalID++
/** The URL at which the manifest says the extension bundle is served. We should intercept requests to this URL. */
const bundleURL = new URL(
`/-/static/extension/00${internalID}-${id.replace(/\//g, '-')}.js?hash--${id.replace(/\//g, '-')}`,
sourcegraphBaseUrl
).href
const extensionManifest: ExtensionManifest = {
url: bundleURL,
activationEvents: ['*'],
}
// Mutate mock data objects
extensionSettings[id] = true
extensionsResult.extensionRegistry.extensions.nodes.push({
id,
extensionID: id,
manifest: {
jsonFields: extensionManifest,
},
})
pollyServer.get(bundleURL).intercept((request, response) => {
// Create an immediately-invoked function expression for the extensionBundle function
const extensionBundleString = `(${bundle.toString()})()`
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
})
},
Extensions: () => extensionsResult,
extensionSettings,
}
export function setupExtensionMocking(): ExtensionMockingUtils {
throw new Error('not yet reimplemented after the extension API deprecation')
}
// Commonly mocked extensions.

View File

@ -44,7 +44,6 @@ interface Mocks
| 'updateSettings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'getScriptURLForExtension'
| 'clientApplication'
| 'showMessage'
| 'showInputBox'
@ -55,7 +54,6 @@ const NOOP_MOCKS: Mocks = {
updateSettings: () => Promise.reject(new Error('Mocks#updateSettings not implemented')),
getGraphQLClient: () => Promise.reject(new Error('Mocks#getGraphQLClient not implemented')),
requestGraphQL: () => throwError(new Error('Mocks#queryGraphQL not implemented')),
getScriptURLForExtension: () => undefined,
clientApplication: 'sourcegraph',
}

View File

@ -26,7 +26,6 @@ export interface VSCodePlatformContext
| 'getGraphQLClient'
| 'showMessage'
| 'showInputBox'
| 'getScriptURLForExtension'
| 'getStaticExtensions'
| 'telemetryService'
| 'clientApplication'
@ -60,7 +59,6 @@ export function createPlatformContext(extensionCoreAPI: Comlink.Remote<Extension
updateSettings: () => Promise.resolve(),
telemetryService: new EventLogger(extensionCoreAPI),
clientApplication: 'other', // TODO add 'vscode-extension' to `clientApplication`,
getScriptURLForExtension: () => undefined,
// TODO showInputBox
// TODO showMessage
getStaticExtensions: () => getInlineExtensions(),

View File

@ -2,14 +2,11 @@ import { downloadAndUnzipVSCode } from '@vscode/test-electron'
import { mixedSearchStreamEvents, highlightFileResult } from '@sourcegraph/shared/src/search/integration'
import { Settings } from '@sourcegraph/shared/src/settings/settings'
import { setupExtensionMocking } from '@sourcegraph/shared/src/testing/integration/mockExtension'
import { createVSCodeIntegrationTestContext, VSCodeIntegrationTestContext } from './context'
import { getVSCodeWebviewFrames } from './getWebview'
import { launchVsCode, VSCodeTestDriver } from './launch'
const sourcegraphBaseUrl = 'https://sourcegraph.com'
describe('VS Code extension', () => {
let vsCodeDriver: VSCodeTestDriver
before(async () => {
@ -39,18 +36,11 @@ describe('VS Code extension', () => {
// fixing before we add more test cases to the suite.
it('works', async () => {
const { Extensions } = setupExtensionMocking({
pollyServer: testContext.server,
sourcegraphBaseUrl,
})
const userSettings: Settings = {
extensions: {},
}
testContext.overrideGraphQL({
Extensions,
...highlightFileResult,
ViewerSettings: () => ({
viewerSettings: {

View File

@ -67,7 +67,6 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
openTelemetry: {
endpoint: ENVIRONMENT_CONFIG.CLIENT_OTEL_EXPORTER_OTLP_ENDPOINT,
},
enableLegacyExtensions: false,
// Site-config overrides default JS context
...siteConfig,
}

View File

@ -85,9 +85,6 @@ const jsContextChanges = `
// Only username/password auth-provider provider is supported with the standalone server.
authProviders: window.context.authProviders.filter(provider => provider.isBuiltin),
// For some reason, the standalone server crashes with legacy extensions enabled.
enableLegacyExtensions: false,
// Sync externalURL with the development environment config.
externalURL: '${HTTPS_WEB_SERVER_URL}',

View File

@ -197,7 +197,6 @@ export const Layout: React.FC<LegacyLayoutProps> = props => {
isRepositoryRelatedPage={isRepositoryRelatedPage}
showKeyboardShortcutsHelp={showKeyboardShortcutsHelp}
showFeedbackModal={showFeedbackModal}
enableLegacyExtensions={window.context.enableLegacyExtensions}
/>
)}
{needsSiteInit && !isSiteInit && <Navigate replace={true} to="/site-admin/init" />}

View File

@ -189,7 +189,6 @@ export const LegacyLayout: FC<LegacyLayoutProps> = props => {
isRepositoryRelatedPage={isRepositoryRelatedPage}
showKeyboardShortcutsHelp={showKeyboardShortcutsHelp}
showFeedbackModal={showFeedbackModal}
enableLegacyExtensions={window.context.enableLegacyExtensions}
/>
)}
{needsSiteInit && !isSiteInit && <Navigate replace={true} to="/site-admin/init" />}

View File

@ -10,14 +10,10 @@ import { combineLatest, from, Subscription, fromEvent, Observable } from 'rxjs'
import { logger } from '@sourcegraph/common'
import { GraphQLClient, HTTPStatusError } from '@sourcegraph/http-client'
import { SharedSpanName, TraceSpanProvider } from '@sourcegraph/observability-client'
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { FetchFileParameters, fetchHighlightedFileLineRanges } from '@sourcegraph/shared/src/backend/file'
import { setCodeIntelSearchContext } from '@sourcegraph/shared/src/codeintel/searchContext'
import { Controller as ExtensionsController } from '@sourcegraph/shared/src/extensions/controller'
import { createController as createExtensionsController } from '@sourcegraph/shared/src/extensions/createLazyLoadedController'
import { createNoopController } from '@sourcegraph/shared/src/extensions/createNoopLoadedController'
import { BrandedNotificationItemStyleProps } from '@sourcegraph/shared/src/notifications/NotificationItem'
import { Notifications } from '@sourcegraph/shared/src/notifications/Notifications'
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import { ShortcutProvider } from '@sourcegraph/shared/src/react-shortcuts'
import {
@ -91,16 +87,6 @@ interface LegacySourcegraphWebAppState extends SettingsCascadeProps {
globbing: boolean
}
const notificationStyles: BrandedNotificationItemStyleProps = {
notificationItemVariants: {
[NotificationType.Log]: 'secondary',
[NotificationType.Success]: 'success',
[NotificationType.Info]: 'info',
[NotificationType.Warning]: 'warning',
[NotificationType.Error]: 'danger',
},
}
const WILDCARD_THEME: WildcardTheme = {
isBranded: true,
}
@ -113,9 +99,7 @@ setLinkComponent(RouterLink)
export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, LegacySourcegraphWebAppState> {
private readonly subscriptions = new Subscription()
private readonly platformContext: PlatformContext = createPlatformContext()
private readonly extensionsController: ExtensionsController | null = window.context.enableLegacyExtensions
? createExtensionsController(this.platformContext)
: createNoopController(this.platformContext)
private readonly extensionsController: ExtensionsController | null = createNoopController(this.platformContext)
constructor(props: StaticAppConfig) {
super(props)
@ -304,13 +288,6 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
]}
>
<RouterProvider router={router} />
{this.extensionsController !== null && window.context.enableLegacyExtensions ? (
<Notifications
key={2}
extensionsController={this.extensionsController}
notificationItemStyleProps={notificationStyles}
/>
) : null}
<UserSessionStores />
</ComponentsComposer>
)

View File

@ -25,7 +25,6 @@ describe('persistenceMapper', () => {
const persistedString = await persistenceMapper(
createStringifiedCache({
viewerSettings: { empty: null, data: true },
extensionRegistry: { data: true },
shouldNotBePersisted: {},
})
)

View File

@ -5,11 +5,7 @@ import { QueryFieldPolicy } from '@sourcegraph/shared/src/graphql-operations'
* After the implementation of the `persistLink` which will support `@persist` directive
* hardcoded query names will be deprecated.
*/
export const QUERIES_TO_PERSIST: (keyof QueryFieldPolicy)[] = [
'viewerSettings',
'extensionRegistry',
'temporarySettings',
]
export const QUERIES_TO_PERSIST: (keyof QueryFieldPolicy)[] = ['viewerSettings', 'temporarySettings']
export const ROOT_QUERY_KEY = 'ROOT_QUERY'
export interface CacheReference {

View File

@ -1,21 +0,0 @@
.button {
color: var(--icon-color);
display: flex;
align-items: center;
&:hover {
// Required to overwrite .btn:hover styles with greater specificity.
color: var(--body-color) !important;
}
}
.action-item {
--link-color: var(--body-color);
--link-hover-color: var(--body-color);
display: block;
text-align: left;
/* Overwrite `ButtonLink` style */
text-decoration: none !important;
}

View File

@ -1,36 +0,0 @@
import React from 'react'
import classNames from 'classnames'
import {
CommandListPopoverButton,
CommandListPopoverButtonProps,
} from '@sourcegraph/shared/src/commandPalette/CommandList'
import { useKeyboardShortcut } from '@sourcegraph/shared/src/keyboardShortcuts/useKeyboardShortcut'
import { Button } from '@sourcegraph/wildcard'
import styles from './WebCommandListPopoverButton.module.scss'
export const WebCommandListPopoverButton: React.FunctionComponent<
React.PropsWithChildren<CommandListPopoverButtonProps>
> = props => {
const showCommandPaletteShortcut = useKeyboardShortcut('commandPalette')
return (
<CommandListPopoverButton
{...props}
as={Button}
variant="link"
buttonClassName={classNames('m-0 p-0', styles.button)}
formClassName="form p-2 bg-1 border-bottom"
inputClassName="form-control px-2 py-1"
listClassName="list-group list-group-flush list-unstyled pt-1"
actionItemClassName={classNames('list-group-item list-group-item-action p-2 border-0', styles.actionItem)}
selectedActionItemClassName="active border-primary"
noResultsClassName="list-group-item text-muted"
keyboardShortcutForShow={showCommandPaletteShortcut}
/>
)
}
WebCommandListPopoverButton.displayName = 'WebCommandListPopoverButton'

View File

@ -1 +0,0 @@
export * from './WebCommandListPopoverButton'

View File

@ -1,3 +1,2 @@
// Components from shared with web-styling class names applied
export { WebHoverOverlay } from './WebHoverOverlay'
export { WebCommandListPopoverButton } from './WebCommandListPopoverButton'

View File

@ -1,129 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { isEqual } from 'lodash'
import { Subscription } from 'rxjs'
import { observeResize } from '@sourcegraph/common'
interface CarouselOptions {
amountToScroll?: number
direction: CarouselDirection
}
type CarouselDirection = 'leftToRight' | 'topToBottom'
interface CarouselState {
canScrollNegative: boolean
canScrollPositive: boolean
onNegativeClicked: () => void
onPositiveClicked: () => void
carouselReference: React.RefCallback<HTMLElement>
}
const defaultCarouselState = { canScrollNegative: false, canScrollPositive: false }
const carouselScrollHandlers: Record<
CarouselDirection,
(carousel: HTMLElement) => Pick<CarouselState, 'canScrollNegative' | 'canScrollPositive'>
> = {
leftToRight: carousel => ({
canScrollNegative: carousel.scrollLeft > 0,
canScrollPositive: carousel.scrollLeft + carousel.clientWidth < carousel.scrollWidth,
}),
topToBottom: carousel => ({
canScrollNegative: carousel.scrollTop > 0,
canScrollPositive: carousel.scrollTop + carousel.clientHeight < carousel.scrollHeight,
}),
}
const carouselClickHandlers: Record<
CarouselDirection,
(options: { carousel: HTMLElement; amountToScroll: number; sign: 'positive' | 'negative' }) => void
> = {
leftToRight: ({ carousel, amountToScroll, sign }) => {
const width = carousel.clientWidth
carousel.scrollBy({
top: 0,
left: sign === 'positive' ? width * amountToScroll : -(width * amountToScroll),
behavior: 'smooth',
})
},
topToBottom: ({ carousel, amountToScroll, sign }) => {
const height = carousel.clientHeight
carousel.scrollBy({
top: sign === 'positive' ? height * amountToScroll : -(height * amountToScroll),
left: 0,
behavior: 'smooth',
})
},
}
export function useCarousel({ amountToScroll = 0.9, direction }: CarouselOptions): CarouselState {
const [carousel, setCarousel] = useState<HTMLElement | null>()
const nextCarousel = useCallback((carousel: HTMLElement) => {
setCarousel(carousel)
}, [])
const [scrollability, setScrollability] = useState(defaultCarouselState)
const scrollabilityReference = useRef(scrollability)
scrollabilityReference.current = scrollability
// Listen for UIEvents that can affect scrollability (e.g. scroll, resize)
useEffect(() => {
function onScroll(): void {
if (carousel) {
const newScrollability = carouselScrollHandlers[direction](carousel)
if (!isEqual(scrollabilityReference.current, newScrollability)) {
setScrollability(newScrollability)
}
}
}
carousel?.addEventListener('scroll', onScroll)
let subscription: Subscription | undefined
if (carousel) {
subscription = observeResize(carousel).subscribe(() => {
const newScrollability = carouselScrollHandlers[direction](carousel)
if (!isEqual(scrollabilityReference.current, newScrollability)) {
setScrollability(newScrollability)
}
})
// Check initial scroll state
const newScrollability = carouselScrollHandlers[direction](carousel)
if (!isEqual(scrollabilityReference.current, newScrollability)) {
setScrollability(newScrollability)
}
}
return () => {
carousel?.removeEventListener('scroll', onScroll)
subscription?.unsubscribe()
}
}, [carousel, direction])
// Handle negative and positive click events
const onNegativeClicked = useCallback(() => {
if (carousel) {
carouselClickHandlers[direction]({ sign: 'negative', amountToScroll, carousel })
}
}, [direction, amountToScroll, carousel])
const onPositiveClicked = useCallback(() => {
if (carousel) {
carouselClickHandlers[direction]({ sign: 'positive', amountToScroll, carousel })
}
}, [direction, amountToScroll, carousel])
return {
canScrollNegative: scrollability.canScrollNegative,
canScrollPositive: scrollability.canScrollPositive,
onNegativeClicked,
onPositiveClicked,
carouselReference: nextCarousel,
}
}

View File

@ -20,7 +20,7 @@ export interface CodeInsightsRouterProps extends TelemetryProps {
export const CodeInsightsRouter: FC<CodeInsightsRouterProps> = props => {
const { authenticatedUser, telemetryService } = props
if (!window.context.codeInsightsEnabled) {
if (!window.context?.codeInsightsEnabled) {
return (
<CodeInsightsDotComGetStartedLazy
telemetryService={telemetryService}

View File

@ -1,151 +0,0 @@
$default-icon-colors: --oc-grape-7, --oc-violet-7, --oc-cyan-9, --oc-indigo-7, --oc-pink-8;
:root {
--action-item-width: 2.5rem;
--action-item-container-width: 2.5625rem; /* 2.5rem + 1px */
}
.bar {
flex: 0 0 auto;
width: var(--action-item-width);
background-color: var(--body-bg);
list-style: none;
&--collapsed {
width: 0.5rem;
}
}
.toggle-container {
width: var(--action-item-width);
background-color: var(--color-bg-2);
background-color: var(--body-bg);
&--open {
margin-bottom: -0.0625rem;
}
}
/* Used to visually separate action items bar sections. */
.divider-horizontal {
height: 0.0625rem;
width: 1.25rem;
background-color: var(--border-color);
left: 0.625rem;
}
/* Used to visually separate action items bar toggle from repo header actions. */
.divider-vertical {
height: 1.25rem;
width: 0.0625rem;
top: 0.75rem;
align-self: center;
border-radius: 2px;
background-color: var(--border-color);
}
.list {
overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.list-item {
user-select: none;
&:first-of-type {
margin-top: 0.375rem;
}
&:last-of-type {
margin-bottom: 0.375rem;
}
}
.action {
width: 2rem;
height: 2rem;
margin-left: 0.25rem;
border-radius: 0.1875rem;
&:hover {
background-color: var(--color-bg-2);
}
&--toggle {
height: auto;
padding: 0.25rem;
}
&--pressed {
color: var(--body-color);
background-color: var(--color-bg-3);
/* Override existing hover styles */
&:hover {
background-color: var(--color-bg-3);
}
}
&--inactive {
cursor: default;
filter: saturate(0%);
opacity: 0.7;
}
/* Default icon generated for extensions with no iconURL */
&--no-icon {
&::after {
color: var(--white);
/* Center letter */
display: flex;
align-items: center;
justify-content: center;
height: 1rem;
width: 1rem;
font-size: (10 / 16) + rem;
content: attr(data-content);
border-radius: (2 / 16) + rem;
}
&-inactive {
&::after {
background-color: var(--color-bg-3) !important;
color: var(--text-muted) !important;
}
}
}
}
.icon {
height: 1rem !important;
width: 1rem !important;
/* Default icon background color */
@for $i from 1 through length($default-icon-colors) {
&-#{$i} {
&::after {
background-color: var(nth($default-icon-colors, $i));
}
}
}
}
/* e.g. "close extensions panel", "add extensions" */
.aux-icon {
color: var(--icon-color);
}
.scroll {
width: var(--action-item-width);
&:hover {
background-color: var(--color-bg-2);
}
}

View File

@ -1,101 +0,0 @@
import React from 'react'
import { DecoratorFn, Meta } from '@storybook/react'
import { EMPTY, noop, of } from 'rxjs'
import { ContributableMenu, Contributions, Evaluated } from '@sourcegraph/client-api'
import { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
import { pretendProxySubscribable, pretendRemote } from '@sourcegraph/shared/src/api/util'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { extensionsController, NOOP_PLATFORM_CONTEXT } from '@sourcegraph/shared/src/testing/searchTestHelpers'
import { AppRouterContainer } from '../../components/AppRouterContainer'
import { WebStory } from '../../components/WebStory'
import { SourcegraphContext } from '../../jscontext'
import { ActionItemsBar, useWebActionItems } from './ActionItemsBar'
const mockIconURL =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE2LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHdpZHRoPSI5N3B4IiBoZWlnaHQ9Ijk3cHgiIHZpZXdCb3g9IjAgMCA5NyA5NyIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTcgOTciIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxwYXRoIGZpbGw9IiNGMDUxMzMiIGQ9Ik05Mi43MSw0NC40MDhMNTIuNTkxLDQuMjkxYy0yLjMxLTIuMzExLTYuMDU3LTIuMzExLTguMzY5LDBsLTguMzMsOC4zMzJMNDYuNDU5LDIzLjE5CgkJYzIuNDU2LTAuODMsNS4yNzItMC4yNzMsNy4yMjksMS42ODVjMS45NjksMS45NywyLjUyMSw0LjgxLDEuNjcsNy4yNzVsMTAuMTg2LDEwLjE4NWMyLjQ2NS0wLjg1LDUuMzA3LTAuMyw3LjI3NSwxLjY3MQoJCWMyLjc1LDIuNzUsMi43NSw3LjIwNiwwLDkuOTU4Yy0yLjc1MiwyLjc1MS03LjIwOCwyLjc1MS05Ljk2MSwwYy0yLjA2OC0yLjA3LTIuNTgtNS4xMS0xLjUzMS03LjY1OGwtOS41LTkuNDk5djI0Ljk5NwoJCWMwLjY3LDAuMzMyLDEuMzAzLDAuNzc0LDEuODYxLDEuMzMyYzIuNzUsMi43NSwyLjc1LDcuMjA2LDAsOS45NTljLTIuNzUsMi43NDktNy4yMDksMi43NDktOS45NTcsMGMtMi43NS0yLjc1NC0yLjc1LTcuMjEsMC05Ljk1OQoJCWMwLjY4LTAuNjc5LDEuNDY3LTEuMTkzLDIuMzA3LTEuNTM3VjM2LjM2OWMtMC44NC0wLjM0NC0xLjYyNS0wLjg1My0yLjMwNy0xLjUzN2MtMi4wODMtMi4wODItMi41ODQtNS4xNC0xLjUxNi03LjY5OAoJCUwzMS43OTgsMTYuNzE1TDQuMjg4LDQ0LjIyMmMtMi4zMTEsMi4zMTMtMi4zMTEsNi4wNiwwLDguMzcxbDQwLjEyMSw0MC4xMThjMi4zMSwyLjMxMSw2LjA1NiwyLjMxMSw4LjM2OSwwTDkyLjcxLDUyLjc3OQoJCUM5NS4wMjEsNTAuNDY4LDk1LjAyMSw0Ni43MTksOTIuNzEsNDQuNDA4eiIvPgo8L2c+Cjwvc3ZnPgo='
if (!window.context) {
window.context = { enableLegacyExtensions: false } as SourcegraphContext & Mocha.SuiteFunction
}
// eslint-disable-next-line id-length
const mockActionItems = [...(new Array(10) as (number | undefined)[])].map((_, index) => `${index}`)
const mockContributions: Evaluated<Contributions> = {
actions: mockActionItems.map((id, index) => ({
id,
actionItem: {
iconURL: mockIconURL,
label: 'Some label',
pressed: index < 2,
description: 'Some description',
},
command: 'open',
active: true,
category: 'Some category',
title: 'Some title',
})),
menus: {
[ContributableMenu.EditorTitle]: mockActionItems.map(id => ({
action: id,
alt: id,
when: true,
})),
},
}
const mockExtensionsController = {
...extensionsController,
extHostAPI: Promise.resolve(
pretendRemote<FlatExtensionHostAPI>({
getContributions: () => pretendProxySubscribable(of(mockContributions)),
registerContributions: () => pretendProxySubscribable(EMPTY).subscribe(noop as any),
haveInitialExtensionsLoaded: () => pretendProxySubscribable(of(true)),
})
),
}
const decorator: DecoratorFn = story => (
<WebStory
initialEntries={[
'/github.com/sourcegraph/sourcegraph/-/blob/client/browser/src/browser-extension/ThemeWrapper.tsx',
]}
>
{() => (
<AppRouterContainer>
<div className="container mt-3">{story()}</div>
</AppRouterContainer>
)}
</WebStory>
)
const config: Meta = {
title: 'web/extensions/ActionItemsBar',
decorators: [decorator],
component: ActionItemsBar,
parameters: {
chromatic: {
enableDarkMode: true,
disableSnapshot: false,
},
},
}
export default config
export const Default: React.FunctionComponent<React.PropsWithChildren<unknown>> = () => {
const { useActionItemsBar } = useWebActionItems()
return (
<ActionItemsBar
repo={undefined}
useActionItemsBar={useActionItemsBar}
extensionsController={mockExtensionsController}
platformContext={NOOP_PLATFORM_CONTEXT as any}
telemetryService={NOOP_TELEMETRY_SERVICE}
/>
)
}

View File

@ -1,412 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { mdiChevronDoubleDown, mdiChevronDoubleUp, mdiMenuDown, mdiMenuUp, mdiPuzzleOutline } from '@mdi/js'
import VisuallyHidden from '@reach/visually-hidden'
import classNames from 'classnames'
import { head, last } from 'lodash'
import { useLocation } from 'react-router-dom'
import { BehaviorSubject, from, of } from 'rxjs'
import { distinctUntilChanged, map } from 'rxjs/operators'
import { focusable, FocusableElement } from 'tabbable'
import { Key } from 'ts-key-enum'
import { ContributableMenu } from '@sourcegraph/client-api'
import { LocalStorageSubject } from '@sourcegraph/common'
import { ActionItem } from '@sourcegraph/shared/src/actions/ActionItem'
import { ActionsContainer } from '@sourcegraph/shared/src/actions/ActionsContainer'
import { haveInitialExtensionsLoaded } from '@sourcegraph/shared/src/api/features'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { isSettingsValid } from '@sourcegraph/shared/src/settings/settings'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Button, ButtonLink, Icon, LoadingSpinner, Tooltip, useObservable } from '@sourcegraph/wildcard'
import { ErrorBoundary } from '../../components/ErrorBoundary'
import { useCarousel } from '../../components/useCarousel'
import { RepositoryFields } from '../../graphql-operations'
import { OpenInEditorActionItem } from '../../open-in-editor/OpenInEditorActionItem'
import { GoToCodeHostAction } from '../../repo/actions/GoToCodeHostAction'
import { ToggleBlameAction } from '../../repo/actions/ToggleBlameAction'
import { fetchFileExternalLinks } from '../../repo/backend'
import { parseBrowserRepoURL } from '../../util/url'
import styles from './ActionItemsBar.module.scss'
const scrollButtonClassName = styles.scroll
function getIconClassName(index: number): string | undefined {
return (styles as Record<string, string>)[`icon${index % 5}`]
}
function arrowable(element: HTMLElement): FocusableElement[] {
return focusable(element).filter(
elm => !elm.classList.contains('disabled') && !elm.classList.contains(scrollButtonClassName)
)
}
export function useWebActionItems(): Pick<ActionItemsBarProps, 'useActionItemsBar'> &
Pick<ActionItemsToggleProps, 'useActionItemsToggle'> {
const toggles = useMemo(() => new LocalStorageSubject('action-items-bar-expanded', true), [])
const [toggleReference, setToggleReference] = useState<HTMLElement | null>(null)
const nextToggleReference = useCallback((toggle: HTMLElement) => {
setToggleReference(toggle)
}, [])
const [barReference, setBarReference] = useState<HTMLElement | null>(null)
const nextBarReference = useCallback((bar: HTMLElement) => {
setBarReference(bar)
}, [])
// Set up keyboard navigation for distant toggle and bar. Remove previous event
// listeners whenever references change.
useEffect(() => {
function onKeyDownToggle(event: KeyboardEvent): void {
if (event.key === Key.ArrowDown && barReference) {
const firstBarArrowable = head(arrowable(barReference))
if (firstBarArrowable) {
firstBarArrowable.focus()
event.preventDefault()
}
}
if (event.key === Key.ArrowUp && barReference) {
const lastBarArrowable = last(arrowable(barReference))
if (lastBarArrowable) {
lastBarArrowable.focus()
event.preventDefault()
}
}
}
function onKeyDownBar(event: KeyboardEvent): void {
if (event.target instanceof HTMLElement && toggleReference && barReference) {
const arrowableChildren = arrowable(barReference)
const indexOfTarget = arrowableChildren.indexOf(event.target)
if (event.key === Key.ArrowDown) {
// If this is the last arrowable element, go back to the toggle
if (indexOfTarget === arrowableChildren.length - 1) {
toggleReference.focus()
event.preventDefault()
return
}
const itemToFocus = arrowableChildren[indexOfTarget + 1]
if (itemToFocus instanceof HTMLElement) {
itemToFocus.focus()
event.preventDefault()
return
}
}
if (event.key === Key.ArrowUp) {
// If this is the first arrowable element, go back to the toggle
if (indexOfTarget === 0) {
toggleReference.focus()
event.preventDefault()
return
}
const itemToFocus = arrowableChildren[indexOfTarget - 1]
if (itemToFocus instanceof HTMLElement) {
itemToFocus.focus()
event.preventDefault()
return
}
}
}
}
toggleReference?.addEventListener('keydown', onKeyDownToggle)
barReference?.addEventListener('keydown', onKeyDownBar)
return () => {
toggleReference?.removeEventListener('keydown', onKeyDownToggle)
toggleReference?.removeEventListener('keydown', onKeyDownBar)
}
}, [toggleReference, barReference])
const barsReferenceCounts = useMemo(() => new BehaviorSubject(0), [])
const useActionItemsBar = useCallback(() => {
// `useActionItemsBar` will be used as a hook
// eslint-disable-next-line react-hooks/rules-of-hooks
const isOpen = useObservable(toggles)
// Let the toggle know it's on the page
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
// Use reference counter so that effect order doesn't matter
barsReferenceCounts.next(barsReferenceCounts.value + 1)
return () => barsReferenceCounts.next(barsReferenceCounts.value - 1)
}, [])
return { isOpen, barReference: nextBarReference }
}, [toggles, nextBarReference, barsReferenceCounts])
const useActionItemsToggle = useCallback(() => {
// `useActionItemsToggle` will be used as a hook
// eslint-disable-next-line react-hooks/rules-of-hooks
const isOpen = useObservable(toggles)
// eslint-disable-next-line react-hooks/rules-of-hooks
const toggle = useCallback(() => toggles.next(!isOpen), [isOpen])
// Only show the action items toggle when the <ActionItemsBar> component is on the page
// eslint-disable-next-line react-hooks/rules-of-hooks
const barInPage = !!useObservable(
// eslint-disable-next-line react-hooks/rules-of-hooks
useMemo(
() =>
barsReferenceCounts.pipe(
map(count => count > 0),
distinctUntilChanged()
),
[]
)
)
return { isOpen, toggle, barInPage, toggleReference: nextToggleReference }
}, [toggles, nextToggleReference, barsReferenceCounts])
return {
useActionItemsBar,
useActionItemsToggle,
}
}
export interface ActionItemsBarProps extends ExtensionsControllerProps, TelemetryProps, PlatformContextProps {
repo?: RepositoryFields
useActionItemsBar: () => { isOpen: boolean | undefined; barReference: React.RefCallback<HTMLElement> }
source?: 'compare' | 'commit' | 'blob'
}
const actionItemClassName = classNames(
'd-flex justify-content-center align-items-center text-decoration-none',
styles.action
)
/**
* Renders extensions (both migrated to the core workflow and legacy) actions items in the sidebar.
*/
export const ActionItemsBar = React.memo<ActionItemsBarProps>(function ActionItemsBar(props) {
const { extensionsController, source } = props
const location = useLocation()
const { isOpen, barReference } = props.useActionItemsBar()
const { repoName, rawRevision, filePath, commitRange, position, range } = parseBrowserRepoURL(
location.pathname + location.search + location.hash
)
const { carouselReference, canScrollNegative, canScrollPositive, onNegativeClicked, onPositiveClicked } =
useCarousel({ direction: 'topToBottom' })
const haveExtensionsLoaded = useObservable(
useMemo(
() =>
extensionsController !== null ? haveInitialExtensionsLoaded(extensionsController.extHostAPI) : of(true),
[extensionsController]
)
)
const settingsOrError = useObservable(
useMemo(() => from(props.platformContext.settings), [props.platformContext.settings])
)
const settings =
settingsOrError !== undefined && isSettingsValid(settingsOrError) ? settingsOrError.final : undefined
const perforceCodeHostUrlToSwarmUrlMap =
(settings?.['perforce.codeHostToSwarmMap'] as { [codeHost: string]: string } | undefined) || {}
if (!isOpen) {
return <div className={classNames(styles.bar, styles.barCollapsed)} />
}
return (
<div className={classNames('p-0 mr-2 position-relative d-flex flex-column', styles.bar)} ref={barReference}>
{/* To be clear to users that this isn't an error reported by extensions about e.g. the code they're viewing. */}
<ErrorBoundary location={location} render={error => <span>Component error: {error.message}</span>}>
<ActionItemsDivider />
{canScrollNegative && (
<Button
className={classNames('p-0 border-0', styles.scroll, styles.listItem)}
onClick={onNegativeClicked}
tabIndex={-1}
variant="link"
aria-label="Scroll up"
>
<Icon aria-hidden={true} svgPath={mdiMenuUp} />
</Button>
)}
{source !== 'compare' && source !== 'commit' && (
<GoToCodeHostAction
repo={props.repo}
repoName={repoName}
// We need a revision to generate code host URLs, if revision isn't available, we use the default branch or HEAD.
revision={rawRevision || props.repo?.defaultBranch?.displayName || 'HEAD'}
filePath={filePath}
commitRange={commitRange}
range={range}
position={position}
perforceCodeHostUrlToSwarmUrlMap={perforceCodeHostUrlToSwarmUrlMap}
fetchFileExternalLinks={fetchFileExternalLinks}
actionType="nav"
source="actionItemsBar"
/>
)}
{source === 'blob' && (
<>
<ToggleBlameAction actionType="nav" source="actionItemsBar" />
{window.context.isAuthenticatedUser && (
<OpenInEditorActionItem
platformContext={props.platformContext}
externalServiceType={props.repo?.externalRepository?.serviceType}
actionType="nav"
source="actionItemsBar"
/>
)}
</>
)}
{extensionsController !== null ? (
<ActionsContainer
menu={ContributableMenu.EditorTitle}
returnInactiveMenuItems={true}
extensionsController={extensionsController}
empty={null}
location={location}
platformContext={props.platformContext}
telemetryService={props.telemetryService}
>
{items => (
<ul className={classNames('list-unstyled m-0', styles.list)} ref={carouselReference}>
{items.map((item, index) => {
const hasIconURL = !!item.action.actionItem?.iconURL
const className = classNames(
actionItemClassName,
!hasIconURL &&
classNames(styles.actionNoIcon, getIconClassName(index), 'text-sm')
)
const inactiveClassName = classNames(
styles.actionInactive,
!hasIconURL && styles.actionNoIconInactive
)
const listItemClassName = classNames(
styles.listItem,
index !== items.length - 1 && 'mb-1'
)
const dataContent = !hasIconURL ? item.action.category?.slice(0, 1) : undefined
return (
<li key={item.action.id} className={listItemClassName}>
<ActionItem
{...props}
{...item}
location={location}
extensionsController={extensionsController}
className={className}
dataContent={dataContent}
variant="actionItem"
iconClassName={styles.icon}
pressedClassName={styles.actionPressed}
inactiveClassName={inactiveClassName}
hideLabel={true}
tabIndex={-1}
hideExternalLinkIcon={true}
disabledDuringExecution={true}
/>
</li>
)
})}
</ul>
)}
</ActionsContainer>
) : null}
{canScrollPositive && (
<Button
className={classNames('p-0 border-0', styles.scroll, styles.listItem)}
onClick={onPositiveClicked}
tabIndex={-1}
variant="link"
aria-label="Scroll down"
>
<Icon aria-hidden={true} svgPath={mdiMenuDown} />
</Button>
)}
{haveExtensionsLoaded && <ActionItemsDivider />}
</ErrorBoundary>
</div>
)
})
export interface ActionItemsToggleProps extends ExtensionsControllerProps<'extHostAPI'> {
useActionItemsToggle: () => {
isOpen: boolean | undefined
toggle: () => void
toggleReference: React.RefCallback<HTMLElement>
barInPage: boolean
}
className?: string
}
export const ActionItemsToggle: React.FunctionComponent<React.PropsWithChildren<ActionItemsToggleProps>> = ({
useActionItemsToggle,
extensionsController,
className,
}) => {
const panelName = extensionsController !== null && window.context.enableLegacyExtensions ? 'extensions' : 'actions'
const { isOpen, toggle, toggleReference, barInPage } = useActionItemsToggle()
const haveExtensionsLoaded = useObservable(
useMemo(
() =>
extensionsController !== null ? haveInitialExtensionsLoaded(extensionsController.extHostAPI) : of(true),
[extensionsController]
)
)
return barInPage ? (
<>
<li className={styles.dividerVertical} />
<li className={classNames('nav-item mr-2', className)}>
<div className={classNames(styles.toggleContainer, isOpen && styles.toggleContainerOpen)}>
<Tooltip content={`${isOpen ? 'Close' : 'Open'} ${panelName} panel`}>
<ButtonLink
aria-label={
isOpen
? `Close ${panelName} panel. Press the down arrow key to enter the ${panelName} panel.`
: `Open ${panelName} panel`
}
className={classNames(actionItemClassName, styles.auxIcon, styles.actionToggle)}
onSelect={toggle}
ref={toggleReference}
>
{!haveExtensionsLoaded ? (
<LoadingSpinner />
) : isOpen ? (
<Icon aria-hidden={true} svgPath={mdiChevronDoubleUp} />
) : (
<Icon
aria-hidden={true}
svgPath={
window.context.enableLegacyExtensions ? mdiPuzzleOutline : mdiChevronDoubleDown
}
/>
)}
{haveExtensionsLoaded && <VisuallyHidden>Down arrow to enter</VisuallyHidden>}
</ButtonLink>
</Tooltip>
</div>
</li>
</>
) : null
}
const ActionItemsDivider: React.FunctionComponent<React.PropsWithChildren<{ className?: string }>> = ({
className,
}) => <div className={classNames('position-relative rounded-sm d-flex', styles.dividerHorizontal, className)} />

View File

@ -1,10 +1,6 @@
import assert from 'assert'
import type { ExtensionContext } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
import { SharedGraphQlOperations } from '@sourcegraph/shared/src/graphql-operations'
import { ExtensionManifest } from '@sourcegraph/shared/src/schema/extensionSchema'
import { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
import { accessibilityAudit } from '@sourcegraph/shared/src/testing/accessibility'
import { Driver, createDriverForTest } from '@sourcegraph/shared/src/testing/driver'
import { afterEachSaveScreenshotIfFailed } from '@sourcegraph/shared/src/testing/screenshotReporter'
@ -18,7 +14,6 @@ import {
createBlobContentResult,
} from './graphQlResponseHelpers'
import { commonWebGraphQlResults, createViewerSettingsGraphQLOverride } from './graphQlResults'
import { percySnapshotWithVariants } from './utils'
describe('Blob viewer', () => {
let driver: Driver
@ -161,366 +156,4 @@ describe('Blob viewer', () => {
await driver.assertWindowLocation(`/${repositoryName}/-/blob/${fileName}?L1-3`)
})
})
// Describes the ways the blob viewer can be extended through Sourcegraph extensions.
describe('extensibility', () => {
beforeEach(() => {
const userSettings: Settings = {
extensions: {
'test/test': true,
},
}
const extensionManifest: ExtensionManifest = {
url: new URL('/-/static/extension/0001-test-test.js?hash--test-test', driver.sourcegraphBaseUrl).href,
activationEvents: ['*'],
}
testContext.overrideGraphQL({
...commonBlobGraphQlResults,
ViewerSettings: () => ({
viewerSettings: {
__typename: 'SettingsCascade',
final: JSON.stringify(userSettings),
subjects: [
{
__typename: 'User',
displayName: 'Test User',
id: 'TestUserSettingsID',
latestSettings: {
id: 123,
contents: JSON.stringify(userSettings),
},
username: 'test',
viewerCanAdminister: true,
settingsURL: '/users/test/settings',
},
],
},
}),
Blob: () => ({
repository: {
commit: {
blob: null,
file: {
__typename: 'VirtualFile',
content: '// Log to console\nconsole.log("Hello world")',
totalLines: 2,
richHTML: '',
highlight: {
aborted: false,
html:
// Note: whitespace in this string is significant.
'<table><tbody><tr>' +
'<td class="line" data-line="1"/>' +
'<td class="code"><span class="hl-source hl-js hl-react"><span class="hl-comment hl-line hl-double-slash hl-js">' +
'<span class="hl-punctuation hl-definition hl-comment hl-js">//</span> ' +
'Log to console\n</span></span></td></tr>' +
'<tr><td class="line" data-line="2"/>' +
'<td class="code"><span class="hl-source hl-js hl-react"><span class="hl-meta hl-function-call hl-method hl-js">' +
'<span class="hl-support hl-type hl-object hl-console hl-js test-console-token">console</span>' +
'<span class="hl-punctuation hl-accessor hl-js">.</span>' +
'<span class="hl-support hl-function hl-console hl-js test-log-token">log</span>' +
'<span class="hl-meta hl-group hl-js"><span class="hl-punctuation hl-section hl-group hl-begin hl-js">(</span>' +
'<span class="hl-meta hl-string hl-js"><span class="hl-string hl-quoted hl-double hl-js">' +
'<span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>' +
'Hello world' +
'<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span></span>' +
'<span class="hl-punctuation hl-section hl-group hl-end hl-js">)</span></span>\n</span></span></td></tr></tbody></table>',
lsif: '',
},
},
},
},
}),
Extensions: () => ({
extensionRegistry: {
__typename: 'ExtensionRegistry',
extensions: {
nodes: [
{
id: 'test',
extensionID: 'test/test',
manifest: {
jsonFields: extensionManifest,
},
},
],
},
},
}),
})
// Serve a mock extension bundle with a simple hover provider
testContext.server
.get(new URL(extensionManifest.url, driver.sourcegraphBaseUrl).href)
.intercept((request, response) => {
function extensionBundle(): void {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const sourcegraph = require('sourcegraph') as typeof import('sourcegraph')
function activate(context: ExtensionContext): void {
context.subscriptions.add(
sourcegraph.languages.registerHoverProvider([{ language: 'typescript' }], {
provideHover: () => ({
contents: {
kind: sourcegraph.MarkupKind.Markdown,
value: 'Test hover content',
},
}),
})
)
}
exports.activate = activate
}
// Create an immediately-invoked function expression for the extensionBundle function
const extensionBundleString = `(${extensionBundle.toString()})()`
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
})
})
it('truncates long file paths properly', async () => {
await driver.page.goto(
`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/this_is_a_long_file_path/apps/rest-showcase/src/main/java/org/demo/rest/example/OrdersController.java`
)
await driver.page.waitForSelector('[data-testid="repo-blob"]')
await driver.page.waitForSelector('.test-breadcrumb')
// Uncomment this snapshot once https://github.com/sourcegraph/sourcegraph/issues/15126 is resolved
// await percySnapshot(driver.page, this.test!.fullTitle())
})
it.skip('shows a hover overlay from a hover provider when a token is hovered', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/${fileName}`)
await driver.page.waitForSelector('[data-testid="repo-blob"]')
// TODO
})
it.skip('gets displayed when navigating to a URL with a token position', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/github.com/sourcegraph/test/-/test.ts#2:9`)
// TODO
})
// Disabled because it's flaky. See: https://github.com/sourcegraph/sourcegraph/issues/31806
it.skip('properly displays reference panel for URIs with spaces', async () => {
const repositoryName = 'github.com/sourcegraph/test%20repo'
const files = ['test.ts', 'test spaces.ts']
const commitID = '1234'
const userSettings: Settings = {
extensions: {
'test/references': true,
},
}
const extensionManifest: ExtensionManifest = {
url: new URL(
'/-/static/extension/0001-test-references.js?hash--test-references',
driver.sourcegraphBaseUrl
).href,
activationEvents: ['*'],
}
testContext.overrideGraphQL({
...commonBlobGraphQlResults,
ViewerSettings: () => ({
viewerSettings: {
__typename: 'SettingsCascade',
final: JSON.stringify(userSettings),
subjects: [
{
__typename: 'User',
displayName: 'Test User',
id: 'TestUserSettingsID',
latestSettings: {
id: 123,
contents: JSON.stringify(userSettings),
},
username: 'test',
viewerCanAdminister: true,
settingsURL: '/users/test/settings',
},
],
},
}),
Extensions: () => ({
extensionRegistry: {
__typename: 'ExtensionRegistry',
extensions: {
nodes: [
{
id: 'test',
extensionID: 'test/references',
manifest: {
jsonFields: extensionManifest,
},
},
],
},
},
}),
Blob: ({ filePath }) => ({
repository: {
commit: {
blob: null,
file: {
__typename: 'VirtualFile',
content: `// file path: ${filePath}\nconsole.log("Hello world")`,
totalLines: 2,
richHTML: '',
highlight: {
aborted: false,
html:
'<table><tbody><tr><td class="line" data-line="1"/><td class="code">' +
'<span class="hl-source hl-js hl-react"><span class="hl-comment hl-line hl-double-slash hl-js">' +
'<span class="hl-punctuation hl-definition hl-comment hl-js">//</span>' +
` file path: ${filePath}\n` +
'</span></span></td></tr>' +
'<tr><td class="line" data-line="2"/>' +
'<td class="code"><span class="hl-source hl-js hl-react">' +
'<span class="hl-meta hl-function-call hl-method hl-js">' +
'<span class="hl-support hl-type hl-object hl-console hl-js test-console-token">console</span>' +
'<span class="hl-punctuation hl-accessor hl-js">.</span>' +
'<span class="hl-support hl-function hl-console hl-js test-log-token">log</span>' +
'<span class="hl-meta hl-group hl-js"><span class="hl-punctuation hl-section hl-group hl-begin hl-js">(</span>' +
'<span class="hl-meta hl-string hl-js"><span class="hl-string hl-quoted hl-double hl-js">' +
'<span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>' +
'Hello world' +
'<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span></span>' +
'<span class="hl-punctuation hl-section hl-group hl-end hl-js">)</span></span>' +
'\n</span></span></td></tr></tbody></table>',
lsif: '',
lineRanges: [],
},
},
},
},
}),
TreeEntries: () => createTreeEntriesResult(repositorySourcegraphUrl, files),
ResolveRepoRev: () => createResolveRepoRevisionResult(repositorySourcegraphUrl, commitID),
HighlightedFile: ({ filePath }) => ({
repository: {
commit: {
file: {
isDirectory: false,
richHTML: '',
highlight: {
aborted: false,
html:
'<table><tbody><tr><td class="line" data-line="1"/><td class="code">' +
'<span class="hl-source hl-js hl-react"><span class="hl-comment hl-line hl-double-slash hl-js">' +
'<span class="hl-punctuation hl-definition hl-comment hl-js">//</span>' +
` file path: ${filePath}\n` +
'</span></span></td></tr>' +
'<tr><td class="line" data-line="2"/>' +
'<td class="code"><span class="hl-source hl-js hl-react">' +
'<span class="hl-meta hl-function-call hl-method hl-js">' +
'<span class="hl-support hl-type hl-object hl-console hl-js test-console-token">console</span>' +
'<span class="hl-punctuation hl-accessor hl-js">.</span>' +
'<span class="hl-support hl-function hl-console hl-js test-log-token">log</span>' +
'<span class="hl-meta hl-group hl-js"><span class="hl-punctuation hl-section hl-group hl-begin hl-js">(</span>' +
'<span class="hl-meta hl-string hl-js"><span class="hl-string hl-quoted hl-double hl-js">' +
'<span class="hl-punctuation hl-definition hl-string hl-begin hl-js">&quot;</span>' +
'Hello world' +
'<span class="hl-punctuation hl-definition hl-string hl-end hl-js">&quot;</span></span></span>' +
'<span class="hl-punctuation hl-section hl-group hl-end hl-js">)</span></span>' +
'\n</span></span></td></tr></tbody></table>',
lineRanges: [],
},
},
},
},
}),
FetchCommits: () => ({
node: { __typename: 'GitCommit' },
}),
// Required for definition provider,
ResolveRawRepoName: () => ({
repository: {
mirrorInfo: {
cloned: true,
},
uri: repositoryName,
},
}),
})
// Serve a mock extension bundle with a simple reference provider
testContext.server
.get(new URL(extensionManifest.url, driver.sourcegraphBaseUrl).href)
.intercept((request, response) => {
function extensionBundle(): void {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const sourcegraph = require('sourcegraph') as typeof import('sourcegraph')
function activate(context: ExtensionContext): void {
context.subscriptions.add(
sourcegraph.languages.registerReferenceProvider(['*'], {
provideReferences: () => [
new sourcegraph.Location(
new URL('git://github.com/sourcegraph/test%20repo?1234#test%20spaces.ts'),
new sourcegraph.Range(
new sourcegraph.Position(0, 0),
new sourcegraph.Position(1, 0)
)
),
],
})
)
// We aren't testing definition providers in this test; we include a definition provider
// because the "Find references" action isn't displayed unless a definition is found
context.subscriptions.add(
sourcegraph.languages.registerDefinitionProvider(['*'], {
provideDefinition: () =>
new sourcegraph.Location(
new URL('git://github.com/sourcegraph/test%20repo?1234#test%20spaces.ts'),
new sourcegraph.Range(
new sourcegraph.Position(0, 0),
new sourcegraph.Position(1, 0)
)
),
})
)
}
exports.activate = activate
}
// Create an immediately-invoked function expression for the extensionBundle function
const extensionBundleString = `(${extensionBundle.toString()})()`
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
})
// TEMPORARY: Mock `Date.now` to prevent temporary Firefox from rendering.
await driver.page.evaluateOnNewDocument(() => {
// Number of ms between Unix epoch and July 1, 2020 (outside of Firefox campaign range)
const mockMs = new Date('July 1, 2020 00:00:00 UTC').getTime()
Date.now = () => mockMs
})
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/test.ts`)
// Click on "log" in "console.log()" in line 2
await driver.page.waitForSelector('.test-log-token', { visible: true })
await driver.page.click('.test-log-token')
// Click 'Find references'
await driver.page.waitForSelector('.test-tooltip-find-references', { visible: true })
await driver.page.click('.test-tooltip-find-references')
await driver.page.waitForSelector('.test-file-match-children-item', { visible: true })
await percySnapshotWithVariants(driver.page, 'Blob Reference Panel', { waitForCodeHighlighting: true })
await accessibilityAudit(driver.page)
// Click on the first reference
await driver.page.click('.test-file-match-children-item')
// Assert that the first line of code has text content which contains: 'file path: test spaces.ts'
try {
await driver.page.waitForFunction(
() =>
document
.querySelector('[data-testid="repo-blob"] [data-line="1"]')
?.nextElementSibling?.textContent?.includes('file path: test spaces.ts'),
{ timeout: 5000 }
)
} catch {
throw new Error('Expected to navigate to file after clicking on link in references panel')
}
})
})
})

View File

@ -2,11 +2,8 @@ import assert from 'assert'
import { ElementHandle, MouseButton } from 'puppeteer'
import type { ExtensionContext } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
import { JsonDocument, SyntaxKind } from '@sourcegraph/shared/src/codeintel/scip'
import { SharedGraphQlOperations } from '@sourcegraph/shared/src/graphql-operations'
import { ExtensionManifest } from '@sourcegraph/shared/src/schema/extensionSchema'
import { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
import { Driver, createDriverForTest, percySnapshot } from '@sourcegraph/shared/src/testing/driver'
import { afterEachSaveScreenshotIfFailed } from '@sourcegraph/shared/src/testing/screenshotReporter'
@ -284,136 +281,6 @@ describe('CodeMirror blob view', () => {
})
})
// Describes the ways the blob viewer can be extended through Sourcegraph extensions.
describe('extensibility', () => {
beforeEach(() => {
testContext.overrideJsContext({ enableLegacyExtensions: true })
})
describe('hovercards', () => {
beforeEach(() => {
const {
graphqlResults: extensionGraphQlResult,
intercept,
userSettings,
} = createExtensionData([
{
id: 'test',
extensionID: 'test/test',
extensionManifest: {
url: new URL(
'/-/static/extension/0001-test-test.js?hash--test-test',
driver.sourcegraphBaseUrl
).href,
activationEvents: ['*'],
},
bundle: function extensionBundle(): void {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const sourcegraph = require('sourcegraph') as typeof import('sourcegraph')
function activate(context: ExtensionContext): void {
context.subscriptions.add(
sourcegraph.languages.registerHoverProvider([{ language: 'typescript' }], {
provideHover: () => ({
contents: {
kind: sourcegraph.MarkupKind.Markdown,
value: 'Test hover content',
},
}),
})
)
}
exports.activate = activate
},
},
])
testContext.overrideGraphQL({
...commonBlobGraphQlResults,
...createViewerSettingsGraphQLOverride({
user: {
...userSettings,
experimentalFeatures: {
enableCodeMirrorFileView: true,
},
},
}),
...extensionGraphQlResult,
})
// Serve a mock extension bundle with a simple hover provider
intercept(testContext, driver)
})
it('shows a hover overlay from a hover provider when a token is hovered', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}${filePaths['test.ts']}`)
await waitForView()
await driver.page.hover(wordSelector)
await driver.page.waitForSelector('.cm-code-intel-hovercard')
assert.strictEqual(
await driver.page.evaluate(
(): string =>
document.querySelector('[data-testid="hover-overlay-contents"]')?.textContent?.trim() ?? ''
),
'Test hover content',
'hovercard is visible with correct content'
)
await driver.page.hover(lineAt(5))
try {
await driver.page.waitForSelector('.cm-code-intel-hovercard', { hidden: true })
} catch {
throw new Error('Timeout waiting for hovercard to disappear')
}
})
it('pins a hovercard and unpins hovercards', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}${filePaths['test.ts']}`)
await waitForView()
await driver.page.hover(wordSelector)
await driver.page.waitForSelector('.cm-code-intel-hovercard [data-testid="hover-copy-link"]')
await driver.page.click('.cm-code-intel-hovercard [data-testid="hover-copy-link"]')
// URL gets updated
await driver.assertWindowLocation(`${filePaths['test.ts']}?L1:1&popover=pinned`)
// Close button is visible
await driver.page.waitForSelector('.cm-code-intel-hovercard [aria-label="Close"]')
// Hovercard stay open when moving the mouse away
await driver.page.hover(lineAt(5))
await driver.page.waitForSelector('.cm-code-intel-hovercard')
// Closes hovercard when clicking on another line
await driver.page.click(lineAt(5))
try {
await driver.page.waitForSelector('.cm-code-intel-hovercard', { hidden: true })
} catch {
throw new Error('Timeout waiting for hovercard to close after selecting another line')
}
// Opens pinned hovecard when navigating back
await driver.page.goBack()
await driver.page.waitForSelector('.cm-code-intel-hovercard')
// Closes hover card when clicking the close button
await driver.page.click('.cm-code-intel-hovercard [aria-label="Close"]')
try {
await driver.page.waitForSelector('.cm-code-intel-hovercard', { hidden: true })
} catch {
throw new Error('Timeout waiting for hovercard to close after clicking close button')
}
})
it('opens a pinned hovercard on page load', async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}${filePaths['test.ts']}?L1:1&popover=pinned`)
await waitForView()
await driver.page.waitForSelector('.cm-code-intel-hovercard')
})
})
})
describe('in-document search', () => {
const { graphqlResults: blobGraphqlResults, filePaths } = createBlobPageData({
repoName,
@ -578,54 +445,3 @@ function createBlobPageData<T extends BlobInfo>({
},
}
}
interface MockExtension {
id: string
extensionID: string
extensionManifest: ExtensionManifest
/**
* A function whose body is a Sourcegraph extension.
*
* Bundle must import 'sourcegraph' (e.g. `const sourcegraph = require('sourcegraph')`)
* */
bundle: () => void
}
function createExtensionData(extensions: MockExtension[]): {
intercept: (testContext: WebIntegrationTestContext, driver: Driver) => void
graphqlResults: Pick<SharedGraphQlOperations, 'Extensions'>
userSettings: Required<Pick<Settings, 'extensions'>>
} {
return {
intercept(testContext: WebIntegrationTestContext, driver: Driver) {
for (const extension of extensions) {
testContext.server
.get(new URL(extension.extensionManifest.url, driver.sourcegraphBaseUrl).href)
.intercept((_request, response) => {
// Create an immediately-invoked function expression for the extensionBundle function
const extensionBundleString = `(${extension.bundle.toString()})()`
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
})
}
},
graphqlResults: {
Extensions: () => ({
extensionRegistry: {
__typename: 'ExtensionRegistry',
extensions: {
nodes: extensions.map(extension => ({
...extension,
manifest: { jsonFields: extension.extensionManifest },
})),
},
},
}),
},
userSettings: {
extensions: extensions.reduce((extensionsSettings: Record<string, boolean>, mockExtension) => {
extensionsSettings[mockExtension.extensionID] = true
return extensionsSettings
}, {}),
},
}
}

View File

@ -49,5 +49,4 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
xhrHeaders: {},
authProviders: [builtinAuthProvider],
authMinPasswordLength: 12,
enableLegacyExtensions: false,
})

View File

@ -201,9 +201,6 @@ export interface SourcegraphContext extends Pick<Required<SiteConfiguration>, 'e
/** Whether the product research sign-up page is enabled on the site. */
productResearchPageEnabled: boolean
/** Whether the use of extensions are enabled. (Doesn't affect code intel and git extras.) */
enableLegacyExtensions?: boolean
/** Contains information about the product license. */
licenseInfo?: {
currentPlan: 'old-starter-0' | 'old-enterprise-0' | 'team-0' | 'enterprise-0' | 'business-0' | 'enterprise-1'

View File

@ -5,7 +5,6 @@ import {
mockFetchSearchContexts,
mockGetUserSearchContextNamespaces,
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
import { extensionsController } from '@sourcegraph/shared/src/testing/searchTestHelpers'
import { Grid, H3 } from '@sourcegraph/wildcard'
import { AuthenticatedUser } from '../auth'
@ -21,7 +20,6 @@ const defaultProps: GlobalNavbarProps = {
final: null,
subjects: null,
},
extensionsController,
telemetryService: NOOP_TELEMETRY_SERVICE,
globbing: false,
platformContext: {} as any,
@ -49,7 +47,6 @@ const allNavItemsProps: Partial<GlobalNavbarProps> = {
batchChangesExecutionEnabled: true,
batchChangesWebhookLogsEnabled: true,
codeInsightsEnabled: true,
enableLegacyExtensions: true,
}
const allAuthenticatedNavItemsProps: Partial<GlobalNavbarProps> = {

View File

@ -5,7 +5,7 @@ import {
mockFetchSearchContexts,
mockGetUserSearchContextNamespaces,
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
import { extensionsController, NOOP_SETTINGS_CASCADE } from '@sourcegraph/shared/src/testing/searchTestHelpers'
import { NOOP_SETTINGS_CASCADE } from '@sourcegraph/shared/src/testing/searchTestHelpers'
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
import { GlobalNavbar } from './GlobalNavbar'
@ -15,7 +15,6 @@ jest.mock('../components/branding/BrandLogo', () => ({ BrandLogo: 'BrandLogo' })
const PROPS: React.ComponentProps<typeof GlobalNavbar> = {
authenticatedUser: null,
extensionsController,
isSourcegraphDotCom: false,
isSourcegraphApp: false,
platformContext: {} as any,
@ -41,16 +40,6 @@ const PROPS: React.ComponentProps<typeof GlobalNavbar> = {
}
describe('GlobalNavbar', () => {
const origContext = window.context
beforeEach(() => {
window.context = {
enableLegacyExtensions: false,
} as any
})
afterEach(() => {
window.context = origContext
})
test('default', () => {
const { asFragment } = renderWithBrandedContext(
<MockedTestProvider>

View File

@ -6,9 +6,7 @@ import BookOutlineIcon from 'mdi-react/BookOutlineIcon'
import MagnifyIcon from 'mdi-react/MagnifyIcon'
import { useLocation } from 'react-router-dom'
import { ContributableMenu } from '@sourcegraph/client-api'
import { isErrorLike, isMacPlatform } from '@sourcegraph/common'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { shortcutDisplayName } from '@sourcegraph/shared/src/keyboardShortcuts'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
@ -27,7 +25,6 @@ import { CodeMonitoringProps } from '../codeMonitoring'
import { CodyIcon } from '../cody/CodyIcon'
import { BrandLogo } from '../components/branding/BrandLogo'
import { getFuzzyFinderFeatureFlags } from '../components/fuzzyFinder/FuzzyFinderFeatureFlag'
import { WebCommandListPopoverButton } from '../components/shared'
import { useFeatureFlag } from '../featureFlags/useFeatureFlag'
import { useRoutesMatch } from '../hooks'
import { CodeInsightsProps } from '../insights/types'
@ -50,7 +47,6 @@ import styles from './GlobalNavbar.module.scss'
export interface GlobalNavbarProps
extends SettingsCascadeProps<Settings>,
PlatformContextProps,
ExtensionsControllerProps,
TelemetryProps,
SearchContextInputProps,
CodeInsightsProps,
@ -67,7 +63,6 @@ export interface GlobalNavbarProps
globbing: boolean
isSearchAutoFocusRequired?: boolean
isRepositoryRelatedPage?: boolean
enableLegacyExtensions?: boolean
branding?: typeof window.context.branding
showKeyboardShortcutsHelp: () => void
showFeedbackModal: () => void
@ -132,8 +127,6 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
searchContextsEnabled,
codeMonitoringEnabled,
notebooksEnabled,
extensionsController,
enableLegacyExtensions,
showFeedbackModal,
...props
}) => {
@ -293,16 +286,6 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
</NavAction>
)}
{fuzzyFinderNavbar && FuzzyFinderNavItem(props.setFuzzyFinderIsVisible)}
{props.authenticatedUser && extensionsController !== null && enableLegacyExtensions && (
<NavAction>
<WebCommandListPopoverButton
{...props}
extensionsController={extensionsController}
location={location}
menu={ContributableMenu.CommandPalette}
/>
</NavAction>
)}
{props.authenticatedUser?.siteAdmin && (
<NavAction>
<StatusMessagesNavItem />
@ -325,7 +308,7 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
>
Sign in
</Button>
{!isSourcegraphDotCom && window.context.allowSignup && (
{!isSourcegraphDotCom && window.context?.allowSignup && (
<ButtonLink to="/sign-up" variant="primary" size="sm">
Sign up
</ButtonLink>

View File

@ -71,10 +71,10 @@ export function createPlatformContext(): PlatformContext {
},
getGraphQLClient: getWebGraphQLClient,
requestGraphQL: ({ request, variables }) => requestGraphQL(request, variables),
createExtensionHost: async () =>
(await import('@sourcegraph/shared/src/api/extension/worker')).createExtensionHost(),
createExtensionHost: () => {
throw new Error('extensions are no longer supported in the web app')
},
urlToFile: toPrettyWebBlobURL,
getScriptURLForExtension: () => undefined,
sourcegraphURL: window.context.externalURL,
clientApplication: 'sourcegraph',
telemetryService: eventLogger,

View File

@ -3,7 +3,7 @@ import React, { FC, Suspense, useEffect, useMemo, useState } from 'react'
import { mdiSourceRepository } from '@mdi/js'
import classNames from 'classnames'
import { escapeRegExp } from 'lodash'
import { matchPath, Location, useLocation, Route, Routes } from 'react-router-dom'
import { Location, useLocation, Route, Routes } from 'react-router-dom'
import { NEVER, of } from 'rxjs'
import { catchError, switchMap } from 'rxjs/operators'
@ -33,7 +33,6 @@ import { CodeIntelligenceProps } from '../codeintel'
import { BreadcrumbSetters, BreadcrumbsProps } from '../components/Breadcrumbs'
import { ErrorBoundary } from '../components/ErrorBoundary'
import { HeroPage } from '../components/HeroPage'
import { ActionItemsBarProps, useWebActionItems } from '../extensions/components/ActionItemsBar'
import { ExternalLinkFields, RepositoryFields } from '../graphql-operations'
import { CodeInsightsProps } from '../insights/types'
import { NotebookProps } from '../notebooks'
@ -45,7 +44,6 @@ import { parseBrowserRepoURL } from '../util/url'
import { GoToCodeHostAction } from './actions/GoToCodeHostAction'
import { fetchFileExternalLinks, ResolvedRevision, resolveRepoRevision } from './backend'
import { RepoContainerError } from './RepoContainerError'
import { compareSpecPath } from './repoContainerRoutes'
import { RepoHeader, RepoHeaderActionButton, RepoHeaderContributionsLifecycleProps } from './RepoHeader'
import { RepoHeaderContributionPortal } from './RepoHeaderContributionPortal'
import {
@ -53,7 +51,7 @@ import {
RepoRevisionContainerContext,
RepoRevisionContainerRoute,
} from './RepoRevisionContainer'
import { commitsPath, repoSplat } from './repoRevisionContainerRoutes'
import { repoSplat } from './repoRevisionContainerRoutes'
import { RepoSettingsAreaRoute } from './settings/RepoSettingsArea'
import { RepoSettingsSideBarGroup } from './settings/RepoSettingsSidebar'
import { repoSettingsAreaPath } from './settings/routes'
@ -74,7 +72,6 @@ export interface RepoContainerContext
TelemetryProps,
Pick<SearchContextProps, 'selectedSearchContextSpec' | 'searchContextsEnabled'>,
BreadcrumbSetters,
ActionItemsBarProps,
SearchStreamingProps,
Pick<StreamingSearchResultsListProps, 'fetchHighlightedFileLineRanges'>,
CodeIntelligenceProps,
@ -273,23 +270,6 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
})
}, [revision, filePath, repoName, onNavbarQueryChange, globbing])
const { useActionItemsBar, useActionItemsToggle } = useWebActionItems()
// render go to the code host action on all the repo container routes and on all compare spec routes
const isGoToCodeHostActionVisible = useMemo(() => {
if (!window.context.enableLegacyExtensions) {
return true
}
const paths = [
...repoContainerRoutes.map(route => route.path),
compareSpecPath,
repoSettingsAreaPath,
commitsPath,
]
return paths.some(path => matchPath(path, location.pathname))
}, [repoContainerRoutes, location.pathname])
const isError = isErrorLike(repoOrError) || isErrorLike(resolvedRevisionOrError)
// if revision for given repo does not resolve then we still proceed to render settings routes
@ -327,7 +307,6 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
repoName,
revision: revision || '',
resolvedRevision,
useActionItemsBar,
}
const perforceCodeHostUrlToSwarmUrlMap =
@ -347,7 +326,6 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
<div className={classNames('w-100 d-flex flex-column', styles.repoContainer)}>
<RepoHeader
actionButtons={props.repoHeaderActionButtons}
useActionItemsToggle={useActionItemsToggle}
breadcrumbs={props.breadcrumbs}
repoName={repoName}
revision={revision}
@ -355,36 +333,33 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
settingsCascade={props.settingsCascade}
authenticatedUser={authenticatedUser}
platformContext={props.platformContext}
extensionsController={extensionsController}
telemetryService={props.telemetryService}
/>
{isGoToCodeHostActionVisible && (
<RepoHeaderContributionPortal
position="right"
priority={2}
id="go-to-code-host"
{...repoHeaderContributionsLifecycleProps}
>
{({ actionType }) => (
<GoToCodeHostAction
repo={repo}
repoName={repoName}
// We need a revision to generate code host URLs, if revision isn't available, we use the default branch or HEAD.
revision={rawRevision || repo?.defaultBranch?.displayName || 'HEAD'}
filePath={filePath}
commitRange={commitRange}
range={range}
position={position}
perforceCodeHostUrlToSwarmUrlMap={perforceCodeHostUrlToSwarmUrlMap}
fetchFileExternalLinks={fetchFileExternalLinks}
actionType={actionType}
source="repoHeader"
key="go-to-code-host"
externalLinks={externalLinks}
/>
)}
</RepoHeaderContributionPortal>
)}
<RepoHeaderContributionPortal
position="right"
priority={2}
id="go-to-code-host"
{...repoHeaderContributionsLifecycleProps}
>
{({ actionType }) => (
<GoToCodeHostAction
repo={repo}
repoName={repoName}
// We need a revision to generate code host URLs, if revision isn't available, we use the default branch or HEAD.
revision={rawRevision || repo?.defaultBranch?.displayName || 'HEAD'}
filePath={filePath}
commitRange={commitRange}
range={range}
position={position}
perforceCodeHostUrlToSwarmUrlMap={perforceCodeHostUrlToSwarmUrlMap}
fetchFileExternalLinks={fetchFileExternalLinks}
actionType={actionType}
source="repoHeader"
key="go-to-code-host"
externalLinks={externalLinks}
/>
)}
</RepoHeaderContributionPortal>
{isCodeIntelRepositoryBadgeVisible && (
<RepoHeaderContributionPortal

View File

@ -8,7 +8,6 @@ import { Button, H1, H2, Icon, Link } from '@sourcegraph/wildcard'
import { BrandedStory } from '@sourcegraph/wildcard/src/stories'
import { AuthenticatedUser } from '../auth'
import { SourcegraphContext } from '../jscontext'
import { GoToPermalinkAction } from './actions/GoToPermalinkAction'
import { FilePathBreadcrumbs } from './FilePathBreadcrumbs'
@ -25,10 +24,6 @@ const mockUser = {
siteAdmin: true,
} as AuthenticatedUser
if (!window.context) {
window.context = { enableLegacyExtensions: false } as SourcegraphContext & Mocha.SuiteFunction
}
const decorator: DecoratorFn = story => (
<BrandedStory initialEntries={['/github.com/sourcegraph/sourcegraph/-/tree/']} styles={webStyles}>
{() => <div className="container mt-3">{story()}</div>}
@ -70,12 +65,6 @@ export const Default: Story = () => (
</div>
</>
)
const useActionItemsToggle = () => ({
isOpen: false,
toggle: () => null,
toggleReference: () => null,
barInPage: false,
})
const onLifecyclePropsChange = (lifecycleProps: RepoHeaderContributionsLifecycleProps) => {
lifecycleProps.repoHeaderContributionsLifecycleProps?.onRepoHeaderContributionAdd({
@ -152,7 +141,6 @@ const createBreadcrumbs = (path: string) => [
const createProps = (path: string, forceWrap: boolean = false): React.ComponentProps<typeof RepoHeader> => ({
actionButtons: [],
useActionItemsToggle,
breadcrumbs: createBreadcrumbs(path),
repoName: 'sourcegraph/sourcegraph',
revision: 'main',
@ -160,7 +148,6 @@ const createProps = (path: string, forceWrap: boolean = false): React.ComponentP
settingsCascade: EMPTY_SETTINGS_CASCADE,
authenticatedUser: mockUser,
platformContext: {} as any,
extensionsController: null,
telemetryService: NOOP_TELEMETRY_SERVICE,
forceWrap,
})

View File

@ -7,13 +7,11 @@ import { useLocation } from 'react-router-dom'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { SettingsCascadeOrError } from '@sourcegraph/shared/src/settings/settings'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { Menu, MenuList, Position, Icon } from '@sourcegraph/wildcard'
import { AuthenticatedUser } from '../auth'
import { Breadcrumbs, BreadcrumbsProps } from '../components/Breadcrumbs'
import { ErrorBoundary } from '../components/ErrorBoundary'
import type { ActionItemsToggleProps } from '../extensions/components/ActionItemsBar'
import { ActionButtonDescriptor } from '../util/contributions'
import { useBreakpoint } from '../util/dom'
@ -21,8 +19,6 @@ import { RepoHeaderActionDropdownToggle } from './components/RepoHeaderActions'
import styles from './RepoHeader.module.scss'
const ActionItemsToggle = lazyComponent(() => import('../extensions/components/ActionItemsBar'), 'ActionItemsToggle')
/**
* Stores the list of RepoHeaderContributions, manages addition/deletion, and ensures they are sorted.
*
@ -121,7 +117,7 @@ export interface RepoHeaderContext {
export interface RepoHeaderActionButton extends ActionButtonDescriptor<RepoHeaderContext> {}
interface Props extends PlatformContextProps, TelemetryProps, BreadcrumbsProps, ActionItemsToggleProps {
interface Props extends PlatformContextProps, TelemetryProps, BreadcrumbsProps {
/**
* An array of render functions for action buttons that can be configured *in addition* to action buttons
* contributed through {@link RepoHeaderContributionsLifecycleProps} and through extensions.
@ -251,14 +247,6 @@ export const RepoHeader: React.FunctionComponent<React.PropsWithChildren<Props>>
</li>
</ul>
)}
{window.context.enableLegacyExtensions ? (
<ul className="navbar-nav">
<ActionItemsToggle
useActionItemsToggle={props.useActionItemsToggle}
extensionsController={props.extensionsController}
/>
</ul>
) : null}
</ErrorBoundary>
</nav>
)

View File

@ -16,7 +16,6 @@ import { AuthenticatedUser } from '../auth'
import { BatchChangesProps } from '../batches'
import { CodeIntelligenceProps } from '../codeintel'
import { BreadcrumbSetters } from '../components/Breadcrumbs'
import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar'
import { RepositoryFields } from '../graphql-operations'
import { CodeInsightsProps } from '../insights/types'
import { NotebookProps } from '../notebooks'
@ -49,7 +48,6 @@ export interface RepoRevisionContainerContext
Pick<SearchContextProps, 'selectedSearchContextSpec' | 'searchContextsEnabled'>,
RevisionSpec,
BreadcrumbSetters,
ActionItemsBarProps,
SearchStreamingProps,
Pick<StreamingSearchResultsListProps, 'fetchHighlightedFileLineRanges'>,
BatchChangesProps,
@ -81,7 +79,6 @@ interface RepoRevisionContainerProps
Pick<SearchContextProps, 'selectedSearchContextSpec' | 'searchContextsEnabled'>,
RevisionSpec,
BreadcrumbSetters,
ActionItemsBarProps,
SearchStreamingProps,
Pick<StreamingSearchResultsListProps, 'fetchHighlightedFileLineRanges'>,
CodeIntelligenceProps,

View File

@ -116,7 +116,6 @@ export const RepositoryFileTreePage: FC<RepositoryFileTreePageProps> = props =>
globbing={globbing}
repo={repo}
repoName={repoName}
useActionItemsBar={context.useActionItemsBar}
isSourcegraphDotCom={context.isSourcegraphDotCom}
className={styles.pageContent}
/>

View File

@ -363,38 +363,33 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, ..
const alwaysRender = (
<>
<PageTitle title={getPageTitle()} />
{!window.context.enableLegacyExtensions ? (
<>
{window.context.isAuthenticatedUser && (
<RepoHeaderContributionPortal
position="right"
priority={112}
id="open-in-editor-action"
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
>
{({ actionType }) => (
<OpenInEditorActionItem
platformContext={props.platformContext}
externalServiceType={props.repoServiceType}
actionType={actionType}
source="repoHeader"
/>
)}
</RepoHeaderContributionPortal>
{window.context.isAuthenticatedUser && (
<RepoHeaderContributionPortal
position="right"
priority={112}
id="open-in-editor-action"
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
>
{({ actionType }) => (
<OpenInEditorActionItem
platformContext={props.platformContext}
externalServiceType={props.repoServiceType}
actionType={actionType}
source="repoHeader"
/>
)}
<RepoHeaderContributionPortal
position="right"
priority={111}
id="toggle-blame-action"
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
>
{({ actionType }) => (
<ToggleBlameAction actionType={actionType} source="repoHeader" renderMode={renderMode} />
)}
</RepoHeaderContributionPortal>
</>
) : null}
</RepoHeaderContributionPortal>
)}
<RepoHeaderContributionPortal
position="right"
priority={111}
id="toggle-blame-action"
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
>
{({ actionType }) => (
<ToggleBlameAction actionType={actionType} source="repoHeader" renderMode={renderMode} />
)}
</RepoHeaderContributionPortal>
<RepoHeaderContributionPortal
position="right"
priority={20}

View File

@ -1,7 +1,5 @@
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar'
import { RepoRevisionWrapper } from './components/RepoRevision'
import { RepoContainerRoute } from './RepoContainer'
@ -17,10 +15,6 @@ const RepositoryReleasesArea = lazyComponent(
)
const RepositoryCompareArea = lazyComponent(() => import('./compare/RepositoryCompareArea'), 'RepositoryCompareArea')
const RepositoryStatsArea = lazyComponent(() => import('./stats/RepositoryStatsArea'), 'RepositoryStatsArea')
const ActionItemsBar = lazyComponent<ActionItemsBarProps, 'ActionItemsBar'>(
() => import('../extensions/components/ActionItemsBar'),
'ActionItemsBar'
)
export const compareSpecPath = '/-/compare/*'
@ -30,15 +24,6 @@ export const repoContainerRoutes: readonly RepoContainerRoute[] = [
render: context => (
<RepoRevisionWrapper>
<RepositoryCommitPage {...context} />
{window.context.enableLegacyExtensions && (
<ActionItemsBar
extensionsController={context.extensionsController}
platformContext={context.platformContext}
useActionItemsBar={context.useActionItemsBar}
telemetryService={context.telemetryService}
source="commit"
/>
)}
</RepoRevisionWrapper>
),
},
@ -55,15 +40,6 @@ export const repoContainerRoutes: readonly RepoContainerRoute[] = [
render: context => (
<RepoRevisionWrapper>
<RepositoryCompareArea {...context} />
{window.context.enableLegacyExtensions && (
<ActionItemsBar
extensionsController={context.extensionsController}
platformContext={context.platformContext}
useActionItemsBar={context.useActionItemsBar}
telemetryService={context.telemetryService}
source="compare"
/>
)}
</RepoRevisionWrapper>
),
},

View File

@ -2,19 +2,12 @@ import { TraceSpanProvider } from '@sourcegraph/observability-client'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { LoadingSpinner } from '@sourcegraph/wildcard'
import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar'
import { RepoRevisionContainerRoute } from './RepoRevisionContainer'
const RepositoryCommitsPage = lazyComponent(() => import('./commits/RepositoryCommitsPage'), 'RepositoryCommitsPage')
const RepositoryFileTreePage = lazyComponent(() => import('./RepositoryFileTreePage'), 'RepositoryFileTreePage')
const ActionItemsBar = lazyComponent<ActionItemsBarProps, 'ActionItemsBar'>(
() => import('../extensions/components/ActionItemsBar'),
'ActionItemsBar'
)
// Work around the issue that react router can not match nested splats when the URL contains spaces
// by expanding the repo matcher to an optional path of up to 10 segments.
//
@ -42,16 +35,6 @@ export const repoRevisionContainerRoutes: readonly RepoRevisionContainerRoute[]
render: props => (
<TraceSpanProvider name="RepositoryFileTreePage" attributes={{ objectType }}>
<RepositoryFileTreePage {...props} objectType={objectType} />
{window.context.enableLegacyExtensions && (
<ActionItemsBar
repo={props.repo}
useActionItemsBar={props.useActionItemsBar}
extensionsController={props.extensionsController}
platformContext={props.platformContext}
telemetryService={props.telemetryService}
source={objectType === 'blob' ? 'blob' : undefined}
/>
)}
</TraceSpanProvider>
),
})),

View File

@ -35,7 +35,6 @@ import { RepoBatchChangesButton } from '../../batches/RepoBatchChangesButton'
import { CodeIntelligenceProps } from '../../codeintel'
import { BreadcrumbSetters } from '../../components/Breadcrumbs'
import { PageTitle } from '../../components/PageTitle'
import { ActionItemsBarProps } from '../../extensions/components/ActionItemsBar'
import { RepositoryFields } from '../../graphql-operations'
import { basename } from '../../util/path'
import { FilePathBreadcrumbs } from '../FilePathBreadcrumbs'
@ -60,7 +59,6 @@ interface Props
commitID: string
revision: string
globbing: boolean
useActionItemsBar: ActionItemsBarProps['useActionItemsBar']
isSourcegraphDotCom: boolean
className?: string
}
@ -85,7 +83,6 @@ export const TreePage: FC<Props> = ({
useBreadcrumb,
codeIntelligenceEnabled,
batchChangesEnabled,
useActionItemsBar,
isSourcegraphDotCom,
className,
...props

View File

@ -43,9 +43,7 @@ interface SearchConsolePageProps
export const SearchConsolePage: React.FunctionComponent<React.PropsWithChildren<SearchConsolePageProps>> = props => {
const location = useLocation()
const navigate = useNavigate()
const { globbing, streamSearch, extensionsController, isSourcegraphDotCom } = props
const extensionHostAPI =
extensionsController !== null && window.context.enableLegacyExtensions ? extensionsController.extHostAPI : null
const { globbing, streamSearch, isSourcegraphDotCom } = props
const enableGoImportsSearchQueryTransform = useExperimentalFeatures(
features => features.enableGoImportsSearchQueryTransform
)
@ -72,11 +70,10 @@ export const SearchConsolePage: React.FunctionComponent<React.PropsWithChildren<
return transformSearchQuery({
query,
extensionHostAPIPromise: extensionHostAPI,
enableGoImportsSearchQueryTransform,
eventLogger,
})
}, [location.search, extensionHostAPI, enableGoImportsSearchQueryTransform])
}, [location.search, enableGoImportsSearchQueryTransform])
const autocompletion = useMemo(
() =>

View File

@ -5,7 +5,6 @@ import {
mockFetchSearchContexts,
mockGetUserSearchContextNamespaces,
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
import { extensionsController } from '@sourcegraph/shared/src/testing/searchTestHelpers'
import { WebStory } from '../../components/WebStory'
import { MockedFeatureFlagsProvider } from '../../featureFlags/MockedFeatureFlagsProvider'
@ -18,7 +17,6 @@ const defaultProps: SearchPageProps = {
final: null,
subjects: null,
},
extensionsController,
telemetryService: NOOP_TELEMETRY_SERVICE,
authenticatedUser: null,
globbing: false,

View File

@ -4,7 +4,6 @@ import { mdiArrowRight } from '@mdi/js'
import classNames from 'classnames'
import { QueryExamples } from '@sourcegraph/branded/src/search-ui/components/QueryExamples'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
import { QueryState, SearchContextInputProps } from '@sourcegraph/shared/src/search'
@ -30,7 +29,6 @@ import styles from './SearchPage.module.scss'
export interface SearchPageProps
extends SettingsCascadeProps<Settings>,
TelemetryProps,
ExtensionsControllerProps<'extHostAPI' | 'executeCommand'>,
PlatformContextProps<'settings' | 'sourcegraphURL' | 'updateSettings' | 'requestGraphQL'>,
SearchContextInputProps,
CodeInsightsProps {

View File

@ -1,13 +1,11 @@
import React, { createContext, Dispatch, SetStateAction, useContext, useEffect, useMemo, useState } from 'react'
import { Remote } from 'comlink'
import { isEqual } from 'lodash'
import { useNavigationType, useLocation } from 'react-router-dom'
import { merge, of } from 'rxjs'
import { last, share, throttleTime } from 'rxjs/operators'
import { transformSearchQuery } from '@sourcegraph/shared/src/api/client/search'
import { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
import { AggregateStreamingSearchResults, StreamSearchOptions } from '@sourcegraph/shared/src/search/stream'
import { TelemetryService } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useObservable } from '@sourcegraph/wildcard'
@ -39,7 +37,6 @@ export function useCachedSearchResults(
streamSearch: SearchStreamingProps['streamSearch'],
query: string,
options: StreamSearchOptions,
extensionHostAPI: Promise<Remote<FlatExtensionHostAPI>> | null,
telemetryService: TelemetryService
): AggregateStreamingSearchResults | undefined {
const [cachedResults, setCachedResults] = useContext(SearchResultsCacheContext)
@ -55,11 +52,10 @@ export function useCachedSearchResults(
() =>
transformSearchQuery({
query,
extensionHostAPIPromise: extensionHostAPI,
enableGoImportsSearchQueryTransform,
eventLogger,
}),
[query, extensionHostAPI, enableGoImportsSearchQueryTransform]
[query, enableGoImportsSearchQueryTransform]
)
const results = useObservable(

View File

@ -3,7 +3,6 @@ import { NEVER } from 'rxjs'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
import { extensionsController } from '@sourcegraph/shared/src/testing/searchTestHelpers'
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
import { SearchPatternType } from '../../graphql-operations'
@ -11,7 +10,6 @@ import { SearchPatternType } from '../../graphql-operations'
import { SearchResultsInfoBar, SearchResultsInfoBarProps } from './SearchResultsInfoBar'
const COMMON_PROPS: Omit<SearchResultsInfoBarProps, 'enableCodeMonitoring'> = {
extensionsController,
platformContext: { settings: NEVER, sourcegraphURL: 'https://sourcegraph.com' },
authenticatedUser: {
id: 'userID',
@ -40,12 +38,6 @@ const renderSearchResultsInfoBar = (
)
describe('SearchResultsInfoBar', () => {
beforeAll(() => {
window.context = {
enableLegacyExtensions: false,
} as any
})
test('code monitoring feature flag disabled', () => {
expect(
renderSearchResultsInfoBar({ query: 'foo type:diff', enableCodeMonitoring: false }).asFragment()

View File

@ -2,12 +2,7 @@ import React, { useMemo, useState } from 'react'
import { mdiChevronDoubleUp, mdiChevronDoubleDown } from '@mdi/js'
import classNames from 'classnames'
import { useLocation } from 'react-router-dom'
import { ContributableMenu } from '@sourcegraph/client-api'
import { ActionItem } from '@sourcegraph/shared/src/actions/ActionItem'
import { ActionsContainer } from '@sourcegraph/shared/src/actions/ActionsContainer'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { SearchPatternTypeProps, CaseSensitivityProps } from '@sourcegraph/shared/src/search'
import { FilterKind, findFilter } from '@sourcegraph/shared/src/search/query/query'
@ -29,8 +24,7 @@ import { SearchActionsMenu } from './SearchActionsMenu'
import styles from './SearchResultsInfoBar.module.scss'
export interface SearchResultsInfoBarProps
extends ExtensionsControllerProps<'executeCommand' | 'extHostAPI'>,
TelemetryProps,
extends TelemetryProps,
PlatformContextProps<'settings' | 'sourcegraphURL'>,
SearchPatternTypeProps,
Pick<CaseSensitivityProps, 'caseSensitive'> {
@ -74,7 +68,6 @@ export interface SearchResultsInfoBarProps
export const SearchResultsInfoBar: React.FunctionComponent<
React.PropsWithChildren<SearchResultsInfoBarProps>
> = props => {
const location = useLocation()
const globalTypeFilter = useMemo(
() => (props.query ? findFilter(props.query, 'type', FilterKind.Global)?.value?.value : undefined),
[props.query]
@ -106,7 +99,7 @@ export const SearchResultsInfoBar: React.FunctionComponent<
)
),
getSearchContextCreateAction(props.query, props.authenticatedUser),
getInsightsCreateAction(props.query, props.patternType, window.context.codeInsightsEnabled),
getInsightsCreateAction(props.query, props.patternType, window.context?.codeInsightsEnabled),
].filter((button): button is CreateAction => button !== null),
[
props.authenticatedUser,
@ -125,15 +118,6 @@ export const SearchResultsInfoBar: React.FunctionComponent<
[props.enableCodeMonitoring, props.patternType, props.query]
)
const extraContext = useMemo(
() => ({
searchQuery: props.query || null,
patternType: props.patternType,
caseSensitive: props.caseSensitive,
}),
[props.query, props.patternType, props.caseSensitive]
)
// Show/hide mobile filters menu
const [showMobileFilters, setShowMobileFilters] = useState(false)
const onShowMobileFiltersClicked = (): void => {
@ -142,8 +126,6 @@ export const SearchResultsInfoBar: React.FunctionComponent<
props.onShowMobileFiltersChanged?.(newShowFilters)
}
const { extensionsController } = props
return (
<aside
role="region"
@ -157,39 +139,6 @@ export const SearchResultsInfoBar: React.FunctionComponent<
<div className={styles.expander} />
<ul className="nav align-items-center">
{extensionsController !== null && window.context.enableLegacyExtensions ? (
<ActionsContainer
{...props}
location={location}
extensionsController={extensionsController}
extraContext={extraContext}
menu={ContributableMenu.SearchResultsToolbar}
>
{actionItems => (
<>
{actionItems.map(actionItem => (
<ActionItem
{...props}
{...actionItem}
location={location}
extensionsController={extensionsController}
key={actionItem.action.id}
showLoadingSpinnerDuringExecution={false}
className="mr-2 text-decoration-none"
actionItemStyleProps={{
actionItemVariant: 'secondary',
actionItemSize: 'sm',
actionItemOutline: true,
}}
/>
))}
</>
)}
</ActionsContainer>
) : null}
<li className={styles.divider} aria-hidden="true" />
<SearchActionsMenu
query={props.query}
patternType={props.patternType}

View File

@ -6,7 +6,6 @@ import { SearchQueryStateStoreProvider } from '@sourcegraph/shared/src/search'
import { AggregateStreamingSearchResults } from '@sourcegraph/shared/src/search/stream'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import {
extensionsController,
HIGHLIGHTED_FILE_LINES_LONG_REQUEST,
MULTIPLE_SEARCH_RESULT,
REPO_MATCH_RESULTS_WITH_METADATA,
@ -33,7 +32,6 @@ const streamingSearchResult: AggregateStreamingSearchResults = {
}
const defaultProps: StreamingSearchResultsProps = {
extensionsController,
telemetryService: NOOP_TELEMETRY_SERVICE,
authenticatedUser: {

View File

@ -13,7 +13,6 @@ import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/teleme
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
import {
COLLAPSABLE_SEARCH_RESULT,
extensionsController,
HIGHLIGHTED_FILE_LINES_REQUEST,
MULTIPLE_SEARCH_RESULT,
REPO_MATCH_RESULT,
@ -33,7 +32,6 @@ describe('StreamingSearchResults', () => {
const streamingSearchResult = MULTIPLE_SEARCH_RESULT
const defaultProps: StreamingSearchResultsProps = {
extensionsController,
telemetryService: NOOP_TELEMETRY_SERVICE,
authenticatedUser: null,
@ -80,9 +78,6 @@ describe('StreamingSearchResults', () => {
searchCaseSensitivity: false,
searchQueryFromURL: 'r:golang/oauth2 test f:travis',
})
window.context = {
enableLegacyExtensions: false,
} as any
})
it('should call streaming search API with the right parameters from URL', async () => {

View File

@ -8,7 +8,6 @@ import { limitHit, StreamingProgress, StreamingSearchResultsList } from '@source
import { asError } from '@sourcegraph/common'
import { FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
import { FilePrefetcher } from '@sourcegraph/shared/src/components/PrefetchableFile'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { QueryUpdate, SearchContextProps } from '@sourcegraph/shared/src/search'
@ -47,7 +46,6 @@ export interface StreamingSearchResultsProps
extends SearchStreamingProps,
Pick<SearchContextProps, 'selectedSearchContextSpec' | 'searchContextsEnabled'>,
SettingsCascadeProps,
ExtensionsControllerProps<'executeCommand' | 'extHostAPI'>,
PlatformContextProps<'settings' | 'requestGraphQL' | 'sourcegraphURL'>,
TelemetryProps,
CodeInsightsProps,
@ -64,7 +62,6 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
authenticatedUser,
telemetryService,
isSourcegraphDotCom,
extensionsController,
searchAggregationEnabled,
codeMonitoringEnabled,
} = props
@ -95,8 +92,6 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
const [showMobileSidebar, setShowMobileSidebar] = useState(false)
// Derived state
const extensionHostAPI =
extensionsController !== null && window.context.enableLegacyExtensions ? extensionsController.extHostAPI : null
const trace = useMemo(() => new URLSearchParams(location.search).get('trace') ?? undefined, [location.search])
const featureOverrides = useDeepMemo(
// Nested use memo here is used for avoiding extra object calculation step on each render
@ -117,7 +112,7 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
[caseSensitive, patternType, searchMode, trace, featureOverrides]
)
const results = useCachedSearchResults(streamSearch, submittedURLQuery, options, extensionHostAPI, telemetryService)
const results = useCachedSearchResults(streamSearch, submittedURLQuery, options, telemetryService)
// Log view event on first load
useEffect(

View File

@ -18,10 +18,6 @@ exports[`SearchResultsInfoBar code monitoring feature flag disabled 1`] = `
<ul
class="nav align-items-center"
>
<li
aria-hidden="true"
class="divider"
/>
<li
class="mr-2 navItem"
>
@ -96,10 +92,6 @@ exports[`SearchResultsInfoBar code monitoring feature flag enabled, can create m
<ul
class="nav align-items-center"
>
<li
aria-hidden="true"
class="divider"
/>
<li
class="mr-2 navItem"
>
@ -174,10 +166,6 @@ exports[`SearchResultsInfoBar code monitoring feature flag enabled, can create m
<ul
class="nav align-items-center"
>
<li
aria-hidden="true"
class="divider"
/>
<li
class="mr-2 navItem"
>
@ -252,10 +240,6 @@ exports[`SearchResultsInfoBar code monitoring feature flag enabled, cannot creat
<ul
class="nav align-items-center"
>
<li
aria-hidden="true"
class="divider"
/>
<li
class="mr-2 navItem"
>
@ -330,10 +314,6 @@ exports[`SearchResultsInfoBar unauthenticated user 1`] = `
<ul
class="nav align-items-center"
>
<li
aria-hidden="true"
class="divider"
/>
<li
class="mr-2 navItem"
>

View File

@ -265,14 +265,6 @@ export class MonacoSettingsEditor extends React.PureComponent<Props, State> {
function setDiagnosticsOptions(editor: typeof monaco, jsonSchema: JSONSchema | undefined): void {
const schema = { ...settingsSchema, properties: { ...settingsSchema.properties } }
if (!window.context.enableLegacyExtensions) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- we need to remove this key conditionally, but not from the schema
// @ts-ignore
delete schema.properties.extensions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- we need to remove this key conditionally, but not from the schema
// @ts-ignore
delete schema.properties['extensions.activeLoggers']
}
editor.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
allowComments: true,

View File

@ -2,26 +2,24 @@ import * as React from 'react'
import classNames from 'classnames'
import AlertCircleIcon from 'mdi-react/AlertCircleIcon'
import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs'
import { combineLatest, Observable, Subject, Subscription } from 'rxjs'
import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'
import { asError, createAggregateError, ErrorLike, isErrorLike, logger } from '@sourcegraph/common'
import { gql } from '@sourcegraph/http-client'
import { extensionIDsFromSettings } from '@sourcegraph/shared/src/extensions/extension'
import { queryConfiguredRegistryExtensions } from '@sourcegraph/shared/src/extensions/helpers'
import { Scalars } from '@sourcegraph/shared/src/graphql-operations'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { gqlToCascade, SettingsCascadeProps, SettingsSubject } from '@sourcegraph/shared/src/settings/settings'
import { SettingsCascadeProps, SettingsSubject } from '@sourcegraph/shared/src/settings/settings'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { LoadingSpinner, PageHeader, ErrorMessage } from '@sourcegraph/wildcard'
import settingsSchemaJSON from '../../../../schema/settings.schema.json'
import { AuthenticatedUser } from '../auth'
import { queryGraphQL } from '../backend/graphql'
import { HeroPage } from '../components/HeroPage'
import { SettingsCascadeResult } from '../graphql-operations'
import { eventLogger } from '../tracking/eventLogger'
import { mergeSettingsSchemas } from './configuration'
import { SettingsPage } from './SettingsPage'
/** Props shared by SettingsArea and its sub-pages. */
@ -92,11 +90,7 @@ export class SettingsArea extends React.Component<Props, State> {
.pipe(
switchMap(([{ id }]) =>
fetchSettingsCascade(id).pipe(
switchMap(cascade =>
this.getMergedSettingsJSONSchema(cascade).pipe(
map(settingsJSONSchema => ({ subjects: cascade.subjects, settingsJSONSchema }))
)
),
map(cascade => ({ subjects: cascade.subjects, settingsJSONSchema: settingsSchemaJSON })),
catchError(error => [asError(error)]),
map(dataOrError => ({ dataOrError }))
)
@ -177,25 +171,6 @@ export class SettingsArea extends React.Component<Props, State> {
}
private onUpdate = (): void => this.refreshRequests.next()
private getMergedSettingsJSONSchema(cascade: SettingsSubjects): Observable<{ $id: string }> {
return queryConfiguredRegistryExtensions(
this.props.platformContext,
extensionIDsFromSettings(gqlToCascade(cascade))
)
.pipe(
catchError(error => {
logger.warn('Unable to get extension settings JSON Schemas for settings editor.', { error })
return of([])
})
)
.pipe(
map(extensions => ({
$id: 'mergedSettings.schema.json#',
...mergeSettingsSchemas(extensions),
}))
)
}
}
function fetchSettingsCascade(subject: Scalars['ID']): Observable<SettingsSubjects> {

View File

@ -1,66 +0,0 @@
import settingsSchemaJSON from '../../../../schema/settings.schema.json'
import { mergeSettingsSchemas } from './configuration'
describe('mergeSettingsSchemas', () => {
test('handles empty', () =>
expect(mergeSettingsSchemas([])).toEqual({
allOf: [{ $ref: settingsSchemaJSON.$id }],
}))
test('overwrites additionalProperties and required', () =>
expect(
mergeSettingsSchemas([
{
manifest: {
contributes: {
configuration: { additionalProperties: false, properties: { a: { type: 'string' } } },
},
},
},
{
manifest: {
contributes: {
configuration: { required: ['b'], properties: { b: { type: 'string' } } },
},
},
},
])
).toEqual({
allOf: [
{ $ref: settingsSchemaJSON.$id },
{ additionalProperties: true, required: [], properties: { a: { type: 'string' } } },
{ additionalProperties: true, required: [], properties: { b: { type: 'string' } } },
],
}))
test('handles error and null configuration', () =>
expect(
mergeSettingsSchemas([
{
manifest: {
contributes: {
configuration: { additionalProperties: false, properties: { a: { type: 'string' } } },
},
},
},
{
manifest: new Error('x'),
},
{
manifest: null,
},
{
manifest: {},
},
{
manifest: { contributes: {} },
},
])
).toEqual({
allOf: [
{ $ref: settingsSchemaJSON.$id },
{ additionalProperties: true, required: [], properties: { a: { type: 'string' } } },
],
}))
})

View File

@ -1,54 +0,0 @@
import { isErrorLike } from '@sourcegraph/common'
import { ConfiguredExtension } from '@sourcegraph/shared/src/extensions/extension'
import settingsSchemaJSON from '../../../../schema/settings.schema.json'
/**
* Merges settings schemas from base settings and all configured extensions.
*
* @param configuredExtensions
* @returns A JSON Schema that describes an instance of settings for a particular subject.
*/
export function mergeSettingsSchemas(configuredExtensions: Pick<ConfiguredExtension<'contributes'>, 'manifest'>[]): {
allOf: object
} {
return {
allOf: [
{ $ref: settingsSchemaJSON.$id },
...(configuredExtensions || [])
.map(configuredExtension => {
if (
configuredExtension.manifest &&
!isErrorLike(configuredExtension.manifest) &&
configuredExtension.manifest.contributes?.configuration
) {
// Adjust the schema to describe a valid instance of settings for a subject (instead of the
// final, merged settings).
//
// This is necessary to avoid erroneous validation errors. For example, suppose an extension's
// configuration schema declares that the property "x" is required. For the configuration to be
// valid, "x" may be set in global, organization, or user settings. It is valid for user
// settings to NOT contain "x" (if global or organization settings contains "x").
//
// The JSON Schema returned by mergeSettingsSchema is used for a single subject's settings
// (e.g., for user settings in the above example). Therefore, we must allow additionalProperties
// and set required to [] to avoid erroneous validation errors.
return {
...configuredExtension.manifest.contributes.configuration,
// Force allow additionalProperties to prevent any single extension's configuration schema
// from invalidating all other extensions' configuration properties.
additionalProperties: true,
// Force no required properties because this instance is only the settings for a single
// subject. It is possible that a required property is specified at a different subject in
// the cascade, in which case we don't want to report this instance as invalid.
required: [],
}
}
return true // JSON Schema that matches everything
})
.filter(schema => schema !== true), // omit trivial JSON Schemas
],
}
}

View File

@ -2,7 +2,6 @@ package graphqlbackend
import (
"context"
"encoding/json"
"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/relay"
@ -11,63 +10,6 @@ import (
"github.com/sourcegraph/sourcegraph/internal/database"
)
var builtinExtensions = map[string]bool{
"sourcegraph/apex": true,
"sourcegraph/clojure": true,
"sourcegraph/cobol": true,
"sourcegraph/cpp": true,
"sourcegraph/csharp": true,
"sourcegraph/cuda": true,
"sourcegraph/dart": true,
"sourcegraph/elixir": true,
"sourcegraph/erlang": true,
"sourcegraph/git-extras": true,
"sourcegraph/go": true,
"sourcegraph/graphql": true,
"sourcegraph/groovy": true,
"sourcegraph/haskell": true,
"sourcegraph/java": true,
"sourcegraph/jsonnet": true,
"sourcegraph/kotlin": true,
"sourcegraph/lisp": true,
"sourcegraph/lua": true,
"sourcegraph/ocaml": true,
"sourcegraph/pascal": true,
"sourcegraph/perl": true,
"sourcegraph/php": true,
"sourcegraph/powershell": true,
"sourcegraph/protobuf": true,
"sourcegraph/python": true,
"sourcegraph/r": true,
"sourcegraph/ruby": true,
"sourcegraph/rust": true,
"sourcegraph/scala": true,
"sourcegraph/shell": true,
"sourcegraph/starlark": true,
"sourcegraph/swift": true,
"sourcegraph/tcl": true,
"sourcegraph/thrift": true,
"sourcegraph/typescript": true,
"sourcegraph/verilog": true,
"sourcegraph/vhdl": true,
}
func defaultSettings() map[string]any {
extensionIDs := []string{}
for id := range builtinExtensions {
extensionIDs = append(extensionIDs, id)
}
extensions := map[string]bool{}
for _, id := range extensionIDs {
extensions[id] = true
}
return map[string]any{
"experimentalFeatures": map[string]any{},
"extensions": extensions,
}
}
const singletonDefaultSettingsGQLID = "DefaultSettings"
type defaultSettingsResolver struct {
@ -82,11 +24,7 @@ func marshalDefaultSettingsGQLID(defaultSettingsID string) graphql.ID {
func (r *defaultSettingsResolver) ID() graphql.ID { return marshalDefaultSettingsGQLID(r.gqlID) }
func (r *defaultSettingsResolver) LatestSettings(ctx context.Context) (*settingsResolver, error) {
contents, err := json.Marshal(defaultSettings())
if err != nil {
return nil, err
}
settings := &api.Settings{Subject: api.SettingsSubject{Default: true}, Contents: string(contents)}
settings := &api.Settings{Subject: api.SettingsSubject{Default: true}, Contents: "{}"}
return &settingsResolver{r.db, &settingsSubject{defaultSettings: r}, settings, nil}, nil
}

View File

@ -1,61 +0,0 @@
package graphqlbackend
import (
"context"
"github.com/graph-gophers/graphql-go"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
var ErrExtensionsDisabled = errors.New("extensions are disabled in site configuration (contact the site admin to enable extensions)")
func (r *schemaResolver) ExtensionRegistry(ctx context.Context) (ExtensionRegistryResolver, error) {
reg := ExtensionRegistry(r.db)
if conf.Extensions() == nil {
return nil, ErrExtensionsDisabled
}
return reg, nil
}
// ExtensionRegistry is the implementation of the GraphQL type ExtensionRegistry.
var ExtensionRegistry func(db database.DB) ExtensionRegistryResolver
// ExtensionRegistryResolver is the interface for the GraphQL type ExtensionRegistry.
type ExtensionRegistryResolver interface {
Extensions(context.Context, *RegistryExtensionConnectionArgs) (RegistryExtensionConnection, error)
}
type RegistryExtensionConnectionArgs struct {
graphqlutil.ConnectionArgs
ExtensionIDs *[]string
}
// NodeToRegistryExtension is called to convert GraphQL node values to values of type
// RegistryExtension. It is assigned at init time.
var NodeToRegistryExtension func(any) (RegistryExtension, bool)
// RegistryExtensionByID is called to look up values of GraphQL type RegistryExtension. It is
// assigned at init time.
var RegistryExtensionByID func(context.Context, database.DB, graphql.ID) (RegistryExtension, error)
// RegistryExtension is the interface for the GraphQL type RegistryExtension.
type RegistryExtension interface {
ID() graphql.ID
ExtensionID() string
Manifest(ctx context.Context) (ExtensionManifest, error)
}
// ExtensionManifest is the interface for the GraphQL type ExtensionManifest.
type ExtensionManifest interface {
Raw() string
JSONFields(*struct{ Fields []string }) JSONValue
}
// RegistryExtensionConnection is the interface for the GraphQL type RegistryExtensionConnection.
type RegistryExtensionConnection interface {
Nodes(context.Context) ([]RegistryExtension, error)
}

View File

@ -636,9 +636,6 @@ func newSchemaResolver(db database.DB, gitserverClient gitserver.Client) *schema
"GitCommit": func(ctx context.Context, id graphql.ID) (Node, error) {
return r.gitCommitByID(ctx, id)
},
"RegistryExtension": func(ctx context.Context, id graphql.ID) (Node, error) {
return RegistryExtensionByID(ctx, db, id)
},
"SavedSearch": func(ctx context.Context, id graphql.ID) (Node, error) {
return r.savedSearchByID(ctx, id)
},

View File

@ -196,13 +196,6 @@ func (r *NodeResolver) ToGitCommit() (*GitCommitResolver, bool) {
return n, ok
}
func (r *NodeResolver) ToRegistryExtension() (RegistryExtension, bool) {
if NodeToRegistryExtension == nil {
return nil, false
}
return NodeToRegistryExtension(r.Node)
}
func (r *NodeResolver) ToSavedSearch() (*savedSearchResolver, bool) {
n, ok := r.Node.(*savedSearchResolver)
return n, ok

View File

@ -245,7 +245,6 @@ type Mutation {
- All user data (access tokens, email addresses, external account info, survey responses, etc)
- Organization membership information (which organizations the user is a part of, any invitations created by or targeting the user).
- Sourcegraph extensions published by the user.
- User, Organization, or Global settings authored by the user.
"""
deleteUser(user: ID!, hard: Boolean): EmptyResponse
@ -1599,10 +1598,6 @@ type Query {
first: Int
): SurveyResponseConnection!
"""
The extension registry.
"""
extensionRegistry: ExtensionRegistry!
"""
FOR INTERNAL USE ONLY: Lists all status messages
"""
statusMessages: [StatusMessage!]!
@ -6910,11 +6905,6 @@ type Site implements SettingsSubject {
the GLOBAL_SETTINGS_FILE environment variable, site settings edits cannot be made through the API.
"""
allowSiteSettingsEdits: Boolean!
"""
Whether to enable the extension registry and the use of extensions.
Reflects the site configuration `enableLegacyExtensions` experimental feature value.
"""
enableLegacyExtensions: Boolean!
"""
FOR INTERNAL USE ONLY: Returns information about instance upgrade readiness.
@ -8022,75 +8012,6 @@ type ProductLicenseInfo {
expiresAt: DateTime!
}
"""
An extension registry.
"""
type ExtensionRegistry {
"""
A list of extensions published in the extension registry.
"""
extensions(
"""
Returns the first n extensions from the list.
"""
first: Int
"""
Returns only extensions with the given IDs.
"""
extensionIDs: [String!]
): RegistryExtensionConnection!
}
"""
An extension's listing in the extension registry.
"""
type RegistryExtension implements Node {
"""
The unique, opaque, permanent ID of the extension. Do not display this ID to the user; display
RegistryExtension.extensionID instead (it is friendlier and still unique, but it can be renamed).
"""
id: ID!
"""
The qualified, unique name that refers to this extension, consisting of the registry name (if non-default),
publisher's name, and the extension's name, all joined by "/" (for example, "acme-corp/my-extension-name").
"""
extensionID: String!
"""
The extension manifest, or null if none is set.
"""
manifest: ExtensionManifest
}
"""
A description of the extension, how to run or access it, and when to activate it.
"""
type ExtensionManifest {
"""
The raw JSON (or JSONC) contents of the manifest. This value may be large (because many
manifests contain README and icon data), and it is JSONC (not strict JSON), which means
it must be parsed with a JSON parser that supports trailing commas and comments. Consider
using jsonFields instead.
"""
raw: String!
"""
The manifest as JSON (not JSONC, even if the raw manifest is JSONC) with only the
specified fields. This is useful for callers that only need certain fields and want
to avoid fetching a large amount of data (because many manifests contain README
and icon data).
"""
jsonFields(fields: [String!]!): JSONValue!
}
"""
A list of registry extensions.
"""
type RegistryExtensionConnection {
"""
A list of registry extensions.
"""
nodes: [RegistryExtension!]!
}
"""
Aggregate local code intelligence for all ranges that fall between a window of lines in a document.
"""

View File

@ -230,10 +230,6 @@ func canUpdateSiteConfiguration() bool {
return os.Getenv("SITE_CONFIG_FILE") == "" || siteConfigAllowEdits
}
func (r *siteResolver) EnableLegacyExtensions() bool {
return conf.ExperimentalFeatures().EnableLegacyExtensions
}
func (r *siteResolver) UpgradeReadiness(ctx context.Context) (*upgradeReadinessResolver, error) {
// 🚨 SECURITY: Only site admins may view upgrade readiness information.
if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil {

View File

@ -164,8 +164,6 @@ type JSContext struct {
ExperimentalFeatures schema.ExperimentalFeatures `json:"experimentalFeatures"`
EnableLegacyExtensions bool `json:"enableLegacyExtensions"`
LicenseInfo *hooks.LicenseInfo `json:"licenseInfo"`
OutboundRequestLogLimit int `json:"outboundRequestLogLimit"`
@ -309,8 +307,6 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext {
ExperimentalFeatures: conf.ExperimentalFeatures(),
EnableLegacyExtensions: conf.ExperimentalFeatures().EnableLegacyExtensions,
LicenseInfo: licenseInfo,
OutboundRequestLogLimit: conf.Get().OutboundRequestLogLimit,

View File

@ -1,12 +0,0 @@
package ui
import (
"github.com/sourcegraph/sourcegraph/internal/conf"
)
func ShouldRedirectLegacyExtensionEndpoints() bool {
if conf.ExperimentalFeatures().EnableLegacyExtensions {
return false
}
return true
}

View File

@ -5,9 +5,7 @@ import (
"net/http/httptest"
"testing"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/schema"
)
func TestLegacyExtensionsRedirects(t *testing.T) {
@ -38,39 +36,3 @@ func TestLegacyExtensionsRedirects(t *testing.T) {
}
}
}
func TestLegacyExtensionsRedirectsWithExtensionsEnabled(t *testing.T) {
enableLegacyExtensions()
defer conf.Mock(nil)
InitRouter(database.NewMockDB())
router := Router()
tests := []string{
"/extensions",
"/extensions/sourcegraph/codecov",
"/extensions/sourcegraph/codecov/-/manifest",
"/-/static/extension/13594-sourcegraph-codecov.js",
}
for i := range tests {
rw := httptest.NewRecorder()
req, err := http.NewRequest("GET", tests[i], nil)
if err != nil {
t.Error(err)
continue
}
router.ServeHTTP(rw, req)
if got := rw.Header().Get("location"); got != "" {
t.Errorf("%s: expected router to not redirect to root page but got %s", tests[i], got)
}
}
}
func enableLegacyExtensions() {
conf.Mock(&conf.Unified{SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
EnableLegacyExtensions: true,
},
}})
}

View File

@ -270,12 +270,10 @@ func initRouter(db database.DB, router *mux.Router) {
router.Get(routeSurvey).Handler(brandedNoIndex("Survey"))
router.Get(routeSurveyScore).Handler(brandedNoIndex("Survey"))
router.Get(routeRegistry).Handler(brandedNoIndex("Registry"))
if ShouldRedirectLegacyExtensionEndpoints() {
if envvar.SourcegraphDotComMode() {
router.Get(routeExtensions).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusMovedPermanently)
})
} else {
router.Get(routeExtensions).Handler(brandedIndex("Extensions"))
}
router.Get(routeHelp).HandlerFunc(serveHelp)
router.Get(routeSnippets).Handler(brandedNoIndex("Snippets"))

View File

@ -1,98 +0,0 @@
package api
import (
"context"
"sort"
"sync"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/client"
"github.com/sourcegraph/sourcegraph/internal/database"
)
func (r *extensionRegistryResolver) Extensions(ctx context.Context, args *graphqlbackend.RegistryExtensionConnectionArgs) (graphqlbackend.RegistryExtensionConnection, error) {
return &registryExtensionConnectionResolver{
args: *args,
db: r.db,
listRemoteRegistryExtensions: listRemoteRegistryExtensions,
}, nil
}
// registryExtensionConnectionResolver resolves a list of registry extensions.
type registryExtensionConnectionResolver struct {
args graphqlbackend.RegistryExtensionConnectionArgs
db database.DB
listRemoteRegistryExtensions func(_ context.Context, query string) ([]*registry.Extension, error)
// cache results because they are used by multiple fields
once sync.Once
registryExtensions []graphqlbackend.RegistryExtension
err error
}
func (r *registryExtensionConnectionResolver) compute(ctx context.Context) ([]graphqlbackend.RegistryExtension, error) {
r.once.Do(func() {
args2 := r.args
if args2.First != nil {
tmp := *args2.First
tmp++ // so we can detect if there is a next page
args2.First = &tmp
}
// Query remote registry extensions, if filters would match any.
var remote []*registry.Extension
xs, err := r.listRemoteRegistryExtensions(ctx, "")
if err != nil {
// Continue execution even if r.err != nil so that partial (local) results are returned
// even when the remote registry is inaccessible.
r.err = err
}
if r.args.ExtensionIDs == nil {
remote = append(remote, xs...)
} else {
// The ExtensionIDs arg ("only include extensions specified by these IDs") is not
// applied at query time for remote extensions, so apply it here.
include := map[string]struct{}{}
for _, id := range *r.args.ExtensionIDs {
include[id] = struct{}{}
}
for _, x := range xs {
if _, ok := include[x.ExtensionID]; ok {
remote = append(remote, x)
}
}
}
r.registryExtensions = make([]graphqlbackend.RegistryExtension, len(remote))
for i, x := range remote {
r.registryExtensions[i] = &registryExtensionRemoteResolver{v: x}
}
sort.SliceStable(r.registryExtensions, func(i, j int) bool {
return r.registryExtensions[i].ExtensionID() < r.registryExtensions[j].ExtensionID()
})
allowedExtensions := ExtensionRegistryListAllowedExtensions()
if allowedExtensions != nil {
filteredExtensions := []graphqlbackend.RegistryExtension{}
for i := range r.registryExtensions {
ext := r.registryExtensions[i]
if _, ok := allowedExtensions[ext.ExtensionID()]; ok {
filteredExtensions = append(filteredExtensions, ext)
}
}
r.registryExtensions = filteredExtensions
}
})
return r.registryExtensions, r.err
}
func (r *registryExtensionConnectionResolver) Nodes(ctx context.Context) ([]graphqlbackend.RegistryExtension, error) {
xs, _ := r.compute(ctx)
if r.args.First != nil && len(xs) > int(*r.args.First) {
xs = xs[:int(*r.args.First)]
}
return xs, nil
}

View File

@ -1,53 +0,0 @@
package api
import (
"context"
"reflect"
"testing"
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/client"
"github.com/sourcegraph/sourcegraph/internal/conf"
)
func TestRegistryExtensionConnectionResolver(t *testing.T) {
enableLegacyExtensions()
defer conf.Mock(nil)
envvar.MockSourcegraphDotComMode(true)
defer envvar.MockSourcegraphDotComMode(false)
stringSlicePtr := func(s []string) *[]string { return &s }
extensionIDs := func(xs []graphqlbackend.RegistryExtension) (ids []string) {
for _, x := range xs {
ids = append(ids, x.ExtensionID())
}
return ids
}
ctx := context.Background()
t.Run("extensionIDs", func(t *testing.T) {
r := registryExtensionConnectionResolver{
args: graphqlbackend.RegistryExtensionConnectionArgs{
ExtensionIDs: stringSlicePtr([]string{"a/a"}),
},
listRemoteRegistryExtensions: func(_ context.Context, query string) ([]*registry.Extension, error) {
return []*registry.Extension{
{ExtensionID: "a/b", Manifest: strptr(`{}`)},
{ExtensionID: "a/a", Manifest: strptr(`{}`)},
}, nil
},
}
nodes, err := r.Nodes(ctx)
if err != nil {
t.Fatal(err)
}
if ids, want := extensionIDs(nodes), []string{"a/a"}; !reflect.DeepEqual(ids, want) {
t.Errorf("got ids %v, want %v", ids, want)
}
})
}
func strptr(s string) *string { return &s }

View File

@ -1,52 +0,0 @@
package api
import (
"context"
"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/relay"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
func init() {
graphqlbackend.NodeToRegistryExtension = func(node any) (graphqlbackend.RegistryExtension, bool) {
n, ok := node.(*registryExtensionRemoteResolver)
return n, ok
}
graphqlbackend.RegistryExtensionByID = registryExtensionByID
}
// RegistryExtensionID identifies a registry extension.
type RegistryExtensionID struct {
RemoteID *registryExtensionRemoteID `json:"r,omitempty"`
}
func MarshalRegistryExtensionID(id RegistryExtensionID) graphql.ID {
return relay.MarshalID("RegistryExtension", id)
}
func UnmarshalRegistryExtensionID(id graphql.ID) (registryExtensionID RegistryExtensionID, err error) {
err = relay.UnmarshalSpec(id, &registryExtensionID)
return
}
func registryExtensionByID(ctx context.Context, db database.DB, id graphql.ID) (graphqlbackend.RegistryExtension, error) {
registryExtensionID, err := UnmarshalRegistryExtensionID(id)
if err != nil {
return nil, err
}
switch {
case registryExtensionID.RemoteID != nil:
x, err := getRemoteRegistryExtension(ctx, "uuid", registryExtensionID.RemoteID.UUID)
if err != nil {
return nil, err
}
return &registryExtensionRemoteResolver{v: x}, nil
default:
return nil, errors.New("invalid registry extension ID")
}
}

View File

@ -1,48 +0,0 @@
package api
import (
"encoding/json"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
"github.com/sourcegraph/sourcegraph/internal/jsonc"
)
// extensionManifest implements the GraphQL type ExtensionManifest.
type extensionManifest struct {
raw string
}
// NewExtensionManifest creates a new resolver for the GraphQL type ExtensionManifest with the given
// raw contents of an extension manifest.
func NewExtensionManifest(raw *string) graphqlbackend.ExtensionManifest {
if raw == nil {
return nil
}
return &extensionManifest{raw: *raw}
}
func (r *extensionManifest) Raw() string { return r.raw }
func (r *extensionManifest) JSONFields(args *struct{ Fields []string }) graphqlbackend.JSONValue {
return jsonValueWithFields(r.raw, args.Fields)
}
func jsonValueWithFields(jsoncStr string, fields []string) graphqlbackend.JSONValue {
o := map[string]json.RawMessage{}
jsonc.Unmarshal(jsoncStr, &o) // ignore error and treat as empty object
keepField := func(field string) bool {
for _, f := range fields {
if f == field {
return true
}
}
return false
}
for field := range o {
if !keepField(field) {
delete(o, field)
}
}
return graphqlbackend.JSONValue{Value: o}
}

View File

@ -1,39 +0,0 @@
package api
import "testing"
func TestJSONValueWithFields(t *testing.T) {
tests := map[string]struct {
jsoncStr string
fields []string
want string
}{
"invalid json top-level": {
jsoncStr: `{`,
fields: []string{"x"},
want: `{}`,
},
"invalid json in field": {
jsoncStr: `{"a": {"b": }, "c": 3}`,
fields: []string{"a", "c"},
want: `{}`,
},
"subset of fields": {
jsoncStr: `{"a": 1, "b": {"c": 3,}, "d": true,}`,
fields: []string{"d", "b", "x"},
want: `{"b":{"c":3},"d":true}`,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
v := jsonValueWithFields(test.jsoncStr, test.fields)
got, err := v.MarshalJSON()
if err != nil {
t.Fatal(err)
}
if string(got) != test.want {
t.Errorf("got %s, want %s", got, test.want)
}
})
}
}

View File

@ -1,37 +0,0 @@
package api
import (
"context"
"github.com/graph-gophers/graphql-go"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/client"
)
// registryExtensionRemoteResolver implements the GraphQL type RegistryExtension with data from a
// remote registry.
type registryExtensionRemoteResolver struct {
v *registry.Extension
}
var _ graphqlbackend.RegistryExtension = &registryExtensionRemoteResolver{}
func (r *registryExtensionRemoteResolver) ID() graphql.ID {
return MarshalRegistryExtensionID(RegistryExtensionID{
RemoteID: &registryExtensionRemoteID{Registry: r.v.RegistryURL, UUID: r.v.UUID},
})
}
// registryExtensionRemoteID identifies a registry extension on a remote registry. It is encoded in
// RegistryExtensionID.
type registryExtensionRemoteID struct {
Registry string `json:"r"`
UUID string `json:"u"`
}
func (r *registryExtensionRemoteResolver) ExtensionID() string { return r.v.ExtensionID }
func (r *registryExtensionRemoteResolver) Manifest(context.Context) (graphqlbackend.ExtensionManifest, error) {
return NewExtensionManifest(r.v.Manifest), nil
}

View File

@ -1,89 +0,0 @@
package api
import (
"context"
"net/url"
"github.com/sourcegraph/sourcegraph/lib/errors"
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/client"
"github.com/sourcegraph/sourcegraph/internal/conf"
)
// getRemoteRegistryURL returns the remote registry URL from site configuration, or nil if there is
// none. If an error exists while parsing the value in site configuration, the error is returned.
func getRemoteRegistryURL() (*url.URL, error) {
pc := conf.Extensions()
if pc == nil || pc.RemoteRegistryURL == "" {
return nil, nil
}
return url.Parse(pc.RemoteRegistryURL)
}
// IsRemoteExtensionAllowed reports whether to allow usage of the remote extension with the given
// extension ID.
//
// It can be overridden to use custom logic.
var IsRemoteExtensionAllowed = func(extensionID string) bool {
// By default, all remote extensions are allowed.
return true
}
// IsRemoteExtensionPublisherAllowed reports whether to allow usage of the remote extension created by
// certain publisher by extension ID.
//
// It can be overridden to use custom logic.
var IsRemoteExtensionPublisherAllowed = func(p registry.Publisher) bool {
// By default, all remote extensions are allowed.
return true
}
// getRemoteRegistryExtension gets the remote registry extension and rewrites its fields to be from
// the frame-of-reference of this site. The field is either "uuid" or "extensionID".
func getRemoteRegistryExtension(ctx context.Context, field, value string) (*registry.Extension, error) {
registryURL, err := getRemoteRegistryURL()
if registryURL == nil || err != nil {
return nil, err
}
var x *registry.Extension
switch field {
case "uuid":
x, err = registry.GetByUUID(ctx, registryURL, value)
case "extensionID":
x, err = registry.GetByExtensionID(ctx, registryURL, value)
default:
panic("unexpected field: " + field)
}
if x != nil {
x.RegistryURL = registryURL.String()
}
if x != nil && !IsRemoteExtensionAllowed(x.ExtensionID) {
return nil, errors.Errorf("extension is not allowed in site configuration: %q", x.ExtensionID)
}
if x != nil && !IsRemoteExtensionPublisherAllowed(x.Publisher) {
return nil, errors.Errorf("Only extensions authored by Sourcegraph are allowed in this site configuration")
}
return x, err
}
// listRemoteRegistryExtensions lists the remote registry extensions and rewrites their fields to be
// from the frame-of-reference of this site.
func listRemoteRegistryExtensions(ctx context.Context, query string) ([]*registry.Extension, error) {
registryURL, err := getRemoteRegistryURL()
if registryURL == nil || err != nil {
return nil, err
}
xs, err := registry.List(ctx, registryURL, query)
if err != nil {
return nil, err
}
for _, x := range xs {
x.RegistryURL = registryURL.String()
}
return xs, nil
}

View File

@ -1,14 +0,0 @@
package api
import (
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/schema"
)
func enableLegacyExtensions() {
conf.Mock(&conf.Unified{SiteConfiguration: schema.SiteConfiguration{
ExperimentalFeatures: &schema.ExperimentalFeatures{
EnableLegacyExtensions: true,
},
}})
}

View File

@ -2,52 +2,9 @@ package api
import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// This list is different from the builtinExtensions since it only includes code
// intel extensions.
var codeIntelExtensions = map[string]bool{
"sourcegraph/apex": true,
"sourcegraph/clojure": true,
"sourcegraph/cobol": true,
"sourcegraph/cpp": true,
"sourcegraph/csharp": true,
"sourcegraph/cuda": true,
"sourcegraph/dart": true,
"sourcegraph/elixir": true,
"sourcegraph/erlang": true,
"sourcegraph/go": true,
"sourcegraph/graphql": true,
"sourcegraph/groovy": true,
"sourcegraph/haskell": true,
"sourcegraph/java": true,
"sourcegraph/jsonnet": true,
"sourcegraph/kotlin": true,
"sourcegraph/lisp": true,
"sourcegraph/lua": true,
"sourcegraph/ocaml": true,
"sourcegraph/pascal": true,
"sourcegraph/perl": true,
"sourcegraph/php": true,
"sourcegraph/powershell": true,
"sourcegraph/protobuf": true,
"sourcegraph/python": true,
"sourcegraph/r": true,
"sourcegraph/ruby": true,
"sourcegraph/rust": true,
"sourcegraph/scala": true,
"sourcegraph/shell": true,
"sourcegraph/starlark": true,
"sourcegraph/swift": true,
"sourcegraph/tcl": true,
"sourcegraph/thrift": true,
"sourcegraph/typescript": true,
"sourcegraph/verilog": true,
"sourcegraph/vhdl": true,
}
func ExtensionRegistryReadEnabled() error {
// We need to allow read access to the extension registry of sourcegraph.com to allow instances
// on older versions to fetch extensions.
@ -55,34 +12,5 @@ func ExtensionRegistryReadEnabled() error {
return nil
}
if conf.ExperimentalFeatures().EnableLegacyExtensions {
return nil
}
return errors.Errorf("extensions are disabled")
}
// The extensions list query (`extensionRegistry.extensions`) will be called by the native
// integration, a deployment method where we do not have control over updating the client.
//
// An example for this is the GitLab native integration that ships with GitLab releases.
// When enabled, it will make a GraphQL query to Sourcegraph to get a list of valid
// extensions.
//
// Since we want to support code navigation in the native integrations after 4.0, we are
// special-casing this API and make it return only code intel legacy extensions.
//
// Since the dotcom instance will be connected to from instances before 4.0, we'll need
// to keep the behavior of being able to list all extensions there.
//
// This method returns nil if all extensions are allowed to be listed.
func ExtensionRegistryListAllowedExtensions() map[string]bool {
err := ExtensionRegistryReadEnabled()
if err != nil {
return codeIntelExtensions
}
// nil means all extensions are allowed
return nil
}

View File

@ -1,24 +0,0 @@
package api
import (
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
"github.com/sourcegraph/sourcegraph/internal/database"
)
func init() {
graphqlbackend.ExtensionRegistry = func(db database.DB) graphqlbackend.ExtensionRegistryResolver {
ExtensionRegistry.db = db
return &ExtensionRegistry
}
}
// ExtensionRegistry is the implementation of the GraphQL type ExtensionRegistry.
//
// To supply implementations of extension registry functionality, use the fields on this value of
// extensionRegistryResolver.
var ExtensionRegistry extensionRegistryResolver
// extensionRegistryResolver implements the GraphQL type ExtensionRegistry.
type extensionRegistryResolver struct {
db database.DB
}

View File

@ -1,57 +0,0 @@
package conf
import (
"os"
"strconv"
"github.com/sourcegraph/sourcegraph/schema"
)
// PlatformConfiguration contains site configuration for the Sourcegraph platform.
type PlatformConfiguration struct {
RemoteRegistryURL string
}
// DefaultRemoteRegistry is the default value for the site configuration property
// "extensions"."remoteRegistry".
//
// It is intentionally not set in the OSS build.
var DefaultRemoteRegistry string
// Extensions returns the configuration for the Sourcegraph platform, or nil if it is disabled.
func Extensions() *PlatformConfiguration {
cfg := Get()
x := cfg.Extensions
if x == nil {
if DefaultRemoteRegistry == "" {
// There is no reasonable default behavior for extensions given that there is no remote
// registry, so consider them disabled.
return nil
}
x = &schema.Extensions{}
}
if x.Disabled != nil && *x.Disabled {
return nil
}
var pc PlatformConfiguration
// If the "remoteRegistry" value is a string, use that. If false, then keep it empty. Otherwise
// use the default.
if s, ok := x.RemoteRegistry.(string); ok {
pc.RemoteRegistryURL = s
} else if b, ok := x.RemoteRegistry.(bool); ok && !b {
// Nothing to do.
} else {
pc.RemoteRegistryURL = DefaultRemoteRegistry
}
if v, _ := strconv.ParseBool(os.Getenv("OFFLINE")); v {
pc.RemoteRegistryURL = ""
}
return &pc
}

View File

@ -1,76 +0,0 @@
package conf
import (
"os"
"reflect"
"testing"
"github.com/sourcegraph/sourcegraph/schema"
)
func TestExtensions(t *testing.T) {
check := func(t *testing.T, want *PlatformConfiguration) {
t.Helper()
got := Extensions()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}
}
t.Run("no config and no DefaultRemoteRegistry", func(t *testing.T) {
DefaultRemoteRegistry = ""
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{}})
check(t, nil)
})
t.Run("no config but valid DefaultRemoteRegistry", func(t *testing.T) {
DefaultRemoteRegistry = "x"
defer func() { DefaultRemoteRegistry = "" }()
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{}})
check(t, &PlatformConfiguration{RemoteRegistryURL: "x"})
})
t.Run("empty config, valid DefaultRemoteRegistry", func(t *testing.T) {
DefaultRemoteRegistry = "x"
defer func() { DefaultRemoteRegistry = "" }()
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{}}})
check(t, &PlatformConfiguration{RemoteRegistryURL: "x"})
})
t.Run("config extensions.disabled", func(t *testing.T) {
DefaultRemoteRegistry = "x"
defer func() { DefaultRemoteRegistry = "" }()
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{Disabled: boolPtr(true)}}})
check(t, nil)
})
t.Run("config extensions.disabled false", func(t *testing.T) {
DefaultRemoteRegistry = "x"
defer func() { DefaultRemoteRegistry = "" }()
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{Disabled: boolPtr(false)}}})
check(t, &PlatformConfiguration{RemoteRegistryURL: "x"})
})
t.Run("config extensions.remoteRegistry overrides DefaultRemoteRegistry", func(t *testing.T) {
DefaultRemoteRegistry = "x"
defer func() { DefaultRemoteRegistry = "" }()
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{RemoteRegistry: "y"}}})
check(t, &PlatformConfiguration{RemoteRegistryURL: "y"})
})
t.Run("config extensions.remoteRegistry false", func(t *testing.T) {
DefaultRemoteRegistry = "x"
defer func() { DefaultRemoteRegistry = "" }()
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{RemoteRegistry: false}}})
check(t, &PlatformConfiguration{RemoteRegistryURL: ""})
})
t.Run("OFFLINE env var", func(t *testing.T) {
os.Setenv("OFFLINE", "1")
defer os.Unsetenv("OFFLINE")
DefaultRemoteRegistry = "x"
defer func() { DefaultRemoteRegistry = "" }()
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{}})
check(t, &PlatformConfiguration{RemoteRegistryURL: ""})
})
}

Some files were not shown because too many files have changed in this diff Show More