remove code host native tooltip toggle, hover alerts, command palette, notifs (#48688)

- Remove support for toggling native tooltips from the browser
extension. This let users choose to keep the browser extension installed
and partially active on GitHub but not show Sourcegraph's hovers. This
functionality is less important now that GitHub's new code nav no longer
uses hovers. For old GitHub Enterprise server instances (and GitHub.com
users who have not enabled the new code view), users can disable the
browser extension if they want to disable Sourcegraph's hover
functionality.
- Remove hover alerts, which were used to warn users that the results
are imprecise. We still show this in a hover badge, which is a much
nicer UI for this than a dismissible warning (which felt more "CYA").
- Remove command palette because it was inextricable from notifications
and it was only used by the Sourcegraph extension API (which is
deprecated and will be removed).
- Remove the old notifications UI, which showed notification messages in
Sourcegraph and code host UIs. This is no longer necessary with the
removal of the command palette, because it is no longer possible to
invoke long-running actions whose errors must be shown in a separate,
global UI.

## Test plan

Existing tests suffice to test the existing code intelligence
functionality. This PR just removes functionality.
This commit is contained in:
Quinn Slack 2023-03-06 20:36:18 -08:00 committed by GitHub
parent 46d81a9fa1
commit ae338b9797
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 100 additions and 3610 deletions

View File

@ -171,9 +171,7 @@ ts_project(
"src/shared/code-hosts/shared/errors.ts",
"src/shared/code-hosts/shared/extensions.tsx",
"src/shared/code-hosts/shared/getNotificationClassName.ts",
"src/shared/code-hosts/shared/hoverAlerts.tsx",
"src/shared/code-hosts/shared/inject.ts",
"src/shared/code-hosts/shared/nativeTooltips.tsx",
"src/shared/code-hosts/shared/testHelpers.ts",
"src/shared/code-hosts/shared/util/fileInfo.ts",
"src/shared/code-hosts/shared/util/selections.ts",

View File

@ -55,7 +55,6 @@ export interface SyncStorageItems extends SourcegraphURL {
* Overrides settings from Sourcegraph.
*/
clientSettings: string
dismissedHoverAlerts: Record<string, boolean | undefined>
}
export interface LocalStorageItems {}

View File

@ -14,22 +14,6 @@ $body-bg-color-light: #ffffff;
--dropdown-border-color: var(--border-color);
}
.command-palette-button {
align-self: center;
> .command-list-popover-button {
user-select: none;
position: relative;
}
}
.command-list-popover {
isolation: isolate;
z-index: 1100; // high enough to prevent most things from obscuring it
border: 1px solid var(--dropdown-border-color);
border-radius: 3px;
}
.sourcegraph-extensions-global {
position: fixed;
bottom: 0;

View File

@ -194,7 +194,6 @@ export const bitbucketCloudCodeHost: CodeHost = {
iconClassName: styles.icon,
contentClassName: styles.content,
},
notificationClassNames: { 1: '', 2: '', 3: '', 4: '', 5: '' },
codeViewsRequireTokenization: true,
observeLineSelection: fromEvent(window, 'hashchange').pipe(
startWith(undefined), // capture intital value

View File

@ -1,31 +1,3 @@
/* Command palette */
.command-palette-button {
z-index: 3000;
font-size: 13px;
svg {
/* The icon we use is taller than the other items' font size, so make it a bit shorter. */
height: 13px;
}
}
.command-palette-popover {
display: block !important;
max-width: unset !important;
header {
padding: 8px;
}
input {
max-width: unset !important;
}
}
.no-results {
padding: 10px;
}
/* Open on Sourcegraph button */
.open-on-sourcegraph {
margin-left: 2px; /* same as other buttons in the row */
@ -141,28 +113,3 @@
opacity: 0.7;
}
}
/* Bitbucket's style is copied here because adding the aui-dropdown2-trigger class
* to the command palette causes exceptions in Atlassian's JS. */
.command-list-popover-button {
padding-right: 24px !important;
&::after {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0;
font-family: 'Adgs Icons';
font-weight: normal;
font-style: normal;
content: '\f15b';
font-size: 16px;
height: 16px;
line-height: 1;
margin-top: -8px;
position: absolute;
right: 4px;
top: 50%;
text-indent: 0;
width: 16px;
}
}

View File

@ -6,7 +6,6 @@ import { bitbucketServerCodeHost, getToolbarMount, parseHash } from './codeHost'
describe('bitbucketServerCodeHost', () => {
testCodeHostMountGetters(bitbucketServerCodeHost, {
getCommandPaletteMount: `${__dirname}/__fixtures__/browse.html`,
getViewContextOnSourcegraphMount: `${__dirname}/__fixtures__/browse.html`,
})
describe('getToolbarMount()', () => {

View File

@ -5,13 +5,11 @@ import { Omit } from 'utility-types'
import { AdjustmentDirection, PositionAdjuster } from '@sourcegraph/codeintellify'
import { LineOrPositionOrRange } from '@sourcegraph/common'
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { FileSpec, RepoSpec, ResolvedRevisionSpec, RevisionSpec } from '@sourcegraph/shared/src/util/url'
import { querySelectorOrSelf } from '../../util/dom'
import { CodeHost, MountGetter } from '../shared/codeHost'
import { CodeHost } from '../shared/codeHost'
import { CodeView, DOMFunctions } from '../shared/codeViews'
import { createNotificationClassNameGetter } from '../shared/getNotificationClassName'
import { ViewResolver } from '../shared/views'
import { getContext } from './context'
@ -183,22 +181,6 @@ const newDiffCodeView: Omit<CodeView, 'element'> = {
dom: newDiffDOMFunctions,
}
const getCommandPaletteMount: MountGetter = (container: HTMLElement): HTMLElement | null => {
const headerElement = querySelectorOrSelf(container, '.aui-header-primary .aui-nav')
if (!headerElement) {
return null
}
const classNames = ['command-palette-button', styles.commandPaletteButton]
const create = (): HTMLElement => {
const mount = document.createElement('li')
mount.className = classNames.join(' ')
headerElement.append(mount)
return mount
}
const preexisting = headerElement.querySelector<HTMLElement>(classNames.map(className => `.${className}`).join(''))
return preexisting || create()
}
function getViewContextOnSourcegraphMount(container: HTMLElement): HTMLElement | null {
const branchSelectorButtons = querySelectorOrSelf(container, '.branch-selector-toolbar .aui-buttons')
if (!branchSelectorButtons) {
@ -221,14 +203,6 @@ export const checkIsBitbucket = (): boolean =>
const iconClassName = 'aui-icon'
const notificationClassNames = {
[NotificationType.Log]: 'aui-message aui-message-info',
[NotificationType.Success]: 'aui-message aui-message-success',
[NotificationType.Info]: 'aui-message aui-message-info',
[NotificationType.Warning]: 'aui-message aui-message-warning',
[NotificationType.Error]: 'aui-message aui-message-error',
}
export const parseHash = (hash: string): LineOrPositionOrRange => {
if (hash.startsWith('#')) {
hash = hash.slice(1)
@ -254,29 +228,6 @@ export const bitbucketServerCodeHost: CodeHost = {
name: 'Bitbucket Server',
check: checkIsBitbucket,
codeViewResolvers: [codeViewResolver, diffCodeViewResolver],
getCommandPaletteMount,
notificationClassNames,
commandPaletteClassProps: {
buttonClassName: classNames(
styles.commandListPopoverButton,
'aui-alignment-target aui-alignment-abutted aui-alignment-abutted-left aui-alignment-element-attached-top aui-alignment-element-attached-left aui-alignment-target-attached-bottom aui-alignment-target-attached-left'
),
buttonElement: 'a',
buttonOpenClassName: 'aui-dropdown2-active active aui-alignment-enabled',
showCaret: false,
popoverClassName: classNames(
styles.commandPalettePopover,
'aui-dropdown2 aui-style-default aui-layer aui-dropdown2-in-header aui-alignment-element aui-alignment-side-bottom aui-alignment-snap-left aui-alignment-enabled aui-alignment-abutted aui-alignment-abutted-left aui-alignment-element-attached-top aui-alignment-element-attached-left aui-alignment-target-attached-bottom aui-alignment-target-attached-left'
),
formClassName: 'aui',
inputClassName: 'text',
resultsContainerClassName: 'results',
listClassName: 'results-list',
listItemClassName: 'result',
selectedListItemClassName: 'focused',
noResultsClassName: styles.noResults,
iconClassName,
},
codeViewToolbarClassProps: {
className: classNames(styles.codeViewToolbar, 'aui-buttons'),
actionItemClass: 'aui-button',
@ -287,7 +238,6 @@ export const bitbucketServerCodeHost: CodeHost = {
hoverOverlayClassProps: {
className: 'aui-dialog',
actionItemClassName: classNames('aui-button', styles.hoverActionItem),
getAlertClassName: createNotificationClassNameGetter(notificationClassNames),
iconClassName,
},
getViewContextOnSourcegraphMount,

View File

@ -365,7 +365,6 @@ export const gerritCodeHost: CodeHost = {
type: 'gerrit',
name: 'Gerrit',
codeViewResolvers,
nativeTooltipResolvers: [],
codeViewsRequireTokenization: true,
// This overrides the default observeMutations because we need to handle shadow DOMS.
observeMutations,
@ -378,7 +377,6 @@ export const gerritCodeHost: CodeHost = {
})
},
check: checkIsGerrit,
notificationClassNames: { 1: '', 2: '', 3: '', 4: '', 5: '' },
hoverOverlayClassProps: {
className: styles.hoverOverlay,
},

View File

@ -18,21 +18,6 @@
}
}
.command-palette-popover {
--dropdown-bg: var(--body-bg);
--dropdown-border-color: var(--border-color);
--popover-border-radius: 6px;
}
.command-palette-action-item {
/* Reset GitHub's 44px min-height */
min-height: initial;
/* Reset default user agent button styles */
border: none;
background-color: transparent;
}
.action-item {
/* Match GitHub's button height even if button only contains icon
* (no text that would push the height) */

View File

@ -7,7 +7,6 @@ import { Omit } from 'utility-types'
import { AdjustmentDirection, PositionAdjuster } from '@sourcegraph/codeintellify'
import { LineOrPositionOrRange } from '@sourcegraph/common'
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { observeSystemIsLightTheme } from '@sourcegraph/shared/src/deprecated-theme-utils'
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import { createURLWithUTM } from '@sourcegraph/shared/src/tracking/utm'
@ -27,13 +26,10 @@ import { getPlatformName } from '../../util/context'
import { querySelectorAllOrSelf, querySelectorOrSelf } from '../../util/dom'
import { CodeHost, MountGetter } from '../shared/codeHost'
import { CodeView, toCodeViewResolver } from '../shared/codeViews'
import { createNotificationClassNameGetter } from '../shared/getNotificationClassName'
import { NativeTooltip } from '../shared/nativeTooltips'
import { getSelectionsFromHash, observeSelectionsFromHash } from '../shared/util/selections'
import { ViewResolver } from '../shared/views'
import { diffDomFunctions, searchCodeSnippetDOMFunctions, singleFileDOMFunctions } from './domFunctions'
import { getCommandPaletteMount } from './extensions'
import { resolveDiffFileInfo, resolveFileInfo, resolveSnippetFileInfo } from './fileInfo'
import { getFileContainers, parseURL, getSelectorFor } from './util'
@ -320,21 +316,8 @@ export const createOpenOnSourcegraphIfNotExists: MountGetter = (container: HTMLE
return mount
}
const nativeTooltipResolver: ViewResolver<NativeTooltip> = {
selector: '.js-tagsearch-popover',
resolveView: element => ({ element }),
}
const iconClassName = classNames(styles.icon, 'v-align-text-bottom')
const notificationClassNames = {
[NotificationType.Log]: 'flash',
[NotificationType.Success]: 'flash flash-success',
[NotificationType.Info]: 'flash',
[NotificationType.Warning]: 'flash flash-warn',
[NotificationType.Error]: 'flash flash-error',
}
const searchEnhancement: GithubCodeHost['searchEnhancement'] = {
searchViewResolver: {
selector: '.js-site-search-form input[type="text"][aria-controls="jump-to-results"]',
@ -682,7 +665,6 @@ export const githubCodeHost: GithubCodeHost = {
searchEnhancement,
enhanceSearchPage,
codeViewResolvers: [genericCodeViewResolver, fileLineContainerResolver, searchResultCodeViewResolver],
nativeTooltipResolvers: [nativeTooltipResolver],
routeChange: mutations =>
mutations.pipe(
map(() => {
@ -728,24 +710,6 @@ export const githubCodeHost: GithubCodeHost = {
iconClassName,
},
check: checkIsGitHub,
getCommandPaletteMount,
notificationClassNames,
commandPaletteClassProps: {
buttonClassName: 'Header-link d-flex flex-items-baseline',
popoverClassName: classNames('Box', styles.commandPalettePopover),
formClassName: 'p-1',
inputClassName: 'form-control input-sm header-search-input jump-to-field-active',
listClassName: 'p-0 m-0 js-navigation-container jump-to-suggestions-results-container',
selectedListItemClassName: 'navigation-focus',
listItemClassName:
'd-flex flex-justify-start flex-items-center p-0 f5 navigation-item js-navigation-item js-jump-to-scoped-search',
actionItemClassName: classNames(
styles.commandPaletteActionItem,
'no-underline d-flex flex-auto flex-items-center jump-to-suggestions-path p-2'
),
noResultsClassName: 'd-flex flex-auto flex-items-center jump-to-suggestions-path p-2',
iconClassName,
},
codeViewToolbarClassProps: {
className: styles.codeViewToolbar,
listItemClass: classNames(styles.codeViewToolbarItem, 'BtnGroup'),
@ -759,7 +723,6 @@ export const githubCodeHost: GithubCodeHost = {
actionItemPressedClassName: 'active',
closeButtonClassName: 'btn-octicon p-0 hover-overlay__close-button--github',
badgeClassName: classNames('label', styles.hoverOverlayBadge),
getAlertClassName: createNotificationClassNameGetter(notificationClassNames, 'flash-full'),
iconClassName,
},
urlToFile: (sourcegraphURL, target, context) => {

View File

@ -1,37 +0,0 @@
import { querySelectorOrSelf } from '../../util/dom'
import { MountGetter } from '../shared/codeHost'
export const getCommandPaletteMount: MountGetter = (container: HTMLElement): HTMLElement | null => {
const className = 'command-palette-button'
// This selector matches both GitHub Enterprise and github.com
const existing =
container.querySelector<HTMLElement>(`.Header .${className}`) ||
container.querySelector<HTMLElement>(`.Header-old .${className}`) // selector for not logged in user on github.com
if (existing) {
return existing
}
// Legacy header (GitHub Enterprise)
const gheHeaderElement = querySelectorOrSelf(container, '.HeaderMenu > :last-child')
if (gheHeaderElement) {
const mount = document.createElement('div')
mount.classList.add(className)
gheHeaderElement.prepend(mount)
return mount
}
// github.com doesn't use HeaderMenu to wrap the right-hand-side menu anymore,
// it has a flatter DOM structure
// Instead of finding the parent to insert into, find the sibling to insert next to
let rightNeighbor = querySelectorOrSelf(container, '.Header-item:nth-last-child(2)')
if (rightNeighbor) {
// Caveat: there is no noticiations icon if web notifications are disabled,
// but the empty header item is still there
if (rightNeighbor.previousElementSibling!.children.length !== 0) {
rightNeighbor = rightNeighbor.previousElementSibling!
}
const mount = document.createElement('div')
mount.classList.add('Header-item', 'mr-0', 'mr-lg-3', className)
rightNeighbor.before(mount)
return mount
}
return null
}

View File

@ -2,25 +2,6 @@
--gray-10: #fafafa; // override value to avoid style conflics: https://github.com/sourcegraph/sourcegraph/pull/32548
}
.command-list-popover {
// The navbar has z-index 1000
z-index: 1001 !important;
--dropdown-bg: var(--gray-50, #ffffff);
--dropdown-shadow: 0 2px 4px rgba(0, 0, 0, 10%);
--dropdown-border-color: var(--border-color);
--popover-border-radius: 3px;
}
:global(.show) > .command-list-popover {
display: block;
}
.command-palette-button > span {
display: flex;
align-items: center;
}
.btn-icon {
img {
// Gitlab applies this to svgs,

View File

@ -6,7 +6,6 @@ import { Omit } from 'utility-types'
import { fetchCache, LineOrPositionOrRange, subtypeOf } from '@sourcegraph/common'
import { gql, dataOrThrowErrors } from '@sourcegraph/http-client'
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { toAbsoluteBlobURL } from '@sourcegraph/shared/src/util/url'
import { background } from '../../../browser-extension/web-extension-api/runtime'
@ -14,12 +13,10 @@ import { ResolveRepoNameResult, ResolveRepoNameVariables } from '../../../graphq
import { isInPage } from '../../context'
import { CodeHost } from '../shared/codeHost'
import { CodeView } from '../shared/codeViews'
import { createNotificationClassNameGetter } from '../shared/getNotificationClassName'
import { getSelectionsFromHash, observeSelectionsFromHash } from '../shared/util/selections'
import { queryWithSelector, ViewResolver } from '../shared/views'
import { diffDOMFunctions, singleFileDOMFunctions } from './domFunctions'
import { getCommandPaletteMount } from './extensions'
import { resolveCommitFileInfo, resolveDiffFileInfo, resolveFileInfo } from './fileInfo'
import {
getPageInfo,
@ -121,14 +118,6 @@ const codeViewResolver: ViewResolver<CodeView> = {
resolveView,
}
const notificationClassNames = {
[NotificationType.Log]: 'alert alert-secondary',
[NotificationType.Success]: 'alert alert-success',
[NotificationType.Info]: 'alert alert-info',
[NotificationType.Warning]: 'alert alert-warning',
[NotificationType.Error]: 'alert alert-danger',
}
/**
* Checks whether repository is private or not using Gitlab API
*
@ -202,7 +191,6 @@ export const gitlabCodeHost = subtypeOf<CodeHost>()({
name: 'GitLab',
check: checkIsGitlab,
codeViewResolvers: [codeViewResolver],
getCommandPaletteMount,
getContext: async () => {
const { repoName, ...pageInfo } = getPageInfo()
return {
@ -252,16 +240,6 @@ export const gitlabCodeHost = subtypeOf<CodeHost>()({
}
return url.href
},
notificationClassNames,
commandPaletteClassProps: {
popoverClassName: classNames('dropdown-menu', styles.commandListPopover),
formClassName: 'dropdown-input',
inputClassName: 'dropdown-input-field',
resultsContainerClassName: 'dropdown-content',
selectedActionItemClassName: 'is-focused',
noResultsClassName: 'px-3',
iconClassName: 's16 align-bottom',
},
codeViewToolbarClassProps: {
className: 'pl-0',
actionItemClass: 'btn btn-md gl-button btn-icon',
@ -274,7 +252,6 @@ export const gitlabCodeHost = subtypeOf<CodeHost>()({
actionItemPressedClassName: 'active',
closeButtonClassName: 'btn btn-transparent p-0 btn-icon--gitlab',
iconClassName: 'square s16',
getAlertClassName: createNotificationClassNameGetter(notificationClassNames),
},
codeViewsRequireTokenization: true,
getHoverOverlayMountLocation: (): string | null => {

View File

@ -1,19 +0,0 @@
import { querySelectorOrSelf } from '../../util/dom'
import { MountGetter } from '../shared/codeHost'
import styles from './codeHost.module.scss'
export const getCommandPaletteMount: MountGetter = (container: HTMLElement): HTMLElement | null => {
const headerElement = querySelectorOrSelf(container, '.navbar-collapse')
if (!headerElement) {
return null
}
const commandListClass = 'command-palette-button'
const createCommandList = (): HTMLElement => {
const mount = document.createElement('div')
mount.classList.add(commandListClass, styles.commandPaletteButton)
headerElement.prepend(mount)
return mount
}
return headerElement.querySelector<HTMLElement>('.' + commandListClass) || createCommandList()
}

View File

@ -3,14 +3,12 @@ import { map } from 'rxjs/operators'
import { AdjustmentDirection, PositionAdjuster } from '@sourcegraph/codeintellify'
import { Position } from '@sourcegraph/extension-api-types'
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import { FileSpec, RepoSpec, ResolvedRevisionSpec, RevisionSpec } from '@sourcegraph/shared/src/util/url'
import { fetchBlobContentLines } from '../../repo/backend'
import { CodeHost } from '../shared/codeHost'
import { CodeView, toCodeViewResolver } from '../shared/codeViews'
import { createNotificationClassNameGetter } from '../shared/getNotificationClassName'
import { ViewResolver } from '../shared/views'
import { diffDomFunctions, diffusionDOMFns } from './domFunctions'
@ -175,14 +173,6 @@ const phabSourceCodeViewResolver = toCodeViewResolver('.phabricator-source-code-
resolveFileInfo: resolveDiffusionFileInfo,
})
const notificationClassNames = {
[NotificationType.Log]: 'phui-info-view phui-info-severity-plain',
[NotificationType.Success]: 'phui-info-view phui-info-severity-success',
[NotificationType.Info]: 'phui-info-view phui-info-severity-notice',
[NotificationType.Warning]: 'phui-info-view phui-info-severity-warning',
[NotificationType.Error]: 'phui-info-view phui-info-severity-error',
}
export const checkIsPhabricator = (): boolean => !!document.querySelector('.phabricator-wordmark')
export const phabricatorCodeHost: CodeHost = {
@ -201,13 +191,11 @@ export const phabricatorCodeHost: CodeHost = {
actionItemClass: classNames('button grey', styles.actionItem),
actionItemIconClass: styles.icon,
},
notificationClassNames,
hoverOverlayClassProps: {
className: classNames('aphront-dialog-view', styles.hoverOverlay),
actionItemClassName: classNames('button grey', styles.hoverOverlayActionItem),
closeButtonClassName: 'button grey btn-icon--phabricator',
iconClassName: styles.hoverOverlayActionItemIcon,
getAlertClassName: createNotificationClassNameGetter(notificationClassNames),
},
codeViewsRequireTokenization: true,
}

View File

@ -15,7 +15,6 @@ import { SuccessGraphQLResult } from '@sourcegraph/http-client'
import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
import { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
import { ExtensionCodeEditor } from '@sourcegraph/shared/src/api/extension/api/codeEditor'
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { Controller } from '@sourcegraph/shared/src/extensions/controller'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { MockIntersectionObserver } from '@sourcegraph/shared/src/testing/MockIntersectionObserver'
@ -39,14 +38,6 @@ import { DEFAULT_GRAPHQL_RESPONSES, mockRequestGraphQL } from './testHelpers'
const RENDER = sinon.spy()
const notificationClassNames = {
[NotificationType.Log]: 'log',
[NotificationType.Success]: 'success',
[NotificationType.Info]: 'info',
[NotificationType.Warning]: 'warning',
[NotificationType.Error]: 'error',
}
const elementRenderedAtMount = (mount: Element): RenderResult | undefined => {
const call = RENDER.args.find(call => call[1] === mount)
return call?.[0]
@ -68,7 +59,6 @@ jest.mock('uuid', () => ({
const createMockController = (extensionHostAPI: Remote<FlatExtensionHostAPI>): Controller => ({
executeCommand: () => Promise.resolve(),
registerCommand: () => new Subscription(),
commandErrors: NEVER,
unsubscribe: noop,
extHostAPI: Promise.resolve(extensionHostAPI),
})
@ -149,7 +139,6 @@ describe('codeHost', () => {
name: 'GitHub',
check: () => true,
codeViewResolvers: [],
notificationClassNames,
},
extensionsController: createMockController(extensionHostAPI),
})
@ -161,27 +150,6 @@ describe('codeHost', () => {
expect(renderedOverlay).not.toBeUndefined()
})
test('renders the command palette if codeHost.getCommandPaletteMount is defined', async () => {
const { extensionHostAPI } = await integrationTestContext()
const commandPaletteMount = createTestElement()
subscriptions.add(
await handleCodeHost({
...commonArguments(),
codeHost: {
type: 'github',
name: 'GitHub',
check: () => true,
getCommandPaletteMount: () => commandPaletteMount,
codeViewResolvers: [],
notificationClassNames,
},
extensionsController: createMockController(extensionHostAPI),
})
)
const renderedCommandPalette = elementRenderedAtMount(commandPaletteMount)
expect(renderedCommandPalette).not.toBeUndefined()
})
test('detects code views based on selectors', async () => {
const { extensionHostAPI, extensionAPI } = await integrationTestContext(undefined, {
roots: [],
@ -205,7 +173,6 @@ describe('codeHost', () => {
type: 'github',
name: 'GitHub',
check: () => true,
notificationClassNames,
codeViewResolvers: [
toCodeViewResolver('#code', {
dom: {
@ -214,7 +181,7 @@ describe('codeHost', () => {
getLineElementFromLineNumber: sinon.spy(),
getLineNumberFromCodeElement: sinon.spy(),
},
resolveFileInfo: codeView => of(blobInfo),
resolveFileInfo: () => of(blobInfo),
getToolbarMount: () => toolbarMount,
}),
],
@ -284,7 +251,6 @@ describe('codeHost', () => {
type: 'github',
name: 'GitHub',
check: () => true,
notificationClassNames,
codeViewResolvers: [
toCodeViewResolver('.code', {
dom: {
@ -349,7 +315,7 @@ describe('codeHost', () => {
expect(getEditors(extensionAPI)).toEqual([])
})
test('Hoverifies a view if the code host has no nativeTooltipResolvers', async () => {
test('Hoverifies a view', async () => {
const { extensionHostAPI, extensionAPI } = await integrationTestContext(undefined, {
roots: [],
viewers: [],
@ -372,11 +338,10 @@ describe('codeHost', () => {
type: 'github',
name: 'GitHub',
check: () => true,
notificationClassNames,
codeViewResolvers: [
toCodeViewResolver('#code', {
dom,
resolveFileInfo: codeView =>
resolveFileInfo: () =>
of({
blob: {
rawRepoName: 'foo',
@ -396,129 +361,6 @@ describe('codeHost', () => {
codeView.dispatchEvent(new MouseEvent('mouseover'))
sinon.assert.called(dom.getCodeElementFromTarget)
})
test('Does not hoverify a view if the code host has nativeTooltipResolvers and they are enabled from settings', async () => {
const { extensionHostAPI, extensionAPI } = await integrationTestContext(undefined, {
roots: [],
viewers: [],
})
const codeView = createTestElement()
codeView.id = 'code'
const codeElement = document.createElement('span')
codeElement.textContent = 'alert(1)'
codeView.append(codeElement)
const dom = {
getCodeElementFromTarget: sinon.spy(() => codeElement),
getCodeElementFromLineNumber: sinon.spy(() => codeElement),
getLineElementFromLineNumber: sinon.spy(() => codeElement),
getLineNumberFromCodeElement: sinon.spy(() => 1),
}
subscriptions.add(
await handleCodeHost({
...commonArguments(),
codeHost: {
type: 'github',
name: 'GitHub',
check: () => true,
notificationClassNames,
nativeTooltipResolvers: [{ selector: '.native', resolveView: element => ({ element }) }],
codeViewResolvers: [
toCodeViewResolver('#code', {
dom,
resolveFileInfo: codeView =>
of({
blob: {
rawRepoName: 'foo',
filePath: '/bar.ts',
commitID: '1',
},
}),
}),
],
},
extensionsController: createMockController(extensionHostAPI),
platformContext: {
...createMockPlatformContext(),
settings: of({
subjects: [],
final: {
extensions: {},
'codeHost.useNativeTooltips': true,
},
}),
},
})
)
await wrapRemoteObservable(extensionHostAPI.viewerUpdates()).pipe(first()).toPromise()
expect(getEditors(extensionAPI).length).toEqual(1)
await tick()
codeView.dispatchEvent(new MouseEvent('mouseover'))
sinon.assert.notCalled(dom.getCodeElementFromTarget)
})
test('Hides native tooltips if they are disabled from settings', async () => {
const { extensionHostAPI, extensionAPI } = await integrationTestContext(undefined, {
roots: [],
viewers: [],
})
const codeView = createTestElement()
codeView.id = 'code'
const codeElement = document.createElement('span')
codeElement.textContent = 'alert(1)'
codeView.append(codeElement)
const nativeTooltip = createTestElement()
nativeTooltip.classList.add('native')
const dom = {
getCodeElementFromTarget: sinon.spy(() => codeElement),
getCodeElementFromLineNumber: sinon.spy(() => codeElement),
getLineElementFromLineNumber: sinon.spy(() => codeElement),
getLineNumberFromCodeElement: sinon.spy(() => 1),
}
subscriptions.add(
await handleCodeHost({
...commonArguments(),
codeHost: {
type: 'github',
name: 'GitHub',
check: () => true,
notificationClassNames,
nativeTooltipResolvers: [{ selector: '.native', resolveView: element => ({ element }) }],
codeViewResolvers: [
toCodeViewResolver('#code', {
dom,
resolveFileInfo: codeView =>
of({
blob: {
rawRepoName: 'foo',
filePath: '/bar.ts',
commitID: '1',
},
}),
}),
],
},
extensionsController: createMockController(extensionHostAPI),
platformContext: {
...createMockPlatformContext(),
settings: of({
subjects: [],
final: {
extensions: {},
'codeHost.useNativeTooltips': false,
},
}),
},
})
)
await wrapRemoteObservable(extensionHostAPI.viewerUpdates()).pipe(first()).toPromise()
expect(getEditors(extensionAPI).length).toEqual(1)
await tick()
codeView.dispatchEvent(new MouseEvent('mouseover'))
sinon.assert.called(dom.getCodeElementFromTarget)
expect(nativeTooltip).toHaveAttribute('data-native-tooltip-hidden', 'true')
})
})
describe('observeHoverOverlayMountLocation()', () => {

View File

@ -63,16 +63,10 @@ import { ActionItemAction } from '@sourcegraph/shared/src/actions/ActionItem'
import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
import { CodeEditorData, CodeEditorWithPartialModel } from '@sourcegraph/shared/src/api/viewerTypes'
import { isRepoNotFoundErrorLike } from '@sourcegraph/shared/src/backend/errors'
import { HoverAlert } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
import {
CommandListClassProps,
CommandListPopoverButtonClassProps,
} from '@sourcegraph/shared/src/commandPalette/CommandList'
import { Controller } from '@sourcegraph/shared/src/extensions/controller'
import { getHoverActions, registerHoverContributions } from '@sourcegraph/shared/src/hover/actions'
import { HoverContext, HoverOverlay, HoverOverlayClassProps } from '@sourcegraph/shared/src/hover/HoverOverlay'
import { getModeFromPath } from '@sourcegraph/shared/src/languages'
import { UnbrandedNotificationItemStyleProps } from '@sourcegraph/shared/src/notifications/NotificationItem'
import { PlatformContext, URLToFileContext } from '@sourcegraph/shared/src/platform/context'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { createURLWithUTM } from '@sourcegraph/shared/src/tracking/utm'
@ -112,14 +106,7 @@ import { phabricatorCodeHost } from '../phabricator/codeHost'
import { CodeView, trackCodeViews, fetchFileContentForDiffOrFileInfo } from './codeViews'
import { NotAuthenticatedError, RepoURLParseError } from './errors'
import { initializeExtensions, renderCommandPalette } from './extensions'
import { createRepoNotFoundHoverAlert, getActiveHoverAlerts, onHoverAlertDismissed } from './hoverAlerts'
import {
handleNativeTooltips,
NativeTooltip,
nativeTooltipsEnabledFromSettings,
registerNativeTooltipContributions,
} from './nativeTooltips'
import { initializeExtensions } from './extensions'
import { SignInButton } from './SignInButton'
import { resolveRepoNamesForDiffOrFileInfo, defaultRevisionToCommitID } from './util/fileInfo'
import {
@ -218,11 +205,6 @@ export interface CodeHost {
*/
codeViewResolvers: ViewResolver<CodeView>[]
/**
* Resolves {@link NativeTooltip}s from the DOM.
*/
nativeTooltipResolvers?: ViewResolver<NativeTooltip>[]
/**
* Override of `observeMutations`, used where a MutationObserve is not viable, such as in the shadow DOMs in Gerrit.
*/
@ -230,13 +212,6 @@ export interface CodeHost {
// Extensions related input
/**
* Mount getter for the command palette button for extensions.
*
* If undefined, the command palette button won't be rendered on the code host.
*/
getCommandPaletteMount?: MountGetter
/**
* Returns a selector used to determine the mount location of the hover overlay in the DOM.
*
@ -259,13 +234,6 @@ export interface CodeHost {
observeLineSelection?: Observable<LineOrPositionOrRange>
notificationClassNames: UnbrandedNotificationItemStyleProps['notificationItemClassNames']
/**
* CSS classes for the command palette to customize styling
*/
commandPaletteClassProps?: CommandListPopoverButtonClassProps & CommandListClassProps
/**
* CSS classes for the code view toolbar to customize styling
*/
@ -349,11 +317,9 @@ function initCodeIntelligence({
extensionsController,
render,
telemetryService,
hoverAlerts,
repoSyncErrors,
}: Pick<CodeIntelligenceProps, 'codeHost' | 'platformContext' | 'extensionsController' | 'telemetryService'> & {
render: Renderer
hoverAlerts: Observable<HoverAlert>[]
mutations: Observable<MutationRecordLike[]>
repoSyncErrors: Observable<boolean>
}): {
@ -394,8 +360,8 @@ function initCodeIntelligence({
getHover: ({ line, character, part, ...rest }) =>
concat(
[{ isLoading: true, result: null }],
combineLatest([
from(extensionsController.extHostAPI).pipe(
from(extensionsController.extHostAPI)
.pipe(
withLatestFrom(repoSyncErrors),
switchMap(([extensionHost, hasRepoSyncError]) =>
// Prevent GraphQL requests that we know will result in error/null when the repo is private (and not added to Cloud)
@ -407,23 +373,15 @@ function initCodeIntelligence({
)
)
)
),
getActiveHoverAlerts([
...hoverAlerts,
repoSyncErrors.pipe(
distinctUntilChanged(),
map(showAlert => (showAlert ? createRepoNotFoundHoverAlert(codeHost) : undefined)),
filter(isDefined)
),
]),
]).pipe(
map(
([{ isLoading, result: hoverMerged }, alerts]): MaybeLoadingResult<HoverMerged | null> => ({
isLoading,
result: hoverMerged || alerts?.length ? { contents: [], ...hoverMerged, alerts } : null,
})
)
)
.pipe(
map(
({ isLoading, result: hoverMerged }): MaybeLoadingResult<HoverMerged | null> => ({
isLoading,
result: hoverMerged || null,
})
)
)
),
getDocumentHighlights: ({ line, character, part, ...rest }) =>
from(extensionsController.extHostAPI).pipe(
@ -512,7 +470,6 @@ function initCodeIntelligence({
extensionsController={extensionsController}
platformContext={platformContext}
location={H.createLocation(window.location)}
onAlertDismissed={onHoverAlertDismissed}
useBrandedLogo={true}
/>
</TrackAnchorClick>
@ -754,7 +711,6 @@ export async function handleCodeHost({
hideActions,
background,
}: HandleCodeHostOptions): Promise<Subscription> {
const history = H.createBrowserHistory()
const subscriptions = new Subscription()
const { requestGraphQL, sourcegraphURL } = platformContext
@ -771,11 +727,6 @@ export async function handleCodeHost({
document.body.classList.toggle('theme-dark', !isLightTheme)
})
)
const nativeTooltipsEnabled = codeHost.nativeTooltipResolvers
? nativeTooltipsEnabledFromSettings(platformContext.settings)
: of(false)
const hoverAlerts: Observable<HoverAlert>[] = []
/**
* A stream that emits a boolean that signifies whether any request for
@ -817,49 +768,18 @@ export async function handleCodeHost({
return subscriptions
}
if (codeHost.nativeTooltipResolvers) {
const { subscription, nativeTooltipsAlert } = handleNativeTooltips(
mutations,
nativeTooltipsEnabled,
codeHost,
repoSyncErrors
)
subscriptions.add(subscription)
hoverAlerts.push(nativeTooltipsAlert)
subscriptions.add(registerNativeTooltipContributions(extensionsController))
}
const { hoverifier, subscription } = initCodeIntelligence({
codeHost,
extensionsController,
platformContext,
telemetryService,
render,
hoverAlerts,
mutations,
repoSyncErrors,
})
subscriptions.add(hoverifier)
subscriptions.add(subscription)
// Inject UI components
// Render command palette
if (codeHost.getCommandPaletteMount && !minimalUI && extensionsController !== null) {
subscriptions.add(
addedElements.pipe(map(codeHost.getCommandPaletteMount), filter(isDefined)).subscribe(
renderCommandPalette({
extensionsController,
history,
platformContext,
telemetryService,
render,
...codeHost.commandPaletteClassProps,
notificationClassNames: codeHost.notificationClassNames,
})
)
)
}
const signInCloses = new Subject<void>()
const nextSignInClose = signInCloses.next.bind(signInCloses)
@ -1268,32 +1188,25 @@ export async function handleCodeHost({
}
const adjustPosition = getPositionAdjuster?.(platformContext.requestGraphQL)
let hoverSubscription = new Subscription()
codeViewEvent.subscriptions.add(
nativeTooltipsEnabled.subscribe(useNativeTooltips => {
hoverSubscription.unsubscribe()
if (!useNativeTooltips) {
hoverSubscription = hoverifier.hoverify({
dom: domFunctions,
positionEvents: of(element).pipe(
findPositionsFromEvents({
domFunctions,
tokenize: !!(typeof overrideTokenize === 'boolean'
? overrideTokenize
: codeHost.codeViewsRequireTokenization),
})
),
resolveContext,
adjustPosition,
scrollBoundaries: codeViewEvent.getScrollBoundaries
? codeViewEvent.getScrollBoundaries(codeViewEvent.element)
: [],
overrideTokenize,
hoverifier.hoverify({
dom: domFunctions,
positionEvents: of(element).pipe(
findPositionsFromEvents({
domFunctions,
tokenize: !!(typeof overrideTokenize === 'boolean'
? overrideTokenize
: codeHost.codeViewsRequireTokenization),
})
}
),
resolveContext,
adjustPosition,
scrollBoundaries: codeViewEvent.getScrollBoundaries
? codeViewEvent.getScrollBoundaries(codeViewEvent.element)
: [],
overrideTokenize,
})
)
codeViewEvent.subscriptions.add(hoverSubscription)
element.classList.add('sg-mounted')
// Render toolbar
@ -1458,7 +1371,7 @@ export function injectCodeIntelligenceToCodeHost(
}
subscriptions.add(
// eslint-disable-next-line rxjs/no-async-subscribe, @typescript-eslint/no-misused-promises
// eslint-disable-next-line rxjs/no-async-subscribe
combineLatest([codeHostReady, extensionDisabled]).subscribe(async ([isCodeHostReady, disableExtension]) => {
if (disableExtension) {
// We don't need to unsubscribe if the extension starts with disabled state.

View File

@ -9,7 +9,7 @@ import { DiffPart } from '@sourcegraph/codeintellify'
import { CodeHost, MountGetter } from './codeHost'
import { CodeView, DOMFunctions } from './codeViews'
const mountGetterKeys = ['getCommandPaletteMount', 'getViewContextOnSourcegraphMount'] as const
const mountGetterKeys = ['getViewContextOnSourcegraphMount'] as const
type MountGetterKey = typeof mountGetterKeys[number]
/**

View File

@ -1,23 +1,6 @@
import classNames from 'classnames'
import * as H from 'history'
import { Renderer } from 'react-dom'
import { ContributableMenu } from '@sourcegraph/client-api'
import {
CommandListPopoverButton,
CommandListPopoverButtonProps,
} from '@sourcegraph/shared/src/commandPalette/CommandList'
import {
ExtensionsControllerProps,
RequiredExtensionsControllerProps,
} from '@sourcegraph/shared/src/extensions/controller'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { createController as createExtensionsController } from '@sourcegraph/shared/src/extensions/createSyncLoadedController'
import { UnbrandedNotificationItemStyleProps } from '@sourcegraph/shared/src/notifications/NotificationItem'
import { Notifications } from '@sourcegraph/shared/src/notifications/Notifications'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { ShortcutProvider } from '../../components/ShortcutProvider'
import { createPlatformContext, SourcegraphIntegrationURLs, BrowserPlatformContext } from '../../platform/context'
import { CodeHost } from './codeHost'
@ -35,38 +18,3 @@ export function initializeExtensions(
const extensionsController = createExtensionsController(platformContext)
return { platformContext, extensionsController }
}
interface InjectProps extends PlatformContextProps<'settings' | 'sourcegraphURL'>, RequiredExtensionsControllerProps {
history: H.History
render: Renderer
}
interface RenderCommandPaletteProps
extends TelemetryProps,
InjectProps,
Pick<CommandListPopoverButtonProps, 'inputClassName' | 'popoverClassName'> {
notificationClassNames: UnbrandedNotificationItemStyleProps['notificationItemClassNames']
}
export const renderCommandPalette =
({ extensionsController, history, render, ...props }: RenderCommandPaletteProps) =>
(mount: HTMLElement): void => {
render(
<ShortcutProvider>
<CommandListPopoverButton
{...props}
popoverClassName={classNames('command-list-popover', props.popoverClassName)}
menu={ContributableMenu.CommandPalette}
extensionsController={extensionsController}
location={history.location}
/>
<Notifications
extensionsController={extensionsController}
notificationItemStyleProps={{
notificationItemClassNames: props.notificationClassNames,
}}
/>
</ShortcutProvider>,
mount
)
}

View File

@ -1,8 +0,0 @@
import classNames from 'classnames'
import type { NotificationType } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
export const createNotificationClassNameGetter =
(notificationClassNames: Record<NotificationType, string>, extraClassName?: string) =>
(notificationType: NotificationType): string =>
classNames(notificationClassNames[notificationType], extraClassName)

View File

@ -1,76 +0,0 @@
import { Observable, of } from 'rxjs'
import { catchError, map, startWith, switchMap } from 'rxjs/operators'
import { combineLatestOrDefault } from '@sourcegraph/common'
import { MarkupKind } from '@sourcegraph/extension-api-classes'
import type { HoverAlert } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
import { ButtonLink } from '@sourcegraph/wildcard'
import { observeStorageKey, storage } from '../../../browser-extension/web-extension-api/storage'
import { SyncStorageItems } from '../../../browser-extension/web-extension-api/types'
import { isInPage } from '../../context'
import { CodeHost } from './codeHost'
/**
* Returns an Observable of all hover alerts that have not yet
* been dismissed by the user.
*/
export function getActiveHoverAlerts(allAlerts: Observable<HoverAlert>[]): Observable<HoverAlert[] | undefined> {
if (isInPage) {
return of(undefined)
}
return observeStorageKey('sync', 'dismissedHoverAlerts').pipe(
switchMap(dismissedAlerts =>
combineLatestOrDefault(allAlerts).pipe(
map(alerts => (dismissedAlerts ? alerts.filter(({ type }) => !type || !dismissedAlerts[type]) : alerts))
)
),
catchError(error => {
console.error('Error getting hover alerts', error)
return [undefined]
}),
startWith([])
)
}
/**
* Marks a hover alert as dismissed in sync storage.
*/
export async function onHoverAlertDismissed(alertType: string): Promise<void> {
try {
const partialStorageItems: Pick<SyncStorageItems, 'dismissedHoverAlerts'> = {
dismissedHoverAlerts: {},
...(await storage.sync.get('dismissedHoverAlerts')),
}
partialStorageItems.dismissedHoverAlerts[alertType] = true
await storage.sync.set(partialStorageItems)
} catch (error) {
console.error('Error dismissing alert', error)
}
}
/**
* Returns the alert to show when the user is on an unindexed repo and does not
* have sourcegraph.com as the URL. The alert informs the user to setup add a
* repo.
*/
export const createRepoNotFoundHoverAlert = (codeHost: Pick<CodeHost, 'hoverOverlayClassProps'>): HoverAlert => ({
type: 'private-code',
buttons: [
<ButtonLink
key="learn_more"
href="/help/admin/repo/add"
className={codeHost.hoverOverlayClassProps?.actionItemClassName ?? ''}
target="_blank"
rel="noopener norefferer"
>
Learn more
</ButtonLink>,
],
summary: {
kind: MarkupKind.Markdown,
value:
'#### Repository not added\n\n' +
'This repository is not indexed by your Sourcegraph instance. Add the repository to get Code Intelligence overlays.',
},
})

View File

@ -1,130 +0,0 @@
import { isEqual } from 'lodash'
import { from, Observable, Unsubscribable } from 'rxjs'
import {
distinctUntilChanged,
filter,
first,
map,
mapTo,
publishReplay,
refCount,
switchMap,
withLatestFrom,
} from 'rxjs/operators'
import { ErrorLike, isErrorLike, isDefined, isNot } from '@sourcegraph/common'
import { MarkupKind } from '@sourcegraph/extension-api-classes'
import { syncRemoteSubscription } from '@sourcegraph/shared/src/api/util'
import type { HoverAlert } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
import { Controller as ExtensionsController } from '@sourcegraph/shared/src/extensions/controller'
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import { Settings } from '@sourcegraph/shared/src/settings/settings'
import { MutationRecordLike } from '../../util/dom'
import { CodeHost } from './codeHost'
import { trackViews } from './views'
import styles from './nativeTooltips.module.scss'
const NATIVE_TOOLTIP_HIDDEN = styles.nativeTooltipHidden
const NATIVE_TOOLTIP_TYPE = 'nativeTooltips'
/**
* Defines a native tooltip that is present on a page and exposes operations for manipulating it.
*/
export interface NativeTooltip {
/** The native tooltip HTML element. */
element: HTMLElement
}
export function handleNativeTooltips(
mutations: Observable<MutationRecordLike[]>,
nativeTooltipsEnabled: Observable<boolean>,
{ nativeTooltipResolvers, name }: Pick<CodeHost, 'nativeTooltipResolvers' | 'name' | 'getContext'>,
repoSyncErrors: Observable<boolean>
): { nativeTooltipsAlert: Observable<HoverAlert>; subscription: Unsubscribable } {
const nativeTooltips = mutations.pipe(trackViews(nativeTooltipResolvers || []))
const nativeTooltipsAlert = nativeTooltips.pipe(
first(),
switchMap(() =>
repoSyncErrors.pipe(
filter(hasError => !hasError),
mapTo({
type: NATIVE_TOOLTIP_TYPE,
summary: {
kind: MarkupKind.Markdown,
value: `<small>Sourcegraph has hidden ${name}'s native hover tooltips. You can toggle this at any time: to enable the native tooltips run "Code host: prefer non-Sourcegraph hover tooltips" from the command palette or set <code>{"codeHost.useNativeTooltips": true}</code> in your user settings.</small>`,
},
})
)
),
publishReplay(1),
refCount()
)
return {
nativeTooltipsAlert,
subscription: nativeTooltips.subscribe(({ element, subscriptions }) => {
subscriptions.add(
nativeTooltipsEnabled
.pipe(withLatestFrom(repoSyncErrors))
// This subscription is correctly handled through the view's `subscriptions`
// eslint-disable-next-line rxjs/no-nested-subscribe
.subscribe(([enabled, hasRepoSyncError]) => {
// If we can't provide the user hovers because it's private code, don't hide native tooltips.
// Otherwise we would have to show the user two alerts at the same time.
const isTooltipHidden = !enabled && !hasRepoSyncError
element.dataset.nativeTooltipHidden = String(isTooltipHidden)
element.classList.toggle(NATIVE_TOOLTIP_HIDDEN, isTooltipHidden)
})
)
}),
}
}
export function nativeTooltipsEnabledFromSettings(settings: PlatformContext['settings']): Observable<boolean> {
return from(settings).pipe(
map(({ final }) => final),
filter(isDefined),
filter(isNot<ErrorLike | Settings, ErrorLike>(isErrorLike)),
map(settings => !!settings['codeHost.useNativeTooltips']),
distinctUntilChanged((a, b) => isEqual(a, b)),
publishReplay(1),
refCount()
)
}
export function registerNativeTooltipContributions(
extensionsController: Pick<ExtensionsController, 'extHostAPI'>
): Unsubscribable {
return syncRemoteSubscription(
extensionsController.extHostAPI.then(extensionHostAPI =>
extensionHostAPI.registerContributions({
actions: [
{
id: 'codeHost.toggleUseNativeTooltips',
command: 'updateConfiguration',
category: 'Code host',
commandArguments: [
'codeHost.useNativeTooltips',
/* eslint-disable-next-line no-template-curly-in-string */
'${!config.codeHost.useNativeTooltips}',
null,
'json',
],
title:
/* eslint-disable-next-line no-template-curly-in-string */
'Prefer ${config.codeHost.useNativeTooltips && "Sourcegraph" || "non-Sourcegraph"} hover tooltips',
},
],
menus: {
commandPalette: [
{
action: 'codeHost.toggleUseNativeTooltips',
},
],
},
})
)
)
}

View File

@ -4,7 +4,6 @@ import classNames from 'classnames'
import { BrowserRouter } from 'react-router-dom'
import { registerHighlightContributions } from '@sourcegraph/common'
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { HoverOverlay, HoverOverlayClassProps } from '@sourcegraph/shared/src/hover/HoverOverlay'
import {
commonProps,
@ -45,14 +44,6 @@ const BITBUCKET_CLASS_PROPS: HoverOverlayClassProps = {
className: 'aui-dialog',
actionItemClassName: classNames('aui-button', bitbucketCodeHostStyles.hoverActionItem),
iconClassName: 'aui-icon',
getAlertClassName: alertKind => {
switch (alertKind) {
case NotificationType.Error:
return 'aui-message aui-message-error'
default:
return 'aui-message aui-message-info'
}
},
}
export const BitbucketStyles: Story = (props = {}) => (

View File

@ -1,5 +0,0 @@
declare module 'string-score' {
function score(target: string, query: string, fuzzyFactor?: number): number
export = score
}

View File

@ -225,9 +225,6 @@ export interface ActionItem {
}
export enum ContributableMenu {
/** The global command palette. */
CommandPalette = 'commandPalette',
/** The global navigation bar in the application. */
GlobalNav = 'global/nav',

View File

@ -13,7 +13,6 @@ describe('HoverMerged', () => {
test('1 MarkupContent', () =>
expect(fromHoverMerged([{ contents: { kind: MarkupKind.Markdown, value: 'x' } }])).toEqual({
contents: [{ kind: MarkupKind.Markdown, value: 'x' }],
alerts: [],
aggregatedBadges: [],
}))
test('2 MarkupContents', () =>
@ -28,39 +27,6 @@ describe('HoverMerged', () => {
{ kind: MarkupKind.Markdown, value: 'y' },
],
range: FIXTURE_RANGE,
alerts: [],
aggregatedBadges: [],
}))
test('1 Alert', () =>
expect(
fromHoverMerged([
{
contents: { kind: MarkupKind.Markdown, value: 'x' },
alerts: [{ summary: { kind: MarkupKind.PlainText, value: 'x' } }],
},
])
).toEqual({
contents: [{ kind: MarkupKind.Markdown, value: 'x' }],
alerts: [{ summary: { kind: MarkupKind.PlainText, value: 'x' } }],
aggregatedBadges: [],
}))
test('2 Alerts', () =>
expect(
fromHoverMerged([
{
contents: { kind: MarkupKind.Markdown, value: 'x' },
alerts: [
{ summary: { kind: MarkupKind.PlainText, value: 'x' } },
{ summary: { kind: MarkupKind.PlainText, value: 'y' } },
],
},
])
).toEqual({
contents: [{ kind: MarkupKind.Markdown, value: 'x' }],
alerts: [
{ summary: { kind: MarkupKind.PlainText, value: 'x' } },
{ summary: { kind: MarkupKind.PlainText, value: 'y' } },
],
aggregatedBadges: [],
}))
@ -69,12 +35,10 @@ describe('HoverMerged', () => {
fromHoverMerged([
{
contents: { kind: MarkupKind.Markdown, value: 'x' },
alerts: [],
aggregableBadges: [{ text: 't01' }, { text: 't03' }],
},
{
contents: { kind: MarkupKind.Markdown, value: 'y' },
alerts: [],
aggregableBadges: [{ text: 't02' }],
},
])
@ -83,7 +47,6 @@ describe('HoverMerged', () => {
{ kind: MarkupKind.Markdown, value: 'x' },
{ kind: MarkupKind.Markdown, value: 'y' },
],
alerts: [],
aggregatedBadges: [{ text: 't01' }, { text: 't02' }, { text: 't03' }],
}))
})

View File

@ -1,4 +1,4 @@
import { Badged, Hover, MarkupContent, HoverAlert, AggregableBadge } from 'sourcegraph'
import { Badged, Hover, MarkupContent, AggregableBadge } from 'sourcegraph'
import { MarkupKind, Range } from '@sourcegraph/extension-api-classes'
import { Hover as PlainHover, Range as PlainRange } from '@sourcegraph/extension-api-types'
@ -6,7 +6,6 @@ import { Hover as PlainHover, Range as PlainRange } from '@sourcegraph/extension
/** A hover that is merged from multiple Hover results and normalized. */
export interface HoverMerged {
contents: Badged<MarkupContent>[]
alerts?: HoverAlert[]
range?: PlainRange
/** Sorted and de-duplicated set of badges in all source hover values. */
@ -14,11 +13,8 @@ export interface HoverMerged {
}
/** Create a merged hover from the given individual hovers. */
export function fromHoverMerged(
values: (Badged<Hover | (PlainHover & { alerts?: HoverAlert[] })> | null | undefined)[]
): HoverMerged | null {
export function fromHoverMerged(values: (Badged<Hover | PlainHover> | null | undefined)[]): HoverMerged | null {
const contents: HoverMerged['contents'] = []
const alerts: HoverMerged['alerts'] = []
const aggregatedBadges = new Map<string, AggregableBadge>()
let range: PlainRange | undefined
for (const result of values) {
@ -29,9 +25,6 @@ export function fromHoverMerged(
kind: result.contents.kind || MarkupKind.PlainText,
})
}
if ('alerts' in result && result.alerts) {
alerts.push(...result.alerts)
}
for (const badge of result.aggregableBadges || []) {
aggregatedBadges.set(badge.text, badge)
@ -54,7 +47,6 @@ export function fromHoverMerged(
return {
contents,
alerts,
...(range ? { range } : {}),
aggregatedBadges: [...aggregatedBadges.values()].sort((a, b) => a.text.localeCompare(b.text)),
}

View File

@ -7,7 +7,7 @@ import { Range } from './location'
*
* @see module:sourcegraph.Hover
*/
export interface Hover extends Pick<APIHover, 'contents' | 'alerts'> {
export interface Hover extends Pick<APIHover, 'contents'> {
/** The range that the hover applies to. */
readonly range?: Range
}

View File

@ -422,54 +422,6 @@ declare module 'sourcegraph' {
*/
export type DocumentSelector = (string | DocumentFilter)[]
/**
* Options for an input box displayed as a result of calling {@link Window#showInputBox}.
*/
export interface InputBoxOptions {
/**
* The text that describes what input the user should provide.
*/
prompt?: string
/**
* The pre-filled input value for the input box.
*/
value?: string
}
export interface ProgressOptions {
title?: string
}
export interface Progress {
/** Optional message. If not set, the previous message is still shown. */
message?: string
/** Integer from 0 to 100. If not set, the previous percentage is still shown. */
percentage?: number
}
export interface ProgressReporter {
/**
* Updates the progress display with a new message and/or percentage.
*/
next(status: Progress): void
/**
* Turns the progress display into an error display for the given error or message.
* Use if the operation failed.
* No further progress updates can be sent after this.
*/
error(error: any): void
/**
* Completes the progress bar and hides the display.
* Sending a percentage of 100 has the same effect.
* No further progress updates can be sent after this.
*/
complete(): void
}
/**
* A window in the client application that is running the extension.
*/
@ -488,52 +440,6 @@ declare module 'sourcegraph' {
* An event that is fired when the active view component changes.
*/
activeViewComponentChanges: Subscribable<ViewComponent | undefined>
/**
* Show a notification message to the user that does not require interaction or steal focus.
*
* @param message The message to show. Markdown is supported.
* @param type a {@link NotificationType} affecting the display of the notification.
*/
showNotification(message: string, type: NotificationType): void
/**
* Show progress in the window. Progress is shown while running the given callback
* and while the promise it returned isn't resolved nor rejected.
*
* @param task A callback returning a promise. Progress state can be reported with
* the provided [ProgressReporter](#ProgressReporter)-object.
*
* @returns The Promise the task-callback returned.
*/
withProgress<R>(options: ProgressOptions, task: (reporter: ProgressReporter) => Promise<R>): Promise<R>
/**
* Show progress in the window. The returned ProgressReporter can be used to update the
* progress bar, complete it or turn the notification into an error notification in case the operation failed.
*
* @returns A ProgressReporter that allows updating the progress display.
*/
showProgress(options: ProgressOptions): Promise<ProgressReporter>
/**
* Show a modal message to the user that the user must dismiss before continuing.
*
* @param message The message to show.
* @returns A promise that resolves when the user dismisses the message.
*/
showMessage(message: string): Promise<void>
/**
* Displays an input box to ask the user for input.
*
* The returned value will be `undefined` if the input box was canceled (e.g., because the user pressed the
* ESC key). Otherwise the returned value will be the string provided by the user.
*
* @param options Configures the behavior of the input box.
* @returns The string provided by the user, or `undefined` if the input box was canceled.
*/
showInputBox(options?: InputBoxOptions): Promise<string | undefined>
}
/**
@ -1090,32 +996,6 @@ declare module 'sourcegraph' {
kind?: MarkupKind
}
/**
* The type of a notification shown through {@link Window.showNotification}.
*/
export enum NotificationType {
/**
* An error message.
*/
Error = 1,
/**
* A warning message.
*/
Warning = 2,
/**
* An info message.
*/
Info = 3,
/**
* A log message.
*/
Log = 4,
/**
* A success message.
*/
Success = 5,
}
/** A badge holds the extra fields that can be attached to a providable type T via Badged<T>. */
export interface Badge {
/**
@ -1164,41 +1044,6 @@ declare module 'sourcegraph' {
* position or the current position itself.
*/
range?: Range
/**
* Alerts that should be shown in this hover.
*/
alerts?: HoverAlert[]
}
export interface HoverAlert {
/**
* Text content to be shown on hovers. Since the alert is displayed inline,
* multiparagraph content will be rendered on one line. It's recommended to
* provide a brief message here, and place futher details in the badge or
* provide a link.
*/
summary: MarkupContent
/**
* When an alert has a dismissal type, dismissing it will prevent all alerts
* of that type from being shown. If no type is provided, the alert is not
* dismissible.
*/
type?: string
/** Predefined icons to display next ot the summary. */
iconKind?: 'info' | 'error' | 'warning'
/**
* When set, this renders a row of button underneath the content. Note
* that this was added after the extension deprecation and will only
* work with newer clients.
*
* When buttons are rendered this way, an eventual dismiss button is
* appended to this list.
*/
buttons?: React.ReactNode[]
}
export interface HoverProvider {

View File

@ -202,10 +202,6 @@ ts_project(
"src/codeintel/legacy-extensions/util/uri.ts",
"src/codeintel/scip.ts",
"src/codeintel/searchContext.ts",
"src/commandPalette/CommandList.tsx",
"src/commandPalette/EmptyCommandList.tsx",
"src/commandPalette/EmptyCommandListContainer/EmptyCommandListContainer.tsx",
"src/commandPalette/EmptyCommandListContainer/index.ts",
"src/commands/commands.ts",
"src/components/CodeMirrorEditor.tsx",
"src/components/HighlightedMatches.tsx",
@ -236,8 +232,6 @@ ts_project(
"src/hover/HoverOverlay.fixtures.ts",
"src/hover/HoverOverlay.tsx",
"src/hover/HoverOverlay.types.ts",
"src/hover/HoverOverlayAlerts/HoverOverlayAlerts.tsx",
"src/hover/HoverOverlayAlerts/index.ts",
"src/hover/HoverOverlayContents/HoverOverlayContent/HoverOverlayContent.tsx",
"src/hover/HoverOverlayContents/HoverOverlayContent/index.ts",
"src/hover/HoverOverlayContents/HoverOverlayContents.tsx",
@ -251,9 +245,6 @@ ts_project(
"src/keyboardShortcuts/keyboardShortcuts.ts",
"src/keyboardShortcuts/useKeyboardShortcut.ts",
"src/languages.ts",
"src/notifications/NotificationItem.tsx",
"src/notifications/Notifications.tsx",
"src/notifications/notification.ts",
"src/platform/context.ts",
"src/polyfills/configure-core-js.ts",
"src/polyfills/index.ts",

View File

@ -62,7 +62,6 @@ export const CommandAction: Story = () => (
telemetryService={NOOP_TELEMETRY_SERVICE}
disabledDuringExecution={true}
showLoadingSpinnerDuringExecution={true}
showInlineError={true}
onDidExecute={onDidExecute}
/>
)
@ -105,7 +104,6 @@ export const Executing: Story = () => {
action={{ id: 'a', command: 'c', title: 'Hello', iconURL: ICON_URL }}
disabledDuringExecution={true}
showLoadingSpinnerDuringExecution={true}
showInlineError={true}
/>
)
}
@ -124,7 +122,6 @@ export const _Error: Story = () => {
action={{ id: 'a', command: 'c', title: 'Hello', iconURL: ICON_URL }}
disabledDuringExecution={true}
showLoadingSpinnerDuringExecution={true}
showInlineError={true}
/>
)
}

View File

@ -193,32 +193,6 @@ describe('ActionItem', () => {
expect(asFragment()).toMatchSnapshot()
})
test('run command with error with showInlineError', async () => {
const { asFragment } = render(
<ActionItem
active={true}
action={{ id: 'c', command: 'c', title: 't', description: 'd', iconURL: 'u', category: 'g' }}
telemetryService={NOOP_TELEMETRY_SERVICE}
variant="actionItem"
showInlineError={true}
location={history.location}
extensionsController={{
...NOOP_EXTENSIONS_CONTROLLER,
executeCommand: () => Promise.reject(new Error('x')),
}}
platformContext={NOOP_PLATFORM_CONTEXT}
/>
)
// Run command (which will reject with an error). (Use setTimeout to wait for the executeCommand resolution
// to result in the setState call.)
userEvent.click(screen.getByRole('button'))
await waitFor(() => expect(screen.getByLabelText('Error: x')).toBeInTheDocument())
expect(asFragment()).toMatchSnapshot()
})
describe('"open" command', () => {
it('renders as link', () => {
jsdom.reconfigure({ url: 'https://example.com/foo' })

View File

@ -7,7 +7,7 @@ import { from, Subject, Subscription } from 'rxjs'
import { catchError, map, mapTo, mergeMap, startWith, tap } from 'rxjs/operators'
import { ActionContribution, Evaluated } from '@sourcegraph/client-api'
import { asError, ErrorLike, isErrorLike, isExternalLink, logger } from '@sourcegraph/common'
import { asError, ErrorLike, isExternalLink, logger } from '@sourcegraph/common'
import {
LoadingSpinner,
Button,
@ -92,20 +92,6 @@ export interface ActionItemProps extends ActionItemAction, ActionItemComponentPr
*/
showLoadingSpinnerDuringExecution?: boolean
/**
* Whether to show the error (if any) from executing the command inline on this component and NOT in the global
* notifications UI component.
*
* This inline error display behavior is intended for actions that are scoped to a particular component. If the
* error were displayed in the global notifications UI component, it might not be clear which of the many
* possible scopes the error applies to.
*
* For example, the hover actions ("Go to definition", "Find references", etc.) use showInlineError == true
* because those actions are scoped to a specific token in a file. The command palette uses showInlineError ==
* false because it is a global UI component (and because showing tooltips on menu items would look strange).
*/
showInlineError?: boolean
/** Instead of showing the icon and/or title, show this element. */
title?: JSX.Element | null
@ -146,7 +132,7 @@ export class ActionItem extends React.PureComponent<ActionItemProps, State, type
mergeMap(parameters =>
from(
this.props.extensionsController
? this.props.extensionsController.executeCommand(parameters, this.props.showInlineError)
? this.props.extensionsController.executeCommand(parameters)
: Promise.reject(
new Error(
'ActionItems commands other than open and invokeFunction-new are deprecated'
@ -278,14 +264,9 @@ export class ActionItem extends React.PureComponent<ActionItemProps, State, type
tabIndex: this.props.tabIndex,
}
const tooltipOrErrorMessage =
this.props.showInlineError && isErrorLike(this.state.actionOrError)
? `Error: ${this.state.actionOrError.message}`
: tooltip
if (!to) {
return (
<Tooltip content={tooltipOrErrorMessage}>
<Tooltip content={tooltip}>
<Button
{...sharedProps}
{...buttonLinkProps}
@ -299,7 +280,7 @@ export class ActionItem extends React.PureComponent<ActionItemProps, State, type
onClick={this.runAction}
data-action-item-pressed={pressed}
aria-pressed={pressed}
aria-label={tooltipOrErrorMessage}
aria-label={tooltip}
>
{content}{' '}
{showLoadingSpinner && (
@ -313,7 +294,7 @@ export class ActionItem extends React.PureComponent<ActionItemProps, State, type
}
return (
<Tooltip content={tooltipOrErrorMessage}>
<Tooltip content={tooltip}>
<span>
<ButtonLink
data-content={this.props.dataContent}
@ -375,12 +356,7 @@ export class ActionItem extends React.PureComponent<ActionItemProps, State, type
const emitDidExecute = (): void => {
if (this.props.onDidExecute) {
// Defer calling onRun until after the URL has been opened. If we call it immediately, then in
// CommandList it immediately updates the (most-recent-first) ordering of the ActionItems, and
// the URL actually changes underneath us before the URL is opened. There is no harm to
// deferring this call; onRun's documentation allows this.
const onDidExecute = this.props.onDidExecute
setTimeout(() => onDidExecute(action.id))
this.props.onDidExecute(action.id)
}
}

View File

@ -177,22 +177,6 @@ exports[`ActionItem run command with error 1`] = `
</DocumentFragment>
`;
exports[`ActionItem run command with error with showInlineError 1`] = `
<DocumentFragment>
<button
aria-label="Error: x"
class="test-action-item"
type="button"
>
<img
alt="d"
src="u"
/>
g: t
</button>
</DocumentFragment>
`;
exports[`ActionItem run command with showLoadingSpinnerDuringExecution 1`] = `
<DocumentFragment>
<button

View File

@ -12,7 +12,7 @@ import { InitData } from '../extension/extensionHost'
import { registerComlinkTransferHandlers } from '../util'
import { ClientAPI } from './api/api'
import { ExposedToClient, initMainThreadAPI } from './mainthread-api'
import { initMainThreadAPI } from './mainthread-api'
export interface ExtensionHostClientConnection {
/**
@ -50,7 +50,6 @@ export async function createExtensionHostClientConnection(
subscription: Unsubscribable
api: comlink.Remote<FlatExtensionHostAPI>
mainThreadAPI: MainThreadAPI
exposedToClient: ExposedToClient
}> {
const subscription = new Subscription()
@ -71,7 +70,7 @@ export async function createExtensionHostClientConnection(
initialSettings: isSettingsValid(initialSettings) ? initialSettings : { final: {}, subjects: [] },
})
const { api: newAPI, exposedToClient, subscription: apiSubscriptions } = initMainThreadAPI(proxy, platformContext)
const { api: newAPI, subscription: apiSubscriptions } = initMainThreadAPI(proxy, platformContext)
subscription.add(apiSubscriptions)
@ -87,5 +86,5 @@ export async function createExtensionHostClientConnection(
// TODO(tj): return MainThreadAPI and add to Controller interface
// to allow app to interact with APIs whose state lives in the main thread
return { subscription, api: proxy, mainThreadAPI: newAPI, exposedToClient }
return { subscription, api: proxy, mainThreadAPI: newAPI }
}

View File

@ -1,16 +1,14 @@
import { Remote, proxy } from 'comlink'
import { Unsubscribable, Subscription, from, Observable, Subject, of } from 'rxjs'
import { Unsubscribable, Subscription, from, of } from 'rxjs'
import { publishReplay, refCount, switchMap } from 'rxjs/operators'
import { asError, logger } from '@sourcegraph/common'
import { logger } from '@sourcegraph/common'
import { InputBoxOptions } from '../../codeintel/legacy-extensions/api'
import { registerBuiltinClientCommands } from '../../commands/commands'
import { PlatformContext } from '../../platform/context'
import { isSettingsValid } from '../../settings/settings'
import { FlatExtensionHostAPI, MainThreadAPI } from '../contract'
import { proxySubscribable } from '../extension/api/common'
import { NotificationType, PlainNotification } from '../extension/extensionHostApi'
import { ProxySubscription } from './api/common'
import { updateSettings } from './services/settings'
@ -36,22 +34,13 @@ export interface ExecuteCommandParameters {
args?: any[]
}
function messageFromExtension(message: string): string {
return `From extension:\n\n${message}`
}
/**
* For state that needs to live in the main thread.
* Returned to Controller for access by client applications.
*/
export interface ExposedToClient {
registerCommand: (entryToRegister: CommandEntry) => Unsubscribable
executeCommand: (parameters: ExecuteCommandParameters, suppressNotificationOnError?: boolean) => Promise<any>
/**
* Observable of error notifications as a result of client applications executing commands.
*/
commandErrors: Observable<PlainNotification>
executeCommand: (parameters: ExecuteCommandParameters) => Promise<any>
}
export const initMainThreadAPI = (
@ -62,8 +51,6 @@ export const initMainThreadAPI = (
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'showMessage'
| 'showInputBox'
| 'getStaticExtensions'
| 'telemetryService'
| 'clientApplication'
@ -106,21 +93,9 @@ export const initMainThreadAPI = (
subscription.add(registerBuiltinClientCommands(platformContext, extensionHost, registerCommand))
const commandErrors = new Subject<PlainNotification>()
const exposedToClient: ExposedToClient = {
registerCommand,
executeCommand: (parameters, suppressNotificationOnError) =>
executeCommand(parameters).catch(error => {
if (!suppressNotificationOnError) {
commandErrors.next({
message: asError(error).message,
type: NotificationType.Error,
source: parameters.command,
})
}
return Promise.reject(error)
}),
commandErrors,
executeCommand,
}
const api: MainThreadAPI = {
@ -141,11 +116,6 @@ export const initMainThreadAPI = (
subscription.add(new ProxySubscription(run))
return proxy(subscription)
},
// User interaction methods
showMessage: message =>
platformContext.showMessage ? platformContext.showMessage(message) : defaultShowMessage(message),
showInputBox: options =>
platformContext.showInputBox ? platformContext.showInputBox(options) : defaultShowInputBox(options),
getEnabledExtensions: () => {
if (platformContext.getStaticExtensions) {
@ -168,17 +138,3 @@ export const initMainThreadAPI = (
return { api, exposedToClient, subscription }
}
function defaultShowMessage(message: string): Promise<void> {
return new Promise<void>(resolve => {
alert(messageFromExtension(message))
resolve()
})
}
function defaultShowInputBox(options?: InputBoxOptions): Promise<string | undefined> {
return new Promise<string | undefined>(resolve => {
const response = prompt(messageFromExtension(options?.prompt ?? ''), options?.value)
resolve(response ?? undefined)
})
}

View File

@ -8,21 +8,14 @@ import { DeepReplace } from '@sourcegraph/common'
import * as clientType from '@sourcegraph/extension-api-types'
import { GraphQLResult } from '@sourcegraph/http-client'
import type { ReferenceContext, InputBoxOptions } from '../codeintel/legacy-extensions/api'
import type { ReferenceContext } from '../codeintel/legacy-extensions/api'
import { ConfiguredExtension } from '../extensions/extension'
import { SettingsCascade } from '../settings/settings'
import { SettingsEdit } from './client/services/settings'
import { ExecutableExtension } from './extension/activation'
import { ProxySubscribable } from './extension/api/common'
import {
ViewContexts,
PanelViewData,
ViewProviderResult,
ProgressNotification,
PlainNotification,
ContributionOptions,
} from './extension/extensionHostApi'
import { ViewContexts, PanelViewData, ViewProviderResult, ContributionOptions } from './extension/extensionHostApi'
import { ExtensionViewer, TextDocumentData, ViewerData, ViewerId, ViewerUpdate } from './viewerTypes'
/**
@ -131,10 +124,6 @@ export interface FlatExtensionHostAPI {
*/
removeViewer(viewer: ViewerId): void
// Notifications
getPlainNotifications: () => ProxySubscribable<PlainNotification>
getProgressNotifications: () => ProxySubscribable<ProgressNotification & ProxyMarked>
// Views
getPanelViews: () => ProxySubscribable<PanelViewData[]>
@ -189,10 +178,6 @@ export interface MainThreadAPI {
command: Remote<((...args: any) => any) & ProxyMarked>
) => Unsubscribable & ProxyMarked
// User interaction methods
showMessage: (message: string) => Promise<void>
showInputBox: (options?: InputBoxOptions) => Promise<string | undefined>
getEnabledExtensions: () => ProxySubscribable<(ConfiguredExtension | ExecutableExtension)[]>
/**

View File

@ -15,7 +15,6 @@ describe('mergeContributions()', () => {
{ id: '1.b', command: 'c', title: '1.B' },
],
menus: {
[ContributableMenu.CommandPalette]: [{ action: '1.a' }],
[ContributableMenu.GlobalNav]: [{ action: '1.a' }, { action: '1.b' }],
},
}
@ -26,7 +25,6 @@ describe('mergeContributions()', () => {
{ id: '2.b', command: 'c', title: '2.B' },
],
menus: {
[ContributableMenu.CommandPalette]: [{ action: '2.a' }],
[ContributableMenu.EditorTitle]: [{ action: '2.a' }, { action: '2.b' }],
},
}
@ -38,7 +36,6 @@ describe('mergeContributions()', () => {
{ id: '2.b', command: 'c', title: '2.B' },
],
menus: {
[ContributableMenu.CommandPalette]: [{ action: '1.a' }, { action: '2.a' }],
[ContributableMenu.GlobalNav]: [{ action: '1.a' }, { action: '1.b' }],
[ContributableMenu.EditorTitle]: [{ action: '2.a' }, { action: '2.b' }],
},
@ -75,13 +72,6 @@ describe('filterContributions()', () => {
expect(filterContributions({ menus: {} })).toEqual(expected)
})
it('handles empty array of menu contributions', () => {
const expected: Evaluated<Contributions> = {
menus: { commandPalette: [] },
}
expect(filterContributions({ menus: { commandPalette: [] } })).toEqual(expected)
})
it('handles non-empty contributions', () => {
const expected: Evaluated<Contributions> = {
actions: [
@ -90,7 +80,6 @@ describe('filterContributions()', () => {
{ id: 'a3', command: 'c' },
],
menus: {
[ContributableMenu.CommandPalette]: [{ action: 'a1', when: true }, { action: 'a3' }],
[ContributableMenu.GlobalNav]: [{ action: 'a1', when: true }, { action: 'a2' }],
},
}
@ -102,11 +91,6 @@ describe('filterContributions()', () => {
{ id: 'a3', command: 'c' },
],
menus: {
[ContributableMenu.CommandPalette]: [
{ action: 'a1', when: true },
{ action: 'a2', when: false },
{ action: 'a3' },
],
[ContributableMenu.GlobalNav]: [
{ action: 'a1', when: true },
{ action: 'a2' },

View File

@ -47,7 +47,6 @@ export function fromHover(hover: sourcegraph.Badged<sourcegraph.Hover>): sourceg
return {
contents: hover.contents,
range: fromRange(hover.range),
alerts: hover.alerts,
aggregableBadges: hover.aggregableBadges,
}
}

View File

@ -1,19 +1,18 @@
import { proxy, Remote } from 'comlink'
import { noop, sortBy } from 'lodash'
import { BehaviorSubject, EMPTY, ReplaySubject, Unsubscribable } from 'rxjs'
import { BehaviorSubject, EMPTY, Unsubscribable } from 'rxjs'
import { debounceTime, mapTo } from 'rxjs/operators'
import * as sourcegraph from 'sourcegraph'
import { asError, logger } from '@sourcegraph/common'
import { logger } from '@sourcegraph/common'
import { Location, MarkupKind, Position, Range, Selection } from '@sourcegraph/extension-api-classes'
import { ClientAPI } from '../client/api/api'
import { syncRemoteSubscription } from '../util'
import { proxySubscribable } from './api/common'
import { DocumentHighlightKind } from './api/documentHighlights'
import { InitData, updateContext } from './extensionHost'
import { NotificationType, PanelViewData } from './extensionHostApi'
import { PanelViewData } from './extensionHostApi'
import { ExtensionHostState } from './extensionHostState'
import { addWithRollback } from './util'
@ -88,47 +87,6 @@ export function createExtensionAPIFactory(
searchContextChanges: state.searchContextChanges.asObservable(),
}
const createProgressReporter = async (
options: sourcegraph.ProgressOptions
// `showProgress` returned a promise when progress reporters were created
// in the main thread. continue to return promise for backward compatibility
// eslint-disable-next-line @typescript-eslint/require-await
): Promise<sourcegraph.ProgressReporter> => {
// There's no guarantee that UI consumers have subscribed to the progress observable
// by the time that an extension reports progress, so replay the latest report on subscription.
const progressSubject = new ReplaySubject<sourcegraph.Progress>(1)
// progress notifications have to be proxied since the observable
// `progress` property cannot be cloned
state.progressNotifications.next(
proxy({
baseNotification: {
message: options.title,
type: NotificationType.Log,
},
progress: proxySubscribable(progressSubject.asObservable()),
})
)
// return ProgressReporter, which exposes a subset of Subject methods to extensions
return {
next: (progress: sourcegraph.Progress) => {
progressSubject.next(progress)
},
error: (value: any) => {
const error = asError(value)
progressSubject.error({
message: error.message,
name: error.name,
stack: error.stack,
})
},
complete: () => {
progressSubject.complete()
},
}
}
// App
const window: sourcegraph.Window = {
get visibleViewComponents(): sourcegraph.ViewComponent[] {
@ -139,24 +97,6 @@ export function createExtensionAPIFactory(
return state.activeViewComponentChanges.value
},
activeViewComponentChanges: state.activeViewComponentChanges.asObservable(),
showNotification: (message, type) => {
state.plainNotifications.next({ message, type })
},
withProgress: async (options, task) => {
const reporter = await createProgressReporter(options)
try {
const result = task(reporter)
reporter.complete()
return await result
} catch (error) {
reporter.error(error)
throw error
}
},
showProgress: options => createProgressReporter(options),
showMessage: message => clientAPI.showMessage(message),
showInputBox: options => clientAPI.showInputBox(options),
}
const app: typeof sourcegraph['app'] = {
@ -305,7 +245,6 @@ export function createExtensionAPIFactory(
Selection,
Location,
MarkupKind,
NotificationType,
DocumentHighlightKind,
app: {
...app,

View File

@ -28,8 +28,6 @@ import { Context } from '@sourcegraph/template-parser'
import type {
ReferenceContext,
DocumentSelector,
NotificationType as LegacyNotificationType,
Progress,
DirectoryViewContext,
View,
PanelView,
@ -41,7 +39,7 @@ import { FlatExtensionHostAPI } from '../contract'
import { ExtensionViewer, ViewerId, ViewerWithPartialModel } from '../viewerTypes'
import { ExtensionCodeEditor } from './api/codeEditor'
import { providerResultToObservable, ProxySubscribable, proxySubscribable } from './api/common'
import { providerResultToObservable, proxySubscribable } from './api/common'
import { computeContext, ContributionScope } from './api/context/context'
import {
evaluateContributions,
@ -370,10 +368,6 @@ export function createExtensionHostAPI(state: ExtensionHostState): FlatExtension
)
),
// Notifications
getPlainNotifications: () => proxySubscribable(state.plainNotifications.asObservable()),
getProgressNotifications: () => proxySubscribable(state.progressNotifications.asObservable()),
// Views
getPanelViews: () =>
// Don't need `combineLatestOrDefault` here since each panel view
@ -647,40 +641,6 @@ export interface PanelViewData extends Omit<PanelView, 'unsubscribe'> {
id: string
}
/**
* A notification message to display to the user.
*/
export type ExtensionNotification = PlainNotification | ProgressNotification
interface BaseNotification {
/** The message of the notification. */
message?: string
/**
* The type of the message.
*/
type: LegacyNotificationType
/** The source of the notification. */
source?: string
}
export interface PlainNotification extends BaseNotification {}
export interface ProgressNotification {
// Put all base notification properties in a nested object because
// ProgressNotifications are proxied, so it's better to clone this
// notification object than to wait for all property access promises
// to resolve
baseNotification: BaseNotification
/**
* Progress updates to show in this notification (progress bar and status messages).
* If this Observable errors, the notification will be changed to an error type.
*/
progress: ProxySubscribable<Progress>
}
export interface ViewProviderResult {
/** The ID of the view provider. */
id: string
@ -689,19 +649,6 @@ export interface ViewProviderResult {
view: View | undefined | ErrorLike
}
/**
* The type of a notification.
* This is needed because if sourcegraph.NotificationType enum values are referenced,
* the `sourcegraph` module import at the top of the file is emitted in the generated code.
*/
export const NotificationType: typeof LegacyNotificationType = {
Error: 1,
Warning: 2,
Info: 3,
Log: 4,
Success: 5,
}
// Contributions
export interface ContributionOptions<T = unknown> {

View File

@ -1,5 +1,5 @@
import * as comlink from 'comlink'
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs'
import { BehaviorSubject, Observable, of, Subject } from 'rxjs'
import * as sourcegraph from 'sourcegraph'
import { Contributions } from '@sourcegraph/client-api'
@ -15,13 +15,7 @@ import { ExtensionCodeEditor } from './api/codeEditor'
import { ExtensionDocument } from './api/textDocument'
import { ExtensionWorkspaceRoot } from './api/workspaceRoot'
import { InitData } from './extensionHost'
import {
RegisteredProvider,
RegisteredViewProvider,
PanelViewData,
PlainNotification,
ProgressNotification,
} from './extensionHostApi'
import { RegisteredProvider, RegisteredViewProvider, PanelViewData } from './extensionHostApi'
import { ReferenceCounter } from './utils/ReferenceCounter'
export function createExtensionHostState(
@ -84,13 +78,6 @@ export function createExtensionHostState(
activeViewComponentChanges: new BehaviorSubject<ExtensionViewer | undefined>(undefined),
viewerUpdates: new Subject<ViewerUpdate>(),
// Use ReplaySubject so we don't lose notifications in case the client application subscribes
// to notification streams after extensions have already sent notifications.
// There should be no issue re: stale notifications, since client applications should only
// create one "notification manager" instance.
plainNotifications: new ReplaySubject<PlainNotification>(3),
progressNotifications: new ReplaySubject<ProgressNotification & comlink.ProxyMarked>(3),
panelViews: new BehaviorSubject<readonly Observable<PanelViewData>[]>([]),
insightsPageViewProviders: new BehaviorSubject<readonly RegisteredViewProvider<'insightsPage'>[]>([]),
homepageViewProviders: new BehaviorSubject<readonly RegisteredViewProvider<'homepage'>[]>([]),
@ -140,9 +127,6 @@ export interface ExtensionHostState {
activeViewComponentChanges: BehaviorSubject<ExtensionViewer | undefined>
viewerUpdates: Subject<ViewerUpdate>
plainNotifications: ReplaySubject<PlainNotification>
progressNotifications: ReplaySubject<ProgressNotification & comlink.ProxyMarked>
// Views
panelViews: BehaviorSubject<readonly Observable<PanelViewData>[]>
insightsPageViewProviders: BehaviorSubject<readonly RegisteredViewProvider<'insightsPage'>[]>

View File

@ -68,7 +68,7 @@ describe('getHover from ExtensionHost API, it aims to have more e2e feel', () =>
{ isLoading: true, result: null },
{
isLoading: false,
result: { contents: [textHover('a1').contents], alerts: [], aggregatedBadges: [] },
result: { contents: [textHover('a1').contents], aggregatedBadges: [] },
},
])
results = []
@ -81,13 +81,12 @@ describe('getHover from ExtensionHost API, it aims to have more e2e feel', () =>
expect(results).toEqual<MaybeLoadingResult<HoverMerged | null>[]>([
{
isLoading: true,
result: { contents: [textHover('a2').contents], alerts: [], aggregatedBadges: [] },
result: { contents: [textHover('a2').contents], aggregatedBadges: [] },
},
{
isLoading: false,
result: {
contents: ['a2', 'b'].map(value => textHover(value).contents),
alerts: [],
aggregatedBadges: [],
},
},
@ -101,7 +100,7 @@ describe('getHover from ExtensionHost API, it aims to have more e2e feel', () =>
{ isLoading: true, result: null },
{
isLoading: false,
result: { contents: [textHover('a3').contents], alerts: [], aggregatedBadges: [] },
result: { contents: [textHover('a3').contents], aggregatedBadges: [] },
},
])
})

View File

@ -195,13 +195,13 @@ describe('mergeProviderResults()', () => {
it('merges a Hover into result', () => {
const hover: Hover = { contents: { value: 'a', kind: MarkupKind.PlainText } }
const merged: HoverMerged = { contents: [hover.contents], alerts: [], aggregatedBadges: [] }
const merged: HoverMerged = { contents: [hover.contents], aggregatedBadges: [] }
expect(mergeHoverResults([hover])).toEqual(merged)
})
it('omits non Hover values from hovers result', () => {
const hover: Hover = { contents: { value: 'a', kind: MarkupKind.PlainText } }
const merged: HoverMerged = { contents: [hover.contents], alerts: [], aggregatedBadges: [] }
const merged: HoverMerged = { contents: [hover.contents], aggregatedBadges: [] }
expect(mergeHoverResults([hover, null, LOADING, undefined])).toEqual(merged)
})
@ -222,7 +222,6 @@ describe('mergeProviderResults()', () => {
{ value: 'c2', kind: MarkupKind.PlainText },
],
range: { start: { line: 1, character: 2 }, end: { line: 3, character: 4 } },
alerts: [],
aggregatedBadges: [],
}

View File

@ -24,7 +24,6 @@ describe('LanguageFeatures (integration)', () => {
}),
labeledProviderResults: labels => ({
contents: labels.map(label => ({ value: label, kind: MarkupKind.PlainText })),
alerts: [],
aggregatedBadges: [],
}),
providerWithImplementation: run => ({ provideHover: run } as sourcegraph.HoverProvider),

View File

@ -1,11 +1,9 @@
import { pick } from 'lodash'
import { from, of } from 'rxjs'
import { switchMap, take, toArray, first } from 'rxjs/operators'
import { switchMap, take, toArray } from 'rxjs/operators'
import type { ViewComponent, Window } from 'sourcegraph'
import { assertToJSON, integrationTestContext } from '../../testing/testHelpers'
import { wrapRemoteObservable } from '../client/api/common'
import { NotificationType } from '../extension/extensionHostApi'
import { TextDocumentData } from '../viewerTypes'
describe('Windows (integration)', () => {
@ -191,35 +189,5 @@ describe('Windows (integration)', () => {
)
})
})
test('Window#showNotification', async () => {
const { extensionAPI, extensionHostAPI } = await integrationTestContext()
const value = wrapRemoteObservable(extensionHostAPI.getPlainNotifications()).pipe(first()).toPromise()
extensionAPI.app.activeWindow!.showNotification('a', NotificationType.Info)
expect(await value).toEqual({ message: 'a', type: NotificationType.Info })
})
test('Window#showMessage', async () => {
const showMessageRequests: string[] = []
const { extensionAPI } = await integrationTestContext({
showMessage: message => {
showMessageRequests.push(message)
return Promise.resolve()
},
})
expect(await extensionAPI.app.activeWindow!.showMessage('a')).toBe(undefined)
expect(showMessageRequests).toEqual(['a'])
})
test('Window#showInputBox', async () => {
const { extensionAPI } = await integrationTestContext({
showInputBox: options => Promise.resolve('default value: ' + (options?.value || '')),
})
expect(await extensionAPI.app.activeWindow!.showInputBox({ prompt: 'a', value: 'b' })).toBe(
'default value: b'
)
})
})
})

View File

@ -1,4 +1,3 @@
/* eslint-disable etc/no-deprecated */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Observable, Unsubscribable } from 'rxjs'
@ -79,54 +78,6 @@ export interface DocumentFilter {
*/
export type DocumentSelector = (string | DocumentFilter)[]
/**
* Options for an input box displayed as a result of calling {@link Window#showInputBox}.
*/
export interface InputBoxOptions {
/**
* The text that describes what input the user should provide.
*/
prompt?: string
/**
* The pre-filled input value for the input box.
*/
value?: string
}
export interface ProgressOptions {
title?: string
}
export interface Progress {
/** Optional message. If not set, the previous message is still shown. */
message?: string
/** Integer from 0 to 100. If not set, the previous percentage is still shown. */
percentage?: number
}
export interface ProgressReporter {
/**
* Updates the progress display with a new message and/or percentage.
*/
next(status: Progress): void
/**
* Turns the progress display into an error display for the given error or message.
* Use if the operation failed.
* No further progress updates can be sent after this.
*/
error(error: any): void
/**
* Completes the progress bar and hides the display.
* Sending a percentage of 100 has the same effect.
* No further progress updates can be sent after this.
*/
complete(): void
}
export interface Directory {
/**
* The URI of the directory.
@ -472,32 +423,6 @@ export interface MarkupContent {
kind?: MarkupKind
}
/**
* The type of a notification shown through {@link Window.showNotification}.
*/
export enum NotificationType {
/**
* An error message.
*/
Error = 1,
/**
* A warning message.
*/
Warning = 2,
/**
* An info message.
*/
Info = 3,
/**
* A log message.
*/
Log = 4,
/**
* A success message.
*/
Success = 5,
}
/** A badge holds the extra fields that can be attached to a providable type T via Badged<T>. */
export interface Badge {
/**
@ -546,41 +471,6 @@ export interface Hover {
* position or the current position itself.
*/
range?: Range
/**
* Alerts that should be shown in this hover.
*/
alerts?: HoverAlert[]
}
export interface HoverAlert {
/**
* Text content to be shown on hovers. Since the alert is displayed inline,
* multiparagraph content will be rendered on one line. It's recommended to
* provide a brief message here, and place futher details in the badge or
* provide a link.
*/
summary: MarkupContent
/**
* When an alert has a dismissal type, dismissing it will prevent all alerts
* of that type from being shown. If no type is provided, the alert is not
* dismissible.
*/
type?: string
/** Predefined icons to display next ot the summary. */
iconKind?: 'info' | 'error' | 'warning'
/**
* When set, this renders a row of button underneath the content. Note
* that this was added after the extension deprecation and will only
* work with newer clients.
*
* When buttons are rendered this way, an eventual dismiss button is
* appended to this list.
*/
buttons?: React.ReactNode[]
}
export interface HoverProvider {
@ -725,12 +615,10 @@ export function requestGraphQL<T>(query: string, vars?: { [name: string]: unknow
)
)
}
return (
context
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
.requestGraphQL<T, any>({ request: query, variables: vars as any, mightContainPrivateInfo: true })
.toPromise()
)
return context
.requestGraphQL<T, any>({ request: query, variables: vars as any, mightContainPrivateInfo: true })
.toPromise()
}
export function getSetting<T>(key: string): T | undefined {

View File

@ -1,32 +0,0 @@
.command-list {
width: 24rem;
:global(.list-group) {
max-height: 18.5rem;
overflow: auto;
}
:global(.list-group-item) {
&.active {
&:hover {
color: var(--body-color);
}
}
}
:global(.list-group-item-action) {
&:hover {
background-color: var(--color-bg-3);
}
}
}
@media (max-width: 24rem) {
.command-list {
width: 100vw;
}
}
.popover-button {
cursor: pointer;
}

View File

@ -1,67 +0,0 @@
import { ActionItemAction } from '../actions/ActionItem'
import { filterAndRankItems } from './CommandList'
describe('filterAndRankItems', () => {
function actionIDs(items: ActionItemAction[]): string[] {
return items.map(({ action: { id } }) => id)
}
test('no query, no recentActions', () =>
expect(
actionIDs(
filterAndRankItems(
[
{ action: { id: 'a', command: 'a' }, active: true },
{ action: { id: 'b', command: 'b' }, active: true },
],
'',
null
)
)
).toEqual(['a', 'b']))
test('query, no recentActions', () =>
expect(
actionIDs(
filterAndRankItems(
[
{ action: { id: 'a', command: 'a', title: 'a' }, active: true },
{ action: { id: 'b1', command: 'b1', title: 'b' }, active: true },
{ action: { id: 'b2', command: 'b2', title: '22b' }, active: true },
],
'b',
null
)
)
).toEqual(['b1', 'b2']))
test('no query, recentActions', () =>
expect(
actionIDs(
filterAndRankItems(
[
{ action: { id: 'a', command: 'a' }, active: true },
{ action: { id: 'b', command: 'b' }, active: true },
],
'',
['b']
)
)
).toEqual(['b', 'a']))
test('query, recentActions', () =>
expect(
actionIDs(
filterAndRankItems(
[
{ action: { id: 'a', command: 'a', title: 'a' }, active: true },
{ action: { id: 'b1', command: 'b1', title: 'b' }, active: true },
{ action: { id: 'b2', command: 'b2', title: '2b' }, active: true },
],
'b',
['b2']
)
)
).toEqual(['b2', 'b1']))
})

View File

@ -1,454 +0,0 @@
import React, { forwardRef, useCallback, useMemo, useState } from 'react'
import { mdiChevronDown, mdiChevronUp, mdiConsole, mdiPuzzle } from '@mdi/js'
import classNames from 'classnames'
import { Remote } from 'comlink'
import * as H from 'history'
import { sortBy, uniq } from 'lodash'
import { from, Subscription } from 'rxjs'
import { filter, switchMap } from 'rxjs/operators'
import stringScore from 'string-score'
import { Key } from 'ts-key-enum'
import { ContributableMenu, Contributions, Evaluated } from '@sourcegraph/client-api'
import { memoizeObservable, logger } from '@sourcegraph/common'
import {
ButtonProps,
ForwardReferenceComponent,
Icon,
Input,
Label,
LoadingSpinner,
Popover,
PopoverContent,
PopoverTrigger,
Position,
} from '@sourcegraph/wildcard'
import { ActionItem, ActionItemAction } from '../actions/ActionItem'
import { wrapRemoteObservable } from '../api/client/api/common'
import { FlatExtensionHostAPI } from '../api/contract'
import { haveInitialExtensionsLoaded } from '../api/features'
import { HighlightedMatches } from '../components/HighlightedMatches'
import { getContributedActionItems } from '../contributions/contributions'
import { RequiredExtensionsControllerProps } from '../extensions/controller'
import { KeyboardShortcut } from '../keyboardShortcuts'
import { PlatformContextProps } from '../platform/context'
import { Shortcut } from '../react-shortcuts'
import { SettingsCascadeOrError } from '../settings/settings'
import { TelemetryProps } from '../telemetry/telemetryService'
import { EmptyCommandList } from './EmptyCommandList'
import { EmptyCommandListContainer } from './EmptyCommandListContainer'
import styles from './CommandList.module.scss'
/**
* Customizable CSS classes for elements of the command list button.
*/
export interface CommandListPopoverButtonClassProps {
/** The class name for the root button element of {@link CommandListPopoverButton}. */
buttonClassName?: string
buttonElement?: 'span' | 'a' | 'button'
buttonOpenClassName?: string
popoverClassName?: string
showCaret?: boolean
}
/**
* Customizable CSS classes for elements of the command list.
*/
export interface CommandListClassProps {
inputClassName?: string
formClassName?: string
listItemClassName?: string
selectedListItemClassName?: string
selectedActionItemClassName?: string
listClassName?: string
resultsContainerClassName?: string
actionItemClassName?: string
noResultsClassName?: string
iconClassName?: string
}
export interface CommandListProps
extends CommandListClassProps,
RequiredExtensionsControllerProps<'executeCommand' | 'extHostAPI'>,
PlatformContextProps<'settings' | 'sourcegraphURL'>,
TelemetryProps {
/** The menu whose commands to display. */
menu: ContributableMenu
/** Called when the user has selected an item in the list. */
onSelect?: () => void
location: H.Location
}
interface State {
/** The contributions, merged from all extensions, or undefined before the initial emission. */
contributions?: Evaluated<Contributions>
input: string
selectedIndex: number
/** Recently invoked actions, which should be sorted first in the list. */
recentActions: string[] | null
settingsCascade?: SettingsCascadeOrError
}
// Memoize contributions to prevent flashing loading spinners on subsequent mounts
const getContributions = memoizeObservable(
(extensionHostAPI: Promise<Remote<FlatExtensionHostAPI>>) =>
from(extensionHostAPI).pipe(switchMap(extensionHost => wrapRemoteObservable(extensionHost.getContributions()))),
() => 'getContributions' // only one instance
)
/** Displays a list of commands contributed by extensions for a specific menu. */
export class CommandList extends React.PureComponent<CommandListProps, State> {
// Persist recent actions in localStorage. Be robust to serialization errors.
private static RECENT_ACTIONS_STORAGE_KEY = 'commandList.recentActions'
private static readRecentActions(): string[] | null {
const value = localStorage.getItem(CommandList.RECENT_ACTIONS_STORAGE_KEY)
if (value === null) {
return null
}
try {
const recentActions: unknown = JSON.parse(value)
if (Array.isArray(recentActions) && recentActions.every(a => typeof a === 'string')) {
return recentActions as string[]
}
return null
} catch (error) {
logger.error('Error reading recent actions:', error)
}
CommandList.writeRecentActions(null)
return null
}
private static writeRecentActions(recentActions: string[] | null): void {
try {
if (recentActions === null) {
localStorage.removeItem(CommandList.RECENT_ACTIONS_STORAGE_KEY)
} else {
const value = JSON.stringify(recentActions)
localStorage.setItem(CommandList.RECENT_ACTIONS_STORAGE_KEY, value)
}
} catch (error) {
logger.error('Error writing recent actions:', error)
}
}
public state: State = {
input: '',
selectedIndex: 0,
recentActions: CommandList.readRecentActions(),
}
private subscriptions = new Subscription()
private selectedItem: ActionItem | null = null
private setSelectedItem = (actionItem: ActionItem | null): void => {
this.selectedItem = actionItem
}
public componentDidMount(): void {
this.subscriptions.add(
// Don't listen for subscriptions until all initial extensions have loaded (to prevent UI jitter)
haveInitialExtensionsLoaded(this.props.extensionsController.extHostAPI)
.pipe(
filter(haveLoaded => haveLoaded),
switchMap(() => getContributions(this.props.extensionsController.extHostAPI))
)
.subscribe(contributions => {
this.setState({ contributions })
})
)
this.subscriptions.add(
this.props.platformContext.settings.subscribe(settingsCascade => this.setState({ settingsCascade }))
)
}
public componentDidUpdate(_previousProps: CommandListProps, previousState: State): void {
if (this.state.recentActions !== previousState.recentActions) {
CommandList.writeRecentActions(this.state.recentActions)
}
}
public componentWillUnmount(): void {
this.subscriptions.unsubscribe()
}
public render(): JSX.Element | null {
if (!this.state.contributions) {
return (
<EmptyCommandListContainer className={styles.commandList}>
<div className="d-flex py-5 align-items-center justify-content-center">
<LoadingSpinner inline={false} />
<span className="mx-2">Loading Sourcegraph extensions</span>
<Icon aria-hidden={true} svgPath={mdiPuzzle} />
</div>
</EmptyCommandListContainer>
)
}
const allItems = getContributedActionItems(this.state.contributions, this.props.menu)
// Filter and sort by score.
const query = this.state.input.trim()
const items = filterAndRankItems(allItems, this.state.input, this.state.recentActions)
// Support wrapping around.
const selectedIndex = ((this.state.selectedIndex % items.length) + items.length) % items.length
return (
<div className={styles.commandList}>
<header>
{/* eslint-disable-next-line react/forbid-elements */}
<form className={this.props.formClassName} onSubmit={this.onSubmit}>
<Label className="sr-only" htmlFor="command-list-input">
Command
</Label>
<Input
id="command-list-input"
inputClassName={this.props.inputClassName}
value={this.state.input}
placeholder="Run Sourcegraph action..."
spellCheck={false}
autoFocus={true}
autoCorrect="off"
autoComplete="off"
onChange={this.onInputChange}
onKeyDown={this.onInputKeyDown}
onClick={this.onInputClick}
/>
</form>
</header>
<div className={this.props.resultsContainerClassName}>
<ul className={this.props.listClassName}>
{items.length > 0 ? (
items.map((item, index) => (
<li
className={classNames(
this.props.listItemClassName,
index === selectedIndex && this.props.selectedListItemClassName
)}
key={item.action.id}
>
<ActionItem
{...this.props}
className={classNames(
this.props.actionItemClassName,
index === selectedIndex && this.props.selectedActionItemClassName
)}
{...item}
ref={index === selectedIndex ? this.setSelectedItem : undefined}
title={
<HighlightedMatches
text={[item.action.category, item.action.title || item.action.command]
.filter(Boolean)
.join(': ')}
pattern={query}
/>
}
onDidExecute={this.onActionDidExecute}
/>
</li>
))
) : query.length > 0 ? (
<li className={this.props.noResultsClassName}>No matching commands</li>
) : (
<EmptyCommandList settingsCascade={this.state.settingsCascade} />
)}
</ul>
</div>
</div>
)
}
private onInputChange: React.ChangeEventHandler<HTMLInputElement> = event => {
this.setState({ input: event.currentTarget.value, selectedIndex: 0 })
}
private onInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = event => {
switch (event.key) {
case Key.ArrowDown: {
event.preventDefault()
this.setSelectedIndex(1)
break
}
case Key.ArrowUp: {
event.preventDefault()
this.setSelectedIndex(-1)
break
}
case Key.Enter: {
if (this.selectedItem) {
this.selectedItem.runAction(event)
}
break
}
}
}
// prevent input click from closing the popover
private onInputClick: React.MouseEventHandler<HTMLInputElement> = event => {
event.stopPropagation()
}
private onSubmit: React.FormEventHandler = event => event.preventDefault()
private setSelectedIndex(delta: number): void {
this.setState(previousState => ({ selectedIndex: previousState.selectedIndex + delta }))
}
private onActionDidExecute = (actionID: string): void => {
const KEEP_RECENT_ACTIONS = 10
this.setState(previousState => {
const { recentActions } = previousState
if (!recentActions) {
return { recentActions: [actionID] }
}
return { recentActions: uniq([actionID, ...recentActions]).slice(0, KEEP_RECENT_ACTIONS) }
})
if (this.props.onSelect) {
this.props.onSelect()
}
}
}
export function filterAndRankItems(
items: Pick<ActionItemAction, 'action' | 'active'>[],
query: string,
recentActions: string[] | null
): ActionItemAction[] {
if (!query) {
if (recentActions === null) {
return items
}
// Show recent actions first.
return sortBy(
items,
(item: Pick<ActionItemAction, 'action'>): number | null => {
const index = recentActions.indexOf(item.action.id)
return index === -1 ? null : index
},
({ action }) => action.id
)
}
// Memoize labels and scores.
const labels = new Array<string>(items.length)
const scores = new Array<number>(items.length)
const scoredItems = items
.filter((item, index) => {
let label = labels[index]
if (label === undefined) {
label = `${item.action.category ? `${item.action.category}: ` : ''}${
item.action.title || item.action.command || ''
}`
labels[index] = label
}
if (scores[index] === undefined) {
scores[index] = stringScore(label, query, 0)
}
return scores[index] > 0
})
.map((item, index) => {
const recentIndex = recentActions?.indexOf(item.action.id)
return { item, score: scores[index], recentIndex: recentIndex === -1 ? null : recentIndex }
})
return sortBy(scoredItems, 'recentIndex', 'score', ({ item }) => item.action.id).map(({ item }) => item)
}
export interface CommandListPopoverButtonProps
extends CommandListProps,
CommandListPopoverButtonClassProps,
CommandListClassProps,
Pick<ButtonProps, 'variant'> {
keyboardShortcutForShow?: KeyboardShortcut
}
export const CommandListPopoverButton = forwardRef((props, ref) => {
const {
as: Component = 'span',
buttonElement,
buttonClassName,
buttonOpenClassName,
popoverClassName,
showCaret = true,
keyboardShortcutForShow,
variant,
} = props
const [isOpen, setIsOpen] = useState(false)
// Capture active element on open in order to restore focus on close.
const originallyFocusedElement = useMemo(() => {
if (isOpen && document.activeElement instanceof HTMLElement) {
return document.activeElement
}
return null
}, [isOpen])
const close = useCallback(() => {
originallyFocusedElement?.focus()
setIsOpen(false)
}, [originallyFocusedElement])
const toggleIsOpen = useCallback(() => {
if (isOpen) {
originallyFocusedElement?.focus()
}
setIsOpen(!isOpen)
}, [isOpen, originallyFocusedElement])
return (
<Popover isOpen={isOpen} onOpenChange={toggleIsOpen}>
<PopoverTrigger
ref={ref}
// Support legacy buttonElement prop since it's used in the different code hosts
// specifications
as={(buttonElement as 'button') ?? Component}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
variant={variant}
aria-label="Command list"
className={classNames(styles.popoverButton, buttonClassName, isOpen && buttonOpenClassName)}
onClick={toggleIsOpen}
>
<Icon size="md" aria-hidden={true} svgPath={mdiConsole} />
{showCaret && <Icon svgPath={isOpen ? mdiChevronUp : mdiChevronDown} aria-hidden={true} />}
</PopoverTrigger>
<PopoverContent className={popoverClassName} position={Position.bottomEnd}>
<CommandList
inputClassName={props.inputClassName}
formClassName={props.formClassName}
listItemClassName={props.listItemClassName}
selectedListItemClassName={props.selectedListItemClassName}
selectedActionItemClassName={props.selectedActionItemClassName}
listClassName={props.listClassName}
resultsContainerClassName={props.resultsContainerClassName}
actionItemClassName={props.actionItemClassName}
noResultsClassName={props.noResultsClassName}
iconClassName={props.iconClassName}
menu={props.menu}
platformContext={props.platformContext}
extensionsController={props.extensionsController}
location={props.location}
telemetryService={props.telemetryService}
onSelect={close}
/>
{keyboardShortcutForShow?.keybindings.map((keybinding, index) => (
<Shortcut key={index} {...keybinding} onMatch={toggleIsOpen} />
))}
</PopoverContent>
</Popover>
)
}) as ForwardReferenceComponent<'button', CommandListPopoverButtonProps>

View File

@ -1,15 +0,0 @@
.title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.text {
line-height: 1.5;
margin-bottom: 2rem;
}
.illustration {
position: absolute;
bottom: 1rem;
right: 1rem;
}

View File

@ -1,73 +0,0 @@
import React from 'react'
import { Text } from '@sourcegraph/wildcard'
import { onlyDefaultExtensionsAdded } from '../extensions/extensions'
import { SettingsCascadeOrError } from '../settings/settings'
import { EmptyCommandListContainer } from './EmptyCommandListContainer'
import styles from './EmptyCommandList.module.scss'
interface Props {
settingsCascade?: SettingsCascadeOrError
}
export const EmptyCommandList: React.FunctionComponent<React.PropsWithChildren<Props>> = ({ settingsCascade }) => {
// if no settings cascade, default to 'no active extensions'
const onlyDefault = settingsCascade ? onlyDefaultExtensionsAdded(settingsCascade) : false
return (
<EmptyCommandListContainer>
<Text className={styles.title}>
{onlyDefault ? "You don't have any extensions enabled" : "You don't have any active actions"}
</Text>
<Text className={styles.text}>
{onlyDefault
? 'Enable Sourcegraph extensions to get additional functionality, integrations, and make special actions available from this menu.'
: 'Commands from your installed extensions will be shown when you navigate to certain pages.'}
</Text>
<PuzzleIllustration className={styles.illustration} />
</EmptyCommandListContainer>
)
}
const PuzzleIllustration = React.memo<{ className?: string }>(({ className }) => (
<svg
width="59"
height="64"
viewBox="0 0 59 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M51.5825 43.9313C55.4271 45.582 57.2515 40.6516 57.8679 40.6516C58.4844 40.6516 58.9775 41.1397 58.9775 41.7497V60.3841C58.9775 62.3806 57.3411 64 55.3237 64H37.9287C37.7942 63.9778 37.6934 63.9556 37.6373 63.9335C35.9449 63.1459 40.2322 61.0779 40.1928 58.8201C40.1396 55.7739 38.0632 54.658 33.5688 54.658C29.0744 54.658 26.6802 55.4072 27.9131 59.6776C28.3325 61.1303 31.0022 63.1903 29.1192 63.9335C29.052 63.9556 28.8838 63.9778 28.6597 64H11.7803C9.78528 64 8.17132 62.4139 8.1377 60.4395V43.225C8.16011 43.0697 8.18253 42.9588 8.21615 42.9033C9.01192 41.2284 11.7803 44.8776 13.3831 45.4322C17.6197 46.8853 21.5201 43.1362 21.5201 38.6884C21.5201 34.2406 18.9535 27.9515 14.7168 29.4156C13.1141 29.9702 9.00072 34.8363 8.24978 32.9729C8.21615 32.9063 8.19374 32.7178 8.17132 32.4626L8.1377 16.9373C8.1377 16.9373 8.18253 16.3605 8.23857 16.0832C8.5636 13.9979 10.7492 13.1328 12.6209 13.1328L14.7168 13.1771C14.7168 13.1771 19.8725 13.2991 24.0644 13.3102H25.8016C27.6509 13.3102 29.0632 13.2659 29.3097 13.166C31.1927 12.4229 27.3147 9.63882 26.7543 8.05269C25.286 3.85997 29.0856 0 33.5688 0C38.052 0 41.8516 3.85997 40.3945 8.04159C39.8341 9.62773 36.1467 12.3785 37.8391 13.1549C37.996 13.2326 38.4219 13.277 39.0496 13.3102H46.7047C49.7533 13.2437 52.4096 13.1549 52.4096 13.1549L54.5055 13.1106C55.2228 13.1106 55.9962 13.2659 56.7023 13.5653C56.7247 13.5764 56.7583 13.5875 56.7919 13.5986C56.8592 13.6319 56.9152 13.6652 56.9825 13.6984C57.1394 13.7872 57.3075 13.8759 57.4532 13.9757C57.5877 14.0756 57.7222 14.1865 57.8455 14.2974C57.8791 14.3307 57.9127 14.3529 57.9464 14.3861C58.4283 14.852 58.787 15.451 58.9215 16.1941C58.9775 16.427 58.9999 16.6711 58.9999 16.9262V29.4156C58.9887 30.0257 58.4218 32.5682 57.9403 32.9484C55.2912 35.0404 54.2316 27.1052 50.5229 30.8565C47.5946 33.8183 47.7379 42.2805 51.5825 43.9313Z"
fill="url(#paint0_linear)"
fillOpacity="0.32"
/>
<rect y="24.6855" width="12.9467" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect y="46.627" width="12.9467" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect y="52.1133" width="12.9467" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect y="19.1992" width="11.0972" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="11.0977" y="30.1699" width="12.9467" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="11.0977" y="41.1426" width="12.9467" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="11.0977" y="35.6562" width="15.7211" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="13.8711" y="24.6855" width="9.24768" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="13.8711" y="46.627" width="9.24768" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="13.8711" y="52.1133" width="5.54861" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="12.0225" y="19.1992" width="5.54861" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="24.9688" y="30.1699" width="4.62384" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="24.9688" y="41.1426" width="9.24767" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="30.5176" y="30.1699" width="7.39814" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="35.1416" y="41.1426" width="4.62384" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<rect x="27.7432" y="35.6562" width="7.39814" height="2.74286" rx="1.37143" fill="#ABC7EB" fillOpacity="0.5" />
<defs>
<linearGradient id="paint0_linear" x1="33.5688" y1="0" x2="33.5688" y2="64" gradientUnits="userSpaceOnUse">
<stop stopColor="#95A5C6" stopOpacity="0.6" />
<stop offset="1" stopColor="#95A5C6" />
</linearGradient>
</defs>
</svg>
))

View File

@ -1,10 +0,0 @@
.empty-command-list {
position: relative;
color: var(--gray-08);
padding: 1rem;
background-color: var(--color-bg-1);
:global(.theme-dark) & {
color: var(--gray-05);
}
}

View File

@ -1,15 +0,0 @@
import React, { HTMLAttributes } from 'react'
import classNames from 'classnames'
import styles from './EmptyCommandListContainer.module.scss'
type EmptyCommandListContainerProps = HTMLAttributes<HTMLDivElement>
export const EmptyCommandListContainer: React.FunctionComponent<
React.PropsWithChildren<EmptyCommandListContainerProps>
> = ({ className, children, ...rest }) => (
<div className={classNames(styles.emptyCommandList, className)} {...rest}>
{children}
</div>
)

View File

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

View File

@ -15,14 +15,10 @@ describe('getContributedActionItems', () => {
{ id: 'c', command: 'c', title: 'tc', description: 'dc' },
],
menus: {
commandPalette: [
{ action: 'a', group: '2' },
{ action: 'b', group: '1', alt: 'c' },
],
'editor/title': [{ action: 'c' }],
'editor/title': [{ action: 'b', alt: 'c' }, { action: 'a' }],
},
},
ContributableMenu.CommandPalette
ContributableMenu.EditorTitle
)
).toEqual([
{

View File

@ -1,9 +1,8 @@
import { Remote } from 'comlink'
import { Observable, Unsubscribable } from 'rxjs'
import { Unsubscribable } from 'rxjs'
import type { CommandEntry, ExecuteCommandParameters } from '../api/client/mainthread-api'
import type { FlatExtensionHostAPI } from '../api/contract'
import type { PlainNotification } from '../api/extension/extensionHostApi'
export interface Controller extends Unsubscribable {
/**
@ -12,17 +11,11 @@ export interface Controller extends Unsubscribable {
*
* All callers should execute commands using this method instead of calling
* {@link sourcegraph:CommandRegistry#executeCommand} directly (to ensure errors are emitted as notifications).
*
* @param suppressNotificationOnError By default, if command execution throws (or rejects with) an error, the
* error will be shown in the global notification UI component. Pass suppressNotificationOnError as true to
* skip this. The error is always returned to the caller.
*/
executeCommand(parameters: ExecuteCommandParameters, suppressNotificationOnError?: boolean): Promise<any>
executeCommand(parameters: ExecuteCommandParameters): Promise<any>
registerCommand(entryToRegister: CommandEntry): Unsubscribable
commandErrors: Observable<PlainNotification>
/**
* Frees all resources associated with this client.
*/

View File

@ -1,6 +1,4 @@
import { Remote } from 'comlink'
import { from } from 'rxjs'
import { switchMap } from 'rxjs/operators'
import { ExposedToClient, initMainThreadAPI } from '../api/client/mainthread-api'
import { FlatExtensionHostAPI } from '../api/contract'
@ -52,9 +50,7 @@ export function createNoopController(platformContext: PlatformContext): Controll
})
})
return {
executeCommand: (parameters, suppressNotificationOnError) =>
api.then(({ exposedToClient }) => exposedToClient.executeCommand(parameters, suppressNotificationOnError)),
commandErrors: from(api).pipe(switchMap(({ exposedToClient }) => exposedToClient.commandErrors)),
executeCommand: parameters => api.then(({ exposedToClient }) => exposedToClient.executeCommand(parameters)),
registerCommand: entryToRegister =>
syncPromiseSubscription(
api.then(({ exposedToClient }) => exposedToClient.registerCommand(entryToRegister))

View File

@ -1,9 +1,7 @@
import { from, Subscription } from 'rxjs'
import { switchMap } from 'rxjs/operators'
import { Subscription } from 'rxjs'
import { createExtensionHostClientConnection } from '../api/client/connection'
import type { InitData } from '../api/extension/extensionHost'
import { syncPromiseSubscription } from '../api/util'
import type { PlatformContext } from '../platform/context'
import type { Controller } from './controller'
@ -24,8 +22,6 @@ export function createController(
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'showMessage'
| 'showInputBox'
| 'getStaticExtensions'
| 'telemetryService'
| 'clientApplication'
@ -49,19 +45,12 @@ export function createController(
// 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)
)
),
executeCommand: () => {
throw new Error('not implemented')
},
registerCommand: () => {
throw new Error('not implemented')
},
extHostAPI: extensionHostClientPromise.then(({ api }) => api),
unsubscribe: () => subscriptions.unsubscribe(),
}

View File

@ -1,4 +1,3 @@
import { action } from '@storybook/addon-actions'
import { createMemoryHistory } from 'history'
import { of } from 'rxjs'
@ -24,7 +23,6 @@ export const commonProps = (): HoverOverlayProps & SettingsCascadeProps => ({
extensionsController: NOOP_EXTENSIONS_CONTROLLER,
platformContext: NOOP_PLATFORM_CONTEXT,
overlayPosition: { top: 16, left: 16 },
onAlertDismissed: action('onAlertDismissed'),
settingsCascade: EMPTY_SETTINGS_CASCADE,
})

View File

@ -106,29 +106,6 @@ describe('HoverOverlay', () => {
).toMatchSnapshot()
})
test('actions, hover and alert present', () => {
expect(
render(
<HoverOverlay
{...commonProps}
actionsOrError={[{ action: { id: 'a', command: 'c' }, active: true }]}
hoverOrError={{
contents: [{ kind: MarkupKind.Markdown, value: 'v' }],
alerts: [
{
summary: {
kind: MarkupKind.Markdown,
value: 'Testing `markdown` rendering.',
},
type: 'test-alert-dismissalType',
},
],
}}
/>
).asFragment()
).toMatchSnapshot()
})
test('actions present, hover loading', () => {
expect(
render(

View File

@ -7,14 +7,12 @@ import { isErrorLike, sanitizeClass } from '@sourcegraph/common'
import { Card, Icon, Button } from '@sourcegraph/wildcard'
import { ActionItem, ActionItemComponentProps } from '../actions/ActionItem'
import { NotificationType } from '../api/extension/extensionHostApi'
import { PlatformContextProps } from '../platform/context'
import { TelemetryProps } from '../telemetry/telemetryService'
import { CopyLinkIcon } from './CopyLinkIcon'
import { toNativeEvent } from './helpers'
import type { HoverContext, HoverOverlayBaseProps, GetAlertClassName, GetAlertVariant } from './HoverOverlay.types'
import { HoverOverlayAlerts, HoverOverlayAlertsProps } from './HoverOverlayAlerts'
import type { HoverContext, HoverOverlayBaseProps } from './HoverOverlay.types'
import { HoverOverlayContents } from './HoverOverlayContents'
import { HoverOverlayLogo } from './HoverOverlayLogo'
import { useLogTelemetryEvent } from './useLogTelemetryEvent'
@ -41,16 +39,6 @@ export interface HoverOverlayClassProps {
actionItemPressedClassName?: string
contentClassName?: string
/**
* Allows providing any custom className to style the notifications as desired.
*/
getAlertClassName?: GetAlertClassName
/**
* Allows providing a specific variant style for use in branded Sourcegraph applications.
*/
getAlertVariant?: GetAlertVariant
}
export interface HoverOverlayProps
@ -58,7 +46,6 @@ export interface HoverOverlayProps
ActionItemComponentProps,
HoverOverlayClassProps,
TelemetryProps,
Pick<HoverOverlayAlertsProps, 'onAlertDismissed'>,
PlatformContextProps<'settings'> {
/** A ref callback to get the root overlay element. Use this to calculate the position. */
hoverRef?: React.Ref<HTMLDivElement>
@ -121,10 +108,6 @@ export const HoverOverlay: React.FunctionComponent<React.PropsWithChildren<Hover
actionItemStyleProps,
getAlertClassName,
getAlertVariant,
onAlertDismissed,
useBrandedLogo,
} = props
@ -179,24 +162,9 @@ export const HoverOverlay: React.FunctionComponent<React.PropsWithChildren<Hover
hoverOrError={hoverOrError}
iconClassName={iconClassName}
badgeClassName={badgeClassName}
errorAlertClassName={getAlertClassName?.(NotificationType.Error)}
errorAlertVariant={getAlertVariant?.(NotificationType.Error)}
contentClassName={contentClassName}
/>
</div>
{hoverOrError &&
hoverOrError !== LOADING &&
!isErrorLike(hoverOrError) &&
hoverOrError.alerts &&
hoverOrError.alerts.length > 0 && (
<HoverOverlayAlerts
hoverAlerts={hoverOrError.alerts}
iconClassName={iconClassName}
getAlertClassName={getAlertClassName}
getAlertVariant={getAlertVariant}
onAlertDismissed={onAlertDismissed}
/>
)}
<div className={hoverOverlayStyle.actionsContainer}>
{actionsOrError !== undefined &&
actionsOrError !== null &&
@ -220,7 +188,6 @@ export const HoverOverlay: React.FunctionComponent<React.PropsWithChildren<Hover
variant="actionItem"
disabledDuringExecution={true}
showLoadingSpinnerDuringExecution={true}
showInlineError={true}
platformContext={platformContext}
telemetryService={telemetryService}
extensionsController={extensionsController}

View File

@ -1,19 +1,9 @@
import { HoverMerged } from '@sourcegraph/client-api'
import { HoverOverlayProps as GenericHoverOverlayProps } from '@sourcegraph/codeintellify'
import { AlertProps } from '@sourcegraph/wildcard'
import { ActionItemAction } from '../actions/ActionItem'
import type { NotificationType } from '../codeintel/legacy-extensions/api'
import { FileSpec, RepoSpec, ResolvedRevisionSpec, RevisionSpec } from '../util/url'
export type HoverContext = RepoSpec & RevisionSpec & FileSpec & ResolvedRevisionSpec
export interface HoverOverlayBaseProps extends GenericHoverOverlayProps<HoverContext, HoverMerged, ActionItemAction> {}
export type GetAlertClassName = (
kind: Exclude<NotificationType, NotificationType.Log | NotificationType.Success>
) => string | undefined
export type GetAlertVariant = (
kind: Exclude<NotificationType, NotificationType.Log | NotificationType.Success>
) => AlertProps['variant']

View File

@ -1,20 +0,0 @@
// Every block inside of the overlay should have equal horizontal padding.
// Only `contents` has bigger padding on the right to avoid close button overlap on scroll.
.hover-overlay__alerts {
--hover-overlay-horizontal-padding: 1rem;
--hover-overlay-separator-color: var(--border-color);
padding-left: var(--hover-overlay-horizontal-padding);
padding-right: var(--hover-overlay-horizontal-padding);
display: grid;
flex-direction: column;
row-gap: 0.75rem;
// `padding-top` and `border` are required to separate alerts block from the scrollable content.
// Otherwise text scrolls right from the alert border which doesn't look good.
border-top: 1px solid var(--hover-overlay-separator-color);
padding-top: 0.75rem;
padding-bottom: 0.75rem;
// Make sure HoverOverlay doesn't get too large even with large alerts.
overflow-y: auto;
max-height: 20rem;
}

View File

@ -1,94 +0,0 @@
import React from 'react'
import classNames from 'classnames'
import { renderMarkdown } from '@sourcegraph/common'
import { Link, Alert } from '@sourcegraph/wildcard'
import { NotificationType } from '../../api/extension/extensionHostApi'
import type { HoverAlert } from '../../codeintel/legacy-extensions/api'
import { GetAlertClassName, GetAlertVariant } from '../HoverOverlay.types'
import hoverOverlayStyle from '../HoverOverlay.module.scss'
import contentStyles from '../HoverOverlayContents/HoverOverlayContent/HoverOverlayContent.module.scss'
import styles from './HoverOverlayAlerts.module.scss'
export interface HoverOverlayAlertsProps {
hoverAlerts: HoverAlert[]
iconClassName?: string
/** Called when an alert is dismissed, with the type of the dismissed alert. */
onAlertDismissed?: (alertType: string) => void
getAlertClassName?: GetAlertClassName
getAlertVariant?: GetAlertVariant
className?: string
}
const iconKindToNotificationType: Record<Required<HoverAlert>['iconKind'], Parameters<GetAlertClassName>[0]> = {
info: NotificationType.Info,
warning: NotificationType.Warning,
error: NotificationType.Error,
}
export const HoverOverlayAlerts: React.FunctionComponent<React.PropsWithChildren<HoverOverlayAlertsProps>> = props => {
const { hoverAlerts, onAlertDismissed, getAlertClassName, getAlertVariant } = props
const createAlertDismissedHandler = (alertType: string) => (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault()
if (onAlertDismissed) {
onAlertDismissed(alertType)
}
}
return (
<div className={classNames(styles.hoverOverlayAlerts, props.className)}>
{hoverAlerts.map(({ summary, iconKind, type, buttons }, index) => {
// Show dismiss button when an alert has a dismissal type. If
// no type is provided, the alert is not dismissible.
const dismissalButton = type ? (
<div className={classNames(hoverOverlayStyle.alertDismiss)}>
{/* Ideally this should a <button> but we can't guarantee we have the .btn-link class here. */}
<Link to="" onClick={createAlertDismissedHandler(type)} role="button">
<small>Dismiss</small>
</Link>
</div>
) : null
return (
<Alert
key={index}
variant={getAlertVariant?.(
iconKind ? iconKindToNotificationType[iconKind] : NotificationType.Info
)}
className={classNames(
hoverOverlayStyle.alert,
getAlertClassName?.(iconKind ? iconKindToNotificationType[iconKind] : NotificationType.Info)
)}
>
<span
data-testid="hover-overlay-content"
className={classNames(
contentStyles.hoverOverlayContent,
hoverOverlayStyle.hoverOverlayContent
)}
>
{summary.kind === 'plaintext' ? (
summary.value
) : (
<span dangerouslySetInnerHTML={{ __html: renderMarkdown(summary.value) }} />
)}
{buttons ? (
<div className={hoverOverlayStyle.buttons}>
{buttons}
{dismissalButton}
</div>
) : null}
</span>
{dismissalButton && !buttons ? dismissalButton : null}
</Alert>
)
})}
</div>
)
}

View File

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

View File

@ -49,7 +49,7 @@ export const HoverOverlayContents: React.FunctionComponent<
return null
}
if (hoverOrError === null || (hoverOrError.contents.length === 0 && hoverOrError.alerts?.length)) {
if (hoverOrError === null || hoverOrError.contents.length === 0) {
return (
// Show some content to give the close button space and communicate to the user we couldn't find a hover.
<small className={classNames(hoverOverlayStyle.hoverEmpty)}>No hover information available.</small>

View File

@ -271,89 +271,6 @@ exports[`HoverOverlay actions present, hover loading 1`] = `
</DocumentFragment>
`;
exports[`HoverOverlay actions, hover and alert present 1`] = `
<DocumentFragment>
<div
class="card hoverOverlay"
data-testid="hover-overlay"
style="opacity: 1; visibility: visible; left: 0px; top: 0px;"
>
<div
class="hoverOverlayContents"
data-testid="hover-overlay-contents"
>
<span
class="hoverOverlayContent hoverOverlayContent test-tooltip-content"
data-testid="hover-overlay-content"
>
<p>
v
</p>
</span>
</div>
<div
class="hoverOverlayAlerts"
>
<div
aria-live="polite"
class="alert"
role="alert"
>
<span
class="hoverOverlayContent hoverOverlayContent"
data-testid="hover-overlay-content"
>
<span>
<p>
Testing
<code>
markdown
</code>
rendering.
</p>
</span>
</span>
<div
class="alertDismiss"
>
<a
class=""
href=""
role="button"
>
<small>
Dismiss
</small>
</a>
</div>
</div>
</div>
<div
class="actionsContainer"
>
<div
class="actions"
>
<div
class="actionsInner"
>
<button
class="test-action-item action test-tooltip-untitled"
type="button"
>
</button>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`HoverOverlay hover empty 1`] = `<DocumentFragment />`;
exports[`HoverOverlay hover error 1`] = `

View File

@ -1,6 +1,4 @@
import { Remote } from 'comlink'
import { createMemoryHistory, MemoryHistory, createPath } from 'history'
import { from, Observable, of, Subscription } from 'rxjs'
import { from, Observable, of } from 'rxjs'
import { first } from 'rxjs/operators'
import { TestScheduler } from 'rxjs/testing'
import * as sinon from 'sinon'
@ -13,12 +11,8 @@ import { Position, Range } from '@sourcegraph/extension-api-classes'
import { Location } from '@sourcegraph/extension-api-types'
import { GraphQLResult, SuccessGraphQLResult } from '@sourcegraph/http-client'
import { ActionItemAction } from '../actions/ActionItem'
import { ExposedToClient } from '../api/client/mainthread-api'
import { FlatExtensionHostAPI } from '../api/contract'
import { WorkspaceRootWithMetadata } from '../api/extension/extensionHostApi'
import { PlatformContext, URLToFileContext } from '../platform/context'
import { integrationTestContext } from '../testing/testHelpers'
import {
FileSpec,
UIPositionSpec,
@ -26,17 +20,10 @@ import {
RepoSpec,
RevisionSpec,
ViewStateSpec,
toAbsoluteBlobURL,
toPrettyBlobURL,
} from '../util/url'
import {
getDefinitionURL,
getHoverActionItems,
getHoverActionsContext,
HoverActionsContext,
registerHoverContributions,
} from './actions'
import { getDefinitionURL, getHoverActionsContext, HoverActionsContext } from './actions'
import { HoverContext } from './HoverOverlay'
const FIXTURE_PARAMS: TextDocumentPositionParameters & URLToFileContext = {
@ -514,294 +501,3 @@ describe('getDefinitionURL', () => {
.toPromise()
).resolves.toEqual({ isLoading: false, result: { url: '/r@v/-/blob/f?L2:2#tab=def', multiple: true } }))
})
describe('registerHoverContributions()', () => {
const subscription = new Subscription()
let history!: MemoryHistory
let extensionHostAPI!: Promise<Remote<FlatExtensionHostAPI>>
let extensionAPI!: typeof sourcegraph
let exposedToClient!: ExposedToClient
let locationAssign!: sinon.SinonSpy<[string], void>
beforeEach(async () => {
resetAllMemoizationCaches()
const context = await integrationTestContext(undefined, {
textDocuments: [
{
languageId: 'x',
uri: 'git://r?c#f',
text: undefined,
},
],
roots: [],
viewers: [],
})
subscription.add(() => context.unsubscribe())
extensionHostAPI = Promise.resolve(context.extensionHostAPI)
extensionAPI = context.extensionAPI
exposedToClient = context.exposedToClient
history = createMemoryHistory()
locationAssign = sinon.spy((_url: string) => undefined)
const contributionsSubscription = registerHoverContributions({
extensionsController: {
extHostAPI: Promise.resolve(extensionHostAPI),
registerCommand: exposedToClient.registerCommand,
},
platformContext: { urlToFile, requestGraphQL },
historyOrNavigate: history,
getLocation: () => history.location,
locationAssign,
})
subscription.add(contributionsSubscription)
await contributionsSubscription.contributionsPromise
})
afterAll(() => subscription.unsubscribe())
describe('getHoverActions()', () => {
const GO_TO_DEFINITION_ACTION: ActionItemAction = {
action: {
command: 'goToDefinition',
commandArguments: ['{"textDocument":{"uri":"git://r?c#f"},"position":{"line":1,"character":1}}'],
id: 'goToDefinition',
title: 'Go to definition',
actionItem: undefined,
category: undefined,
description: undefined,
iconURL: undefined,
},
active: true,
disabledWhen: false,
altAction: undefined,
}
const GO_TO_DEFINITION_PRELOADED_ACTION: ActionItemAction = {
action: {
command: 'open',
commandArguments: ['/r2@c2/-/blob/f2?L3:3'],
id: 'goToDefinition.preloaded',
title: 'Go to definition',
disabledTitle: 'You are at the definition',
},
active: true,
disabledWhen: false,
altAction: undefined,
}
const FIND_REFERENCES_ACTION: ActionItemAction = {
action: {
command: 'open',
commandArguments: ['/r@v/-/blob/f?L2:2#tab=references'],
id: 'findReferences',
title: 'Find references',
},
active: true,
disabledWhen: false,
altAction: undefined,
}
it('shows goToDefinition (non-preloaded) when the definition is loading', () =>
expect(
getHoverActionItems(
{
'goToDefinition.showLoading': true,
'goToDefinition.url': null,
'goToDefinition.notFound': false,
'goToDefinition.error': false,
'findReferences.url': null,
hoverPosition: FIXTURE_PARAMS,
hoveredOnDefinition: false,
},
extensionHostAPI
).toPromise()
).resolves.toEqual([GO_TO_DEFINITION_ACTION]))
it('shows goToDefinition (non-preloaded) when the definition had an error', () =>
expect(
getHoverActionItems(
{
'goToDefinition.showLoading': false,
'goToDefinition.url': null,
'goToDefinition.notFound': false,
'goToDefinition.error': true,
'findReferences.url': null,
hoverPosition: FIXTURE_PARAMS,
hoveredOnDefinition: false,
},
extensionHostAPI
).toPromise()
).resolves.toEqual([GO_TO_DEFINITION_ACTION]))
it('hides goToDefinition when the definition was not found', () =>
expect(
getHoverActionItems(
{
'goToDefinition.showLoading': false,
'goToDefinition.url': null,
'goToDefinition.notFound': true,
'goToDefinition.error': false,
'findReferences.url': null,
hoverPosition: FIXTURE_PARAMS,
hoveredOnDefinition: false,
},
extensionHostAPI
).toPromise()
).resolves.toEqual([]))
it('shows goToDefinition.preloaded when goToDefinition.url is available', () =>
expect(
getHoverActionItems(
{
'goToDefinition.showLoading': false,
'goToDefinition.url': '/r2@c2/-/blob/f2?L3:3',
'goToDefinition.notFound': false,
'goToDefinition.error': false,
'findReferences.url': null,
hoverPosition: FIXTURE_PARAMS,
hoveredOnDefinition: false,
},
extensionHostAPI
).toPromise()
).resolves.toEqual([GO_TO_DEFINITION_PRELOADED_ACTION]))
it('shows findReferences when the definition exists', () =>
expect(
getHoverActionItems(
{
'goToDefinition.showLoading': false,
'goToDefinition.url': '/r2@c2/-/blob/f2?L3:3',
'goToDefinition.notFound': false,
'goToDefinition.error': false,
'findReferences.url': '/r@v/-/blob/f?L2:2#tab=references',
hoverPosition: FIXTURE_PARAMS,
hoveredOnDefinition: false,
},
extensionHostAPI
).toPromise()
).resolves.toEqual([GO_TO_DEFINITION_PRELOADED_ACTION, FIND_REFERENCES_ACTION]))
it('hides findReferences when the definition might exist (and is still loading)', () =>
expect(
getHoverActionItems(
{
'goToDefinition.showLoading': true,
'goToDefinition.url': null,
'goToDefinition.notFound': false,
'goToDefinition.error': false,
'findReferences.url': '/r@v/-/blob/f?L2:2#tab=references',
hoverPosition: FIXTURE_PARAMS,
hoveredOnDefinition: false,
},
extensionHostAPI
).toPromise()
).resolves.toEqual([GO_TO_DEFINITION_ACTION, FIND_REFERENCES_ACTION]))
it('shows findReferences when the definition had an error', () =>
expect(
getHoverActionItems(
{
'goToDefinition.showLoading': false,
'goToDefinition.url': null,
'goToDefinition.notFound': false,
'goToDefinition.error': true,
'findReferences.url': '/r@v/-/blob/f?L2:2#tab=references',
hoverPosition: FIXTURE_PARAMS,
hoveredOnDefinition: false,
},
extensionHostAPI
).toPromise()
).resolves.toEqual([GO_TO_DEFINITION_ACTION, FIND_REFERENCES_ACTION]))
it('does not show findReferences when the definition was not found', () =>
expect(
getHoverActionItems(
{
'goToDefinition.showLoading': false,
'goToDefinition.url': null,
'goToDefinition.notFound': true,
'goToDefinition.error': false,
'findReferences.url': '/r@v/-/blob/f?L2:2#tab=references',
hoverPosition: FIXTURE_PARAMS,
hoveredOnDefinition: false,
},
extensionHostAPI
).toPromise()
).resolves.toEqual([]))
})
describe('goToDefinition command', () => {
test('reports no definition found', async () => {
const definitionSubscription = extensionAPI.languages.registerDefinitionProvider(['*'], {
provideDefinition: () => of([]),
})
await expect(
exposedToClient.executeCommand({ command: 'goToDefinition', args: [JSON.stringify(FIXTURE_PARAMS)] })
).rejects.toMatchObject({ message: 'No definition found.' })
definitionSubscription.unsubscribe()
})
test('navigates to an in-app URL using the passed history object', async () => {
jsdom.reconfigure({ url: 'https://sourcegraph.test/r2@c2/-/blob/f1' })
history.replace('/r2@c2/-/blob/f1')
expect(history).toHaveLength(1)
const definitionSubscription = extensionAPI.languages.registerDefinitionProvider(['*'], {
provideDefinition: () => of(FIXTURE_LOCATION),
})
await exposedToClient.executeCommand({ command: 'goToDefinition', args: [JSON.stringify(FIXTURE_PARAMS)] })
sinon.assert.notCalled(locationAssign)
expect(history).toHaveLength(2)
expect(createPath(history.location)).toBe('/r2@c2/-/blob/f2?L3:3')
definitionSubscription.unsubscribe()
})
test('navigates to an external URL using the global location object', async () => {
jsdom.reconfigure({ url: 'https://github.test/r2@c2/-/blob/f1' })
history.replace('/r2@c2/-/blob/f1')
expect(history).toHaveLength(1)
urlToFile.callsFake(toAbsoluteBlobURL.bind(null, 'https://sourcegraph.test'))
const definitionSubscription = extensionAPI.languages.registerDefinitionProvider(['*'], {
provideDefinition: () =>
of([FIXTURE_LOCATION, { ...FIXTURE_LOCATION, uri: new URL('git://r3?v3#f3') }]),
})
await exposedToClient.executeCommand({ command: 'goToDefinition', args: [JSON.stringify(FIXTURE_PARAMS)] })
sinon.assert.calledOnce(locationAssign)
sinon.assert.calledWith(locationAssign, 'https://sourcegraph.test/r@c/-/blob/f?L2:2#tab=def')
expect(history).toHaveLength(1)
definitionSubscription.unsubscribe()
})
test('reports panel already visible', async () => {
const definitionSubscription = extensionAPI.languages.registerDefinitionProvider(['*'], {
provideDefinition: () =>
of([FIXTURE_LOCATION, { ...FIXTURE_LOCATION, uri: new URL('git://r3?v3#f3') }]),
})
history.push('/r@c/-/blob/f?L2:2#tab=def')
await expect(
exposedToClient.executeCommand({ command: 'goToDefinition', args: [JSON.stringify(FIXTURE_PARAMS)] })
).rejects.toMatchObject({ message: 'Multiple definitions shown in panel below.' })
definitionSubscription.unsubscribe()
})
test('reports already at the definition', async () => {
const definitionSubscription = extensionAPI.languages.registerDefinitionProvider(['*'], {
provideDefinition: () => of([FIXTURE_LOCATION]),
})
history.push('/r2@c2/-/blob/f2?L3:3')
await expect(
exposedToClient.executeCommand({ command: 'goToDefinition', args: [JSON.stringify(FIXTURE_PARAMS)] })
).rejects.toMatchObject({ message: 'Already at the definition.' })
definitionSubscription.unsubscribe()
})
})
})

View File

@ -3,7 +3,6 @@ import { isMacPlatform, isSafari } from '@sourcegraph/common'
import { KeyboardShortcut } from '../keyboardShortcuts'
type KEYBOARD_SHORTCUT_IDENTIFIERS =
| 'commandPalette'
| 'switchTheme'
| 'keyboardShortcutsHelp'
| 'focusSearch'
@ -17,10 +16,6 @@ type KEYBOARD_SHORTCUT_IDENTIFIERS =
export type KEYBOARD_SHORTCUT_MAPPING = Record<KEYBOARD_SHORTCUT_IDENTIFIERS, KeyboardShortcut>
export const KEYBOARD_SHORTCUTS: KEYBOARD_SHORTCUT_MAPPING = {
commandPalette: {
title: 'Show command palette',
keybindings: [{ held: ['Control'], ordered: ['p'] }, { ordered: ['F1'] }, { held: ['Alt'], ordered: ['x'] }],
},
switchTheme: {
title: 'Switch color theme',
// use '†' here to make `Alt + t` works on macos

View File

@ -1,55 +0,0 @@
.sourcegraph-notification-item {
display: block;
transition: all 300ms ease-in;
}
.progress {
// important is required to override the sibling class
background: transparent !important;
border-radius: 0;
}
.progressbar {
height: 0.25rem;
transition: width 0.6s ease;
}
.body-container {
display: flex;
align-items: flex-start;
}
.body {
flex: 1;
padding: 0.5rem;
overflow: hidden;
pre,
code {
overflow-x: auto;
}
}
.content > :last-child,
.title > :last-child {
margin-bottom: 0;
}
.close {
cursor: pointer;
flex: 0 0;
/* stylelint-disable-next-line declaration-property-unit-allowed-list */
font-size: 1.25rem;
line-height: 1;
background-color: transparent;
border-width: 0;
outline: 0 !important;
color: inherit;
opacity: 0.7;
padding: 0.5rem;
&:hover,
&:focus {
opacity: 1;
}
}

View File

@ -1,112 +0,0 @@
import { action } from '@storybook/addon-actions'
import { DecoratorFn, Meta, Story } from '@storybook/react'
import { of } from 'rxjs'
import { NotificationType } from '@sourcegraph/extension-api-classes'
import { BrandedStory } from '@sourcegraph/wildcard/src/stories'
import type { NotificationType as NotificationTypeType } from '../codeintel/legacy-extensions/api'
import { NotificationItem } from './NotificationItem'
const notificationClassNames = {
[NotificationType.Log]: 'bg-secondary',
[NotificationType.Success]: 'bg-success',
[NotificationType.Info]: 'bg-info',
[NotificationType.Warning]: 'bg-warning',
[NotificationType.Error]: 'bg-danger',
}
const onDismiss = action('onDismiss')
const decorator: DecoratorFn = story => (
<BrandedStory>{() => <div style={{ maxWidth: '20rem', margin: '2rem' }}>{story()}</div>}</BrandedStory>
)
const config: Meta = {
title: 'shared/NotificationItem',
decorators: [decorator],
argTypes: {
message: {
name: 'Message',
control: { type: 'text' },
defaultValue: 'My *custom* message',
},
type: {
name: 'type',
control: {
type: 'select',
options: NotificationType as Record<keyof typeof NotificationType, NotificationTypeType>,
},
},
source: {
name: 'Source',
control: { type: 'text' },
defaultValue: 'some source',
},
},
}
export default config
export const WithoutProgress: Story = args => {
const message = args.message
const type = args.type
const source = args.source
return (
<NotificationItem
notification={{ message, type, source }}
notificationItemStyleProps={{ notificationItemClassNames: notificationClassNames }}
onDismiss={onDismiss}
/>
)
}
WithoutProgress.argTypes = {
type: {
defaultValue: NotificationType.Error,
},
}
export const WithProgress: Story = args => {
const message = args.message
const type = args.type
const source = args.source
const progressMessage = args.progressMessage
const progressPercentage = args.progressPercentage
return (
<NotificationItem
notification={{
message,
type,
source,
progress: of({
message: progressMessage,
percentage: progressPercentage,
}),
}}
notificationItemStyleProps={{ notificationItemClassNames: notificationClassNames }}
onDismiss={onDismiss}
/>
)
}
WithProgress.argTypes = {
progressMessage: {
name: 'Progress message',
control: { type: 'text' },
defaultValue: 'My *custom* progress message',
},
progressPercentage: {
name: 'Progress % (0-100)',
control: { type: 'number', min: 0, max: 100 },
defaultValue: 50,
},
type: {
defaultValue: NotificationType.Info,
},
}
WithProgress.storyName = 'With progress'
WithProgress.parameters = {
chromatic: {
enableDarkMode: true,
disableSnapshot: false,
},
}

View File

@ -1,139 +0,0 @@
import * as React from 'react'
import classNames from 'classnames'
import { from, Subject, Subscription } from 'rxjs'
import { catchError, distinctUntilChanged, map, scan, switchMap } from 'rxjs/operators'
import { renderMarkdown } from '@sourcegraph/common'
import { Alert } from '@sourcegraph/wildcard'
import type { NotificationType, Progress } from '../codeintel/legacy-extensions/api'
import { Notification } from './notification'
import styles from './NotificationItem.module.scss'
export interface UnbrandedNotificationItemStyleProps {
notificationItemClassNames: Record<NotificationType, string>
}
export interface NotificationItemProps {
notification: Notification
onDismiss: (notification: Notification) => void
className?: string
notificationItemStyleProps: UnbrandedNotificationItemStyleProps
}
interface NotificationItemState {
progress?: Required<Progress>
}
/**
* A notification message displayed in a {@link module:./Notifications.Notifications} component.
*/
export class NotificationItem extends React.PureComponent<NotificationItemProps, NotificationItemState> {
private componentUpdates = new Subject<NotificationItemProps>()
private subscription = new Subscription()
constructor(props: NotificationItemProps) {
super(props)
this.state = {
progress: props.notification.progress && {
percentage: 0,
message: '',
},
}
}
public componentDidMount(): void {
this.subscription.add(
this.componentUpdates
.pipe(
map(props => props.notification.progress),
distinctUntilChanged(),
switchMap(progress =>
from(progress || []).pipe(
// Hide progress bar and update message if error occurred
// Merge new progress updates with previous
scan<Progress, Required<Progress>>(
(current, { message = current.message, percentage = current.percentage }) => ({
message,
percentage,
}),
{
percentage: 0,
message: '',
}
),
catchError(() => [undefined])
)
)
)
.subscribe(progress => {
this.setState({ progress })
})
)
this.componentUpdates.next(this.props)
}
public componentDidUpdate(): void {
this.componentUpdates.next(this.props)
}
public componentWillUnmount(): void {
this.subscription.unsubscribe()
}
public render(): JSX.Element | null {
const baseAlertClassName = classNames(styles.sourcegraphNotificationItem, this.props.className)
const { notificationItemStyleProps } = this.props
const alertProps = {
className: classNames(
baseAlertClassName,
notificationItemStyleProps.notificationItemClassNames[this.props.notification.type]
),
}
return (
<Alert {...alertProps}>
<div className={styles.bodyContainer}>
<div className={styles.body}>
<div
className={styles.title}
dangerouslySetInnerHTML={{
__html: renderMarkdown(this.props.notification.message || '', {
allowDataUriLinksAndDownloads: true,
}),
}}
/>
{this.state.progress && (
<div
className={styles.content}
dangerouslySetInnerHTML={{
__html: renderMarkdown(this.state.progress.message),
}}
/>
)}
</div>
{(!this.props.notification.progress || !this.state.progress) && (
<button
type="button"
className={classNames('close', styles.close)}
onClick={this.onDismiss}
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
)}
</div>
{this.props.notification.progress && this.state.progress && (
<div className={classNames('progress', styles.progress)}>
<div
className={classNames('progress-bar', styles.progressbar)}
// eslint-disable-next-line react/forbid-dom-props
style={{ width: `${this.state.progress.percentage}%` }}
/>
</div>
)}
</Alert>
)
}
private onDismiss = (): void => this.props.onDismiss(this.props.notification)
}

View File

@ -1,7 +0,0 @@
.sourcegraph-notifications {
position: fixed;
width: 28rem;
top: 82px;
right: 0;
z-index: 90;
}

View File

@ -1,211 +0,0 @@
import * as React from 'react'
import { uniqueId } from 'lodash'
import { from, merge, Subscription } from 'rxjs'
import { delay, map, mergeMap, switchMap, takeWhile } from 'rxjs/operators'
import { tabbable } from 'tabbable'
import { asError, logger } from '@sourcegraph/common'
import { wrapRemoteObservable } from '../api/client/api/common'
import { NotificationType } from '../api/extension/extensionHostApi'
import { syncRemoteSubscription } from '../api/util'
import { RequiredExtensionsControllerProps } from '../extensions/controller'
import { Notification } from './notification'
import { NotificationItem, NotificationItemProps } from './NotificationItem'
import styles from './Notifications.module.scss'
export interface NotificationsProps
extends RequiredExtensionsControllerProps,
Pick<NotificationItemProps, 'notificationItemStyleProps'> {}
interface NotificationsState {
// TODO(tj): use remote progress observable type
notifications: (Notification & { id: string })[]
}
const HAS_NOTIFICATIONS_CONTEXT_KEY = 'hasNotifications'
/**
* A notifications center that displays global, non-modal messages.
*/
export class Notifications extends React.PureComponent<NotificationsProps, NotificationsState> {
/**
* The maximum number of notifications at a time. Older notifications are truncated when the length exceeds
* this number.
*/
private static MAX_RETAIN = 7
public state: NotificationsState = {
notifications: [],
}
private subscriptions = new Subscription()
private notificationsReference = React.createRef<HTMLDivElement>()
public componentDidMount(): void {
// Subscribe to plain notifications
this.subscriptions.add(
from(this.props.extensionsController.extHostAPI)
.pipe(
switchMap(extensionHostAPI =>
merge(
wrapRemoteObservable(extensionHostAPI.getPlainNotifications()),
// Subscribe to command error notifications (also plain)
this.props.extensionsController.commandErrors
)
),
map(notification => ({ ...notification, id: uniqueId('n') }))
)
.subscribe(notification => {
this.setState(previousState => ({
notifications: [...previousState.notifications.slice(-Notifications.MAX_RETAIN), notification],
}))
})
)
// Subscribe to progress notifications. This is more complex to handle than
// plain notifications because the emissions of the progress notification observable
// have to be proxied as well
this.subscriptions.add(
from(this.props.extensionsController.extHostAPI)
.pipe(mergeMap(extensionHostAPI => wrapRemoteObservable(extensionHostAPI.getProgressNotifications())))
.subscribe(progressNotification => {
// Progress notifications are remote, so property access is asynchronous
progressNotification.baseNotification
.then(baseNotification => {
// Turn ExtensionNotification type into client Notification type
// for NotificationItem to render (and subscribe to progressObservable)
const progressObservable = wrapRemoteObservable(progressNotification.progress)
const notification: Notification & { id: string } = {
...baseNotification,
progress: progressObservable,
id: uniqueId('n'),
}
this.setState(previousState => ({
notifications: [
...previousState.notifications.slice(-Notifications.MAX_RETAIN),
notification,
],
}))
// Remove this notification once progress is finished
this.subscriptions.add(
progressObservable
.pipe(
takeWhile(({ percentage }) => !percentage || percentage < 100),
delay(1000)
)
// eslint-disable-next-line rxjs/no-nested-subscribe
.subscribe({
error: error => {
const erroredNotification = notification
this.setState(({ notifications }) => ({
notifications: notifications.map(notification =>
notification === erroredNotification
? {
...notification,
type: NotificationType.Error,
message: asError(error).message,
}
: notification
),
}))
},
complete: () => {
const completedNotification = notification
this.setState(previousState => ({
notifications: previousState.notifications.filter(
notification => notification !== completedNotification
),
}))
},
})
)
})
.catch(() => {
// noop. there's no meaningful information to log if accessing
// baseNotification somehow failed
})
})
)
// Register command to focus notifications.
this.subscriptions.add(
this.props.extensionsController.registerCommand({
command: 'focusNotifications',
run: () => {
const notificationsElement = this.notificationsReference.current
if (notificationsElement) {
tabbable(notificationsElement)[0]?.focus()
}
return Promise.resolve()
},
})
)
this.subscriptions.add(
syncRemoteSubscription(
this.props.extensionsController.extHostAPI.then(extensionHostAPI =>
extensionHostAPI.registerContributions({
menus: {
commandPalette: [
{
action: 'focusNotifications',
when: HAS_NOTIFICATIONS_CONTEXT_KEY,
},
],
},
actions: [
{
id: 'focusNotifications',
title: 'Focus notifications',
command: 'focusNotifications',
},
],
})
)
)
)
}
public componentDidUpdate(): void {
// Update context to show/hide "Focus notifications" command.
this.props.extensionsController.extHostAPI
.then(extensionHostAPI =>
extensionHostAPI.updateContext({
[HAS_NOTIFICATIONS_CONTEXT_KEY]: this.state.notifications.length > 0,
})
)
.catch(error => logger.error('Error updating context for notifications', error))
}
public componentWillUnmount(): void {
this.subscriptions.unsubscribe()
}
public render(): JSX.Element | null {
return (
<div className={styles.sourcegraphNotifications} ref={this.notificationsReference}>
{this.state.notifications.slice(0, Notifications.MAX_RETAIN).map(notification => (
<NotificationItem
key={notification.id}
notification={notification}
onDismiss={this.onDismiss}
className="sourcegraph-notifications__notification m-2"
notificationItemStyleProps={this.props.notificationItemStyleProps}
/>
))}
</div>
)
}
private onDismiss = (dismissedNotification: Notification): void => {
this.setState(previousState => ({
notifications: previousState.notifications.filter(notification => notification !== dismissedNotification),
}))
}
}

View File

@ -1,25 +0,0 @@
import { Observable } from 'rxjs'
import type { NotificationType, Progress } from '../codeintel/legacy-extensions/api'
/**
* A notification message to display to the user.
*/
export interface Notification {
/** The message of the notification. */
message?: string
/**
* The type of the message.
*/
type: NotificationType
/** The source of the notification. */
source?: string
/**
* Progress updates to show in this notification (progress bar and status messages).
* If this Observable errors, the notification will be changed to an error type.
*/
progress?: Observable<Progress>
}

View File

@ -8,7 +8,6 @@ import { GraphQLClient, GraphQLResult } from '@sourcegraph/http-client'
import { SettingsEdit } from '../api/client/services/settings'
import { ExecutableExtension } from '../api/extension/activation'
import type { InputBoxOptions } from '../codeintel/legacy-extensions/api'
import { Scalars } from '../graphql-operations'
import { Settings, SettingsCascadeOrError } from '../settings/settings'
import { TelemetryService } from '../telemetry/telemetryService'
@ -189,22 +188,6 @@ export interface PlatformContext {
* the extension host will not activate any other settings (e.g. extensions from user settings)
*/
getStaticExtensions?: () => Observable<ExecutableExtension[] | undefined>
/**
* Display a modal message from an extension to the user.
*
* @param message The message to display
* @returns a Promise that resolves when the user dismisses the message
*/
showMessage?(message: string): Promise<void>
/**
* Displays an input box for an extension that asks the user for input.
*
* @param options Configures the behavior of the input box.
* @returns The string provided by the user, or `undefined` if the input box was canceled.
*/
showInputBox?(options: InputBoxOptions | undefined): Promise<string | undefined>
}
/**

View File

@ -630,7 +630,6 @@ export const extensionsController: Controller = {
haveInitialExtensionsLoaded: () => pretendProxySubscribable(of(true)),
})
),
commandErrors: EMPTY,
unsubscribe: noop,
}

View File

@ -5,7 +5,6 @@ import { throwError, of, Subscription, Unsubscribable, Subscribable } from 'rxjs
import * as sourcegraph from 'sourcegraph'
import { createExtensionHostClientConnection } from '../api/client/connection'
import { ExposedToClient } from '../api/client/mainthread-api'
import { FlatExtensionHostAPI, MainThreadAPI } from '../api/contract'
import { InitData, startExtensionHost } from '../api/extension/extensionHost'
import { WorkspaceRootWithMetadata } from '../api/extension/extensionHostApi'
@ -40,13 +39,7 @@ const FIXTURE_INIT_DATA: TestInitData = {
interface Mocks
extends Pick<
PlatformContext,
| 'settings'
| 'updateSettings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'clientApplication'
| 'showMessage'
| 'showInputBox'
'settings' | 'updateSettings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
> {}
const NOOP_MOCKS: Mocks = {
@ -70,7 +63,6 @@ export async function integrationTestContext(
extensionAPI: typeof sourcegraph
extensionHostAPI: Remote<FlatExtensionHostAPI>
mainThreadAPI: MainThreadAPI
exposedToClient: ExposedToClient
} & Unsubscribable
> {
const mocks = partialMocks ? { ...NOOP_MOCKS, ...partialMocks } : NOOP_MOCKS
@ -93,11 +85,7 @@ export async function integrationTestContext(
clientApplication: 'sourcegraph',
}
const {
api: extensionHostAPI,
mainThreadAPI,
exposedToClient,
} = await createExtensionHostClientConnection(
const { api: extensionHostAPI, mainThreadAPI } = await createExtensionHostClientConnection(
Promise.resolve({
endpoints: clientEndpoints,
subscription: new Subscription(),
@ -116,7 +104,6 @@ export async function integrationTestContext(
extensionAPI,
extensionHostAPI,
mainThreadAPI,
exposedToClient,
unsubscribe: () => extensionHost.unsubscribe(),
}
}

View File

@ -1,5 +0,0 @@
declare module 'string-score' {
function score(target: string, query: string, fuzzyFactor?: number): number
export = score
}

View File

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

View File

@ -65,38 +65,6 @@ exports[`KeyboardShortcutsHelp is triggered correctly 1`] = `
<ul
class="list-group list-group-flush"
>
<li
class="list-group-item d-flex align-items-center justify-content-between"
>
Show command palette
<span>
<span>
<kbd>
Ctrl
</kbd>
+
<kbd>
p
</kbd>
</span>
<span>
or
<kbd>
F1
</kbd>
</span>
<span>
or
<kbd>
Alt
</kbd>
+
<kbd>
x
</kbd>
</span>
</span>
</li>
<li
class="list-group-item d-flex align-items-center justify-content-between"
>

View File

@ -1,10 +1,5 @@
import { MarkupKind } from '@sourcegraph/extension-api-classes'
import type {
MarkupContent,
Badged,
AggregableBadge,
HoverAlert,
} from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
import type { MarkupContent, Badged, AggregableBadge } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
import { FIXTURE_SEMANTIC_BADGE } from '@sourcegraph/shared/src/hover/HoverOverlay.fixtures'
export const FIXTURE_CONTENT_LONG_CODE: Badged<MarkupContent> = {
@ -27,20 +22,3 @@ export const FIXTURE_PARTIAL_BADGE: AggregableBadge = {
...FIXTURE_SEMANTIC_BADGE,
text: 'partial semantic',
}
export const FIXTURE_SMALL_TEXT_MARKDOWN_ALERT: HoverAlert = {
summary: {
kind: MarkupKind.Markdown,
value: '<small>This is an info alert wrapped into a \\<small\\> element. Enim esse quis commodo ex. Pariatur tempor laborum officiairure est do est laborum nostrud cillum. Cupidatat id consectetur et eiusmod Loremproident cupidatat ullamco dolor nostrud. Cupidatat sit do dolor aliqua labore adlaboris cillum deserunt dolor. Sunt labore veniam Lorem reprehenderit quis occaecatsint do mollit aliquip. Consectetur mollit mollit magna eiusmod duis ex. Sint nisilabore labore nulla laboris.</small>',
},
type: 'test-alert-type',
}
export const FIXTURE_WARNING_MARKDOWN_ALERT: HoverAlert = {
summary: {
kind: MarkupKind.Markdown,
value: "This is a warning alert. [It uses Markdown.](https://sourcegraph.com) `To render things easily`. *Cool!*\n\nIt's a second paragraph.",
},
type: 'test-alert-type',
iconKind: 'warning',
}

View File

@ -1,8 +1,6 @@
import { action } from '@storybook/addon-actions'
import { DecoratorFn, Meta, Story } from '@storybook/react'
import { registerHighlightContributions } from '@sourcegraph/common'
import { MarkupKind } from '@sourcegraph/extension-api-classes'
import {
commonProps,
FIXTURE_ACTIONS,
@ -18,8 +16,6 @@ import {
FIXTURE_CONTENT_LONG_TEXT_ONLY,
FIXTURE_CONTENT_MARKDOWN,
FIXTURE_PARTIAL_BADGE,
FIXTURE_SMALL_TEXT_MARKDOWN_ALERT,
FIXTURE_WARNING_MARKDOWN_ALERT,
} from './WebHoverOverlay.fixtures'
import styles from './WebHoverOverlay.story.module.scss'
@ -148,117 +144,33 @@ export const MultipleMarkupContents: Story = () => (
MultipleMarkupContents.storyName = 'Multiple MarkupContents'
export const WithSmallTextAlert: Story = () => (
export const WithLongMarkdownTextIcon: Story = () => (
<WebHoverOverlay
{...commonProps()}
hoverOrError={{
contents: [FIXTURE_CONTENT],
alerts: [FIXTURE_SMALL_TEXT_MARKDOWN_ALERT],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
/>
)
WithSmallTextAlert.storyName = 'With small-text alert'
export const WithOneLineAlert: Story = () => (
<WebHoverOverlay
{...commonProps()}
hoverOrError={{
contents: [FIXTURE_CONTENT],
alerts: [
{
summary: {
kind: MarkupKind.PlainText,
value: 'This is a test alert.',
},
},
],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
/>
)
WithOneLineAlert.storyName = 'With one-line alert'
export const WithAlertWithWarningIcon: Story = () => (
<WebHoverOverlay
{...commonProps()}
hoverOrError={{
contents: [FIXTURE_CONTENT],
alerts: [
{
summary: {
kind: MarkupKind.PlainText,
value: 'This is a warning alert.',
},
iconKind: 'warning',
},
],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
/>
)
WithAlertWithWarningIcon.storyName = 'With alert with warning icon'
export const WithDismissibleAlertWithIcon: Story = () => (
<WebHoverOverlay
{...commonProps()}
hoverOrError={{
contents: [FIXTURE_CONTENT],
alerts: [
{
summary: {
kind: MarkupKind.Markdown,
value: 'Search based result.<br /> [Learn more about precise code navigation](https://sourcegraph.com/github.com/sourcegraph/code-intel-extensions/-/blob/shared/indicators.ts#L67)',
},
type: 'test-alert-type',
iconKind: 'info',
},
],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
/>
)
WithDismissibleAlertWithIcon.storyName = 'With dismissible alert with icon'
export const WithLongMarkdownTextAndDismissibleAlertWithIcon: Story = () => (
<WebHoverOverlay
{...commonProps()}
hoverOrError={{
contents: [FIXTURE_CONTENT],
alerts: [FIXTURE_WARNING_MARKDOWN_ALERT],
aggregatedBadges: [FIXTURE_PARTIAL_BADGE, FIXTURE_SEMANTIC_BADGE],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
/>
)
WithLongMarkdownTextAndDismissibleAlertWithIcon.storyName = 'With long markdown text and dismissible alert with icon.'
WithLongMarkdownTextIcon.storyName = 'With long markdown text and icon.'
export const MultipleMarkupContentsWithBadgesAndAlerts: Story = () => (
export const MultipleMarkupContentsWithBadges: Story = () => (
<div className={styles.container}>
<WebHoverOverlay
{...commonProps()}
hoverOrError={{
contents: [FIXTURE_CONTENT, FIXTURE_CONTENT, FIXTURE_CONTENT],
aggregatedBadges: [FIXTURE_SEMANTIC_BADGE],
alerts: [FIXTURE_SMALL_TEXT_MARKDOWN_ALERT, FIXTURE_WARNING_MARKDOWN_ALERT],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
/>
</div>
)
MultipleMarkupContentsWithBadgesAndAlerts.storyName = 'Multiple MarkupContents with badges and alerts'
MultipleMarkupContentsWithBadges.storyName = 'Multiple MarkupContents with badges'
export const WithCloseButton: Story = () => (
<WebHoverOverlay
@ -266,10 +178,8 @@ export const WithCloseButton: Story = () => (
hoverOrError={{
contents: [FIXTURE_CONTENT, FIXTURE_CONTENT, FIXTURE_CONTENT],
aggregatedBadges: [FIXTURE_SEMANTIC_BADGE],
alerts: [FIXTURE_SMALL_TEXT_MARKDOWN_ALERT, FIXTURE_WARNING_MARKDOWN_ALERT],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
pinOptions={{ showCloseButton: true }}
/>
)

View File

@ -1,31 +1,21 @@
import React, { useCallback, useEffect } from 'react'
import React, { useEffect } from 'react'
import classNames from 'classnames'
import { Observable } from 'rxjs'
import { isErrorLike } from '@sourcegraph/common'
import { urlForClientCommandOpen } from '@sourcegraph/shared/src/actions/ActionItem'
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
import { HoverOverlay, HoverOverlayProps } from '@sourcegraph/shared/src/hover/HoverOverlay'
import { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import { AlertProps, useLocalStorage } from '@sourcegraph/wildcard'
import { HoverThresholdProps } from '../../repo/RepoContainer'
import styles from './WebHoverOverlay.module.scss'
const iconKindToAlertVariant: Record<number, AlertProps['variant']> = {
[NotificationType.Info]: 'secondary',
[NotificationType.Error]: 'danger',
[NotificationType.Warning]: 'warning',
}
const getAlertVariant: HoverOverlayProps['getAlertVariant'] = iconKind => iconKindToAlertVariant[iconKind]
export interface WebHoverOverlayProps
extends Omit<
HoverOverlayProps,
'className' | 'closeButtonClassName' | 'actionItemClassName' | 'getAlertVariant' | 'actionItemStyleProps'
'className' | 'closeButtonClassName' | 'actionItemClassName' | 'actionItemStyleProps'
>,
HoverThresholdProps,
SettingsCascadeProps {
@ -41,28 +31,7 @@ export interface WebHoverOverlayProps
}
export const WebHoverOverlay: React.FunctionComponent<React.PropsWithChildren<WebHoverOverlayProps>> = props => {
const { onAlertDismissed: outerOnAlertDismissed } = props
const [dismissedAlerts, setDismissedAlerts] = useLocalStorage<string[]>('WebHoverOverlay.dismissedAlerts', [])
const onAlertDismissed = useCallback(
(alertType: string) => {
if (!dismissedAlerts.includes(alertType)) {
setDismissedAlerts([...dismissedAlerts, alertType])
outerOnAlertDismissed?.(alertType)
}
},
[dismissedAlerts, setDismissedAlerts, outerOnAlertDismissed]
)
let propsToUse = props
if (props.hoverOrError && props.hoverOrError !== 'loading' && !isErrorLike(props.hoverOrError)) {
const filteredAlerts = (props.hoverOrError?.alerts || []).filter(
alert => !alert.type || !dismissedAlerts.includes(alert.type)
)
propsToUse = { ...props, hoverOrError: { ...props.hoverOrError, alerts: filteredAlerts } }
}
const { hoverOrError } = propsToUse
const { onHoverShown, hoveredToken } = props
const { hoverOrError, onHoverShown, hoveredToken } = props
/** Whether the hover has actual content (that provides value to the user) */
const hoverHasValue = hoverOrError !== 'loading' && !isErrorLike(hoverOrError) && !!hoverOrError?.contents?.length
@ -75,12 +44,10 @@ export const WebHoverOverlay: React.FunctionComponent<React.PropsWithChildren<We
return (
<HoverOverlay
{...propsToUse}
{...props}
className={classNames(styles.webHoverOverlay, props.hoverOverlayContainerClassName)}
closeButtonClassName={styles.webHoverOverlayCloseButton}
actionItemClassName="border-0"
onAlertDismissed={onAlertDismissed}
getAlertVariant={getAlertVariant}
actionItemStyleProps={{
actionItemSize: 'sm',
actionItemVariant: 'secondary',

View File

@ -130,7 +130,6 @@ export class HovercardView implements TooltipView {
// hovercard to render
overlayPosition={dummyOverlayPosition}
hoveredToken={hoveredToken}
onAlertDismissed={() => repositionTooltips(this.view)}
pinOptions={{
showCloseButton: pinned,
onCloseButtonClick: () => {

View File

@ -1,5 +0,0 @@
declare module 'string-score' {
function score(target: string, query: string, fuzzyFactor?: number): number
export = score
}

View File

@ -49,16 +49,16 @@ func TestMergeSettings(t *testing.T) {
}, {
name: "merge bool",
left: &schema.Settings{
AlertsShowPatchUpdates: false,
CodeHostUseNativeTooltips: true,
AlertsShowPatchUpdates: false,
BasicCodeIntelGlobalSearchesEnabled: true,
},
right: &schema.Settings{
AlertsShowPatchUpdates: true,
CodeHostUseNativeTooltips: false, // This is the zero value, so will not override a previous non-zero value
AlertsShowPatchUpdates: true,
BasicCodeIntelGlobalSearchesEnabled: false, // This is the zero value, so will not override a previous non-zero value
},
expected: &schema.Settings{
AlertsShowPatchUpdates: true,
CodeHostUseNativeTooltips: true,
AlertsShowPatchUpdates: true,
BasicCodeIntelGlobalSearchesEnabled: true,
},
}, {
name: "merge int",

View File

@ -10,7 +10,6 @@ The browser extension also does not store sensitive data locally. The informatio
- AnonymousUid
- Feature flags
- Hover alerts
- Client settings
- Enable/disable status
- Sourcegraph URL

View File

@ -461,8 +461,6 @@
"sanitize-html": "^2.0.0",
"semver": "^7.3.2",
"stream-http": "^3.2.0",
"string-score": "^1.0.1",
"tabbable": "^5.1.5",
"tagged-template-noop": "^2.1.1",
"ts-key-enum": "^2.0.7",
"tslib": "^2.1.0",

View File

@ -358,12 +358,10 @@ importers:
storybook-dark-mode: ^2.0.4
stream-browserify: ^3.0.0
stream-http: ^3.2.0
string-score: ^1.0.1
string-width: ^4.2.0
style-loader: ^3.3.1
stylelint: ^14.3.0
svgo: ^2.7.0
tabbable: ^5.1.5
tagged-template-noop: ^2.1.1
term-size: ^2.2.0
terser-webpack-plugin: ^5.3.6
@ -527,8 +525,6 @@ importers:
sanitize-html: 2.3.3
semver: 7.3.8
stream-http: 3.2.0
string-score: 1.0.1
tabbable: 5.2.1
tagged-template-noop: 2.1.1
ts-key-enum: 2.0.8
tslib: 2.1.0
@ -30869,10 +30865,6 @@ packages:
strip-ansi: 6.0.1
dev: true
/string-score/1.0.1:
resolution: {integrity: sha512-HRm/tlGZR3NCphwEa+D60qvtTCucu5yybc2S9BI9An/Zq01/r8EOyrKD0wOgZebpijR/iydGP4lPgCzzSiX+Ug==}
dev: false
/string-width/1.0.2:
resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==}
engines: {node: '>=0.10.0'}

View File

@ -1989,8 +1989,6 @@ type Settings struct {
BasicCodeIntelIndexOnly bool `json:"basicCodeIntel.indexOnly,omitempty"`
// BasicCodeIntelUnindexedSearchTimeout description: The timeout (in milliseconds) for un-indexed search requests.
BasicCodeIntelUnindexedSearchTimeout float64 `json:"basicCodeIntel.unindexedSearchTimeout,omitempty"`
// CodeHostUseNativeTooltips description: Whether to use the code host's native hover tooltips when they exist (GitHub's jump-to-definition tooltips, for example).
CodeHostUseNativeTooltips bool `json:"codeHost.useNativeTooltips,omitempty"`
// CodeIntelDisableRangeQueries description: Whether to fetch multiple precise definitions and references on hover.
CodeIntelDisableRangeQueries bool `json:"codeIntel.disableRangeQueries,omitempty"`
// CodeIntelDisableSearchBased description: Never fall back to search-based code intelligence.
@ -2094,7 +2092,6 @@ func (v *Settings) UnmarshalJSON(data []byte) error {
delete(m, "basicCodeIntel.includeForks")
delete(m, "basicCodeIntel.indexOnly")
delete(m, "basicCodeIntel.unindexedSearchTimeout")
delete(m, "codeHost.useNativeTooltips")
delete(m, "codeIntel.disableRangeQueries")
delete(m, "codeIntel.disableSearchBased")
delete(m, "codeIntel.mixPreciseAndSearchBasedReferences")

View File

@ -549,11 +549,6 @@
"enum": ["browser-extension", "native-integration"],
"default": "browser-extension"
},
"codeHost.useNativeTooltips": {
"description": "Whether to use the code host's native hover tooltips when they exist (GitHub's jump-to-definition tooltips, for example).",
"type": "boolean",
"default": false
},
"search.hideSuggestions": {
"description": "Disable search suggestions below the search bar when constructing queries. Defaults to false.",
"type": "boolean",

View File

@ -1138,7 +1138,6 @@ Yarn,stable,0.1.8,MIT,"",Approved
Yarn,statuses,1.5.0,MIT,"",Approved
Yarn,store2,2.12.0,(MIT OR GPL-3.0),http://www.esha.com/,Approved
Yarn,stream-http,3.2.0,MIT,https://github.com/jhiesey/stream-http#readme,Approved
Yarn,string-score,1.0.1,MIT,https://github.com/KenPowers/string-score,Approved
Yarn,string-width,4.2.3,MIT,sindresorhus.com,Approved
Yarn,string_decoder,1.1.1,MIT,https://github.com/nodejs/string_decoder,Approved
Yarn,strip-ansi,3.0.1,MIT,sindresorhus.com,Approved
@ -1153,8 +1152,6 @@ Yarn,supports-color,8.1.1,MIT,https://sindresorhus.com,Approved
Yarn,supports-preserve-symlinks-flag,1.0.0,MIT,https://github.com/inspect-js/node-supports-preserve-symlinks-flag#readme,Approved
Yarn,symbol-observable,4.0.0,MIT,"",Approved
Yarn,synchronous-promise,2.0.15,New BSD,https://github.com/fluffynuts,Approved
Yarn,tabbable,4.0.0,MIT,https://github.com/davidtheclark/tabbable#readme,Approved
Yarn,tabbable,5.2.1,MIT,https://github.com/focus-trap/tabbable#readme,Approved
Yarn,tagged-template-noop,2.1.1,MIT,https://github.com/lleaff/tagged-template-noop#readme,Approved
Yarn,tapable,1.1.3,MIT,https://github.com/webpack/tapable,Approved
Yarn,tapable,2.2.0,MIT,https://github.com/webpack/tapable,Approved

1 package_manager name version licenses homepage approved
1138 Yarn statuses 1.5.0 MIT Approved
1139 Yarn store2 2.12.0 (MIT OR GPL-3.0) http://www.esha.com/ Approved
1140 Yarn stream-http 3.2.0 MIT https://github.com/jhiesey/stream-http#readme Approved
Yarn string-score 1.0.1 MIT https://github.com/KenPowers/string-score Approved
1141 Yarn string-width 4.2.3 MIT sindresorhus.com Approved
1142 Yarn string_decoder 1.1.1 MIT https://github.com/nodejs/string_decoder Approved
1143 Yarn strip-ansi 3.0.1 MIT sindresorhus.com Approved
1152 Yarn supports-preserve-symlinks-flag 1.0.0 MIT https://github.com/inspect-js/node-supports-preserve-symlinks-flag#readme Approved
1153 Yarn symbol-observable 4.0.0 MIT Approved
1154 Yarn synchronous-promise 2.0.15 New BSD https://github.com/fluffynuts Approved
Yarn tabbable 4.0.0 MIT https://github.com/davidtheclark/tabbable#readme Approved
Yarn tabbable 5.2.1 MIT https://github.com/focus-trap/tabbable#readme Approved
1155 Yarn tagged-template-noop 2.1.1 MIT https://github.com/lleaff/tagged-template-noop#readme Approved
1156 Yarn tapable 1.1.3 MIT https://github.com/webpack/tapable Approved
1157 Yarn tapable 2.2.0 MIT https://github.com/webpack/tapable Approved