mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 13:31:54 +00:00
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:
parent
46d81a9fa1
commit
ae338b9797
2
client/browser/BUILD.bazel
generated
2
client/browser/BUILD.bazel
generated
@ -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",
|
||||
|
||||
@ -55,7 +55,6 @@ export interface SyncStorageItems extends SourcegraphURL {
|
||||
* Overrides settings from Sourcegraph.
|
||||
*/
|
||||
clientSettings: string
|
||||
dismissedHoverAlerts: Record<string, boolean | undefined>
|
||||
}
|
||||
|
||||
export interface LocalStorageItems {}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()', () => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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) */
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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()', () => {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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]
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -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.',
|
||||
},
|
||||
})
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -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 = {}) => (
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
declare module 'string-score' {
|
||||
function score(target: string, query: string, fuzzyFactor?: number): number
|
||||
|
||||
export = score
|
||||
}
|
||||
@ -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',
|
||||
|
||||
|
||||
@ -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' }],
|
||||
}))
|
||||
})
|
||||
|
||||
@ -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)),
|
||||
}
|
||||
|
||||
2
client/extension-api-types/src/hover.d.ts
vendored
2
client/extension-api-types/src/hover.d.ts
vendored
@ -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
|
||||
}
|
||||
|
||||
155
client/extension-api/src/sourcegraph.d.ts
vendored
155
client/extension-api/src/sourcegraph.d.ts
vendored
@ -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 {
|
||||
|
||||
9
client/shared/BUILD.bazel
generated
9
client/shared/BUILD.bazel
generated
@ -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",
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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' })
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)[]>
|
||||
|
||||
/**
|
||||
|
||||
@ -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' },
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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'>[]>
|
||||
|
||||
@ -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: [] },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@ -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: [],
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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']))
|
||||
})
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
))
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
@ -1 +0,0 @@
|
||||
export * from './EmptyCommandListContainer'
|
||||
@ -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([
|
||||
{
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from './HoverOverlayAlerts'
|
||||
@ -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>
|
||||
|
||||
@ -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`] = `
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
@ -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">×</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)
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
.sourcegraph-notifications {
|
||||
position: fixed;
|
||||
width: 28rem;
|
||||
top: 82px;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
}
|
||||
@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -630,7 +630,6 @@ export const extensionsController: Controller = {
|
||||
haveInitialExtensionsLoaded: () => pretendProxySubscribable(of(true)),
|
||||
})
|
||||
),
|
||||
commandErrors: EMPTY,
|
||||
unsubscribe: noop,
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
declare module 'string-score' {
|
||||
function score(target: string, query: string, fuzzyFactor?: number): number
|
||||
|
||||
export = score
|
||||
}
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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 }}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -130,7 +130,6 @@ export class HovercardView implements TooltipView {
|
||||
// hovercard to render
|
||||
overlayPosition={dummyOverlayPosition}
|
||||
hoveredToken={hoveredToken}
|
||||
onAlertDismissed={() => repositionTooltips(this.view)}
|
||||
pinOptions={{
|
||||
showCloseButton: pinned,
|
||||
onCloseButtonClick: () => {
|
||||
|
||||
5
client/web/src/types/string-score/index.d.ts
vendored
5
client/web/src/types/string-score/index.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
declare module 'string-score' {
|
||||
function score(target: string, query: string, fuzzyFactor?: number): number
|
||||
|
||||
export = score
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user