Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Commit a79ccc5

Browse filesBrowse files
committed
feat: createPanelView API for extension panels with custom Markdown
Extensions can use the new `sourcegraph.app.createPanelView` API to add panels to the UI. These panels can contain Markdown.
1 parent 3c629da commit a79ccc5
Copy full SHA for a79ccc5

12 files changed

+412-6Lines changed: 412 additions & 6 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/client/api/views.ts‎

Copy file name to clipboard
+72Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { combineLatest, ReplaySubject, Subject, Subscription } from 'rxjs'
2+
import { map } from 'rxjs/operators'
3+
import { handleRequests } from '../../common/proxy'
4+
import { ContributableViewContainer } from '../../protocol'
5+
import { Connection } from '../../protocol/jsonrpc2/connection'
6+
import * as plain from '../../protocol/plainTypes'
7+
import { ViewProviderRegistry } from '../providers/view'
8+
import { SubscriptionMap } from './common'
9+
10+
/** @internal */
11+
export interface ClientViewsAPI {
12+
$unregister(id: number): void
13+
$registerPanelViewProvider(id: number, provider: { id: string }): void
14+
$acceptPanelViewUpdate(id: number, params: Partial<plain.PanelView>): void
15+
}
16+
17+
interface PanelViewSubjects {
18+
title: Subject<string>
19+
content: Subject<string>
20+
}
21+
22+
/** @internal */
23+
export class ClientViews implements ClientViewsAPI {
24+
private subscriptions = new Subscription()
25+
private panelViews = new Map<number, Record<keyof plain.PanelView, Subject<string>>>()
26+
private registrations = new SubscriptionMap()
27+
28+
constructor(connection: Connection, private viewRegistry: ViewProviderRegistry) {
29+
this.subscriptions.add(this.registrations)
30+
31+
handleRequests(connection, 'views', this)
32+
}
33+
34+
public $unregister(id: number): void {
35+
this.registrations.remove(id)
36+
}
37+
38+
public $registerPanelViewProvider(id: number, provider: { id: string }): void {
39+
const panelView: PanelViewSubjects = {
40+
title: new ReplaySubject<string>(1),
41+
content: new ReplaySubject<string>(1),
42+
}
43+
this.panelViews.set(id, panelView)
44+
const registryUnsubscribable = this.viewRegistry.registerProvider(
45+
{ ...provider, container: ContributableViewContainer.Panel },
46+
combineLatest(panelView.title, panelView.content).pipe(map(([title, content]) => ({ title, content })))
47+
)
48+
this.registrations.add(id, {
49+
unsubscribe: () => {
50+
registryUnsubscribable.unsubscribe()
51+
this.panelViews.delete(id)
52+
},
53+
})
54+
}
55+
56+
public $acceptPanelViewUpdate(id: number, params: { title?: string; content?: string }): void {
57+
const panelView = this.panelViews.get(id)
58+
if (panelView === undefined) {
59+
throw new Error(`no panel view with ID ${id}`)
60+
}
61+
if (params.title !== undefined) {
62+
panelView.title.next(params.title)
63+
}
64+
if (params.content !== undefined) {
65+
panelView.content.next(params.content)
66+
}
67+
}
68+
69+
public unsubscribe(): void {
70+
this.subscriptions.unsubscribe()
71+
}
72+
}
Collapse file

‎src/client/controller.ts‎

Copy file name to clipboardExpand all lines: src/client/controller.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ClientContext } from './api/context'
2020
import { ClientDocuments } from './api/documents'
2121
import { ClientLanguageFeatures } from './api/languageFeatures'
2222
import { Search } from './api/search'
23+
import { ClientViews } from './api/views'
2324
import { ClientWindows } from './api/windows'
2425
import { applyContextUpdate, EMPTY_CONTEXT } from './context/context'
2526
import { EMPTY_ENVIRONMENT, Environment } from './environment'
@@ -249,6 +250,7 @@ export class Controller<X extends Extension, C extends ConfigurationCascade> imp
249250
})
250251
)
251252
)
253+
subscription.add(new ClientViews(client, this.registries.views))
252254
subscription.add(new ClientCodeEditor(client, this.registries.textDocumentDecoration))
253255
subscription.add(
254256
new ClientDocuments(
Collapse file

‎src/client/providers/registry.ts‎

Copy file name to clipboardExpand all lines: src/client/providers/registry.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BehaviorSubject, Observable, Unsubscribable } from 'rxjs'
22
import { map } from 'rxjs/operators'
33

44
/** A registry entry for a registered provider. */
5-
interface Entry<O, P> {
5+
export interface Entry<O, P> {
66
registrationOptions: O
77
provider: P
88
}
Collapse file

‎src/client/providers/view.test.ts‎

Copy file name to clipboard
+139Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as assert from 'assert'
2+
import { Observable, of, throwError } from 'rxjs'
3+
import { TestScheduler } from 'rxjs/testing'
4+
import { ContributableViewContainer } from '../../protocol'
5+
import * as plain from '../../protocol/plainTypes'
6+
import { Entry } from './registry'
7+
import { getView, getViews, ViewProviderRegistrationOptions } from './view'
8+
9+
const FIXTURE_CONTAINER = ContributableViewContainer.Panel
10+
11+
const FIXTURE_ENTRY_1: Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>> = {
12+
registrationOptions: { container: FIXTURE_CONTAINER, id: '1' },
13+
provider: of<plain.PanelView>({ title: 't1', content: 'c1' }),
14+
}
15+
const FIXTURE_RESULT_1 = { container: FIXTURE_CONTAINER, id: '1', title: 't1', content: 'c1' }
16+
17+
const FIXTURE_ENTRY_2: Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>> = {
18+
registrationOptions: { container: FIXTURE_CONTAINER, id: '2' },
19+
provider: of<plain.PanelView>({ title: 't2', content: 'c2' }),
20+
}
21+
const FIXTURE_RESULT_2 = { container: FIXTURE_CONTAINER, id: '2', title: 't2', content: 'c2' }
22+
23+
const scheduler = () => new TestScheduler((a, b) => assert.deepStrictEqual(a, b))
24+
25+
describe('getView', () => {
26+
describe('0 providers', () => {
27+
it('returns null', () =>
28+
scheduler().run(({ cold, expectObservable }) =>
29+
expectObservable(
30+
getView(
31+
cold<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>('-a-|', { a: [] }),
32+
'1'
33+
)
34+
).toBe('-a-|', {
35+
a: null,
36+
})
37+
))
38+
})
39+
40+
it('returns result from provider', () =>
41+
scheduler().run(({ cold, expectObservable }) =>
42+
expectObservable(
43+
getView(
44+
cold<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>('-a-|', {
45+
a: [FIXTURE_ENTRY_1],
46+
}),
47+
'1'
48+
)
49+
).toBe('-a-|', {
50+
a: FIXTURE_RESULT_1,
51+
})
52+
))
53+
54+
describe('multiple emissions', () => {
55+
it('returns stream of results', () =>
56+
scheduler().run(({ cold, expectObservable }) =>
57+
expectObservable(
58+
getView(
59+
cold<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>('-a-b-|', {
60+
a: [FIXTURE_ENTRY_1],
61+
b: [FIXTURE_ENTRY_1, FIXTURE_ENTRY_2],
62+
}),
63+
'2'
64+
)
65+
).toBe('-a-b-|', {
66+
a: null,
67+
b: FIXTURE_RESULT_2,
68+
})
69+
))
70+
})
71+
})
72+
73+
describe('getViews', () => {
74+
describe('0 providers', () => {
75+
it('returns null', () =>
76+
scheduler().run(({ cold, expectObservable }) =>
77+
expectObservable(
78+
getViews(
79+
cold<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>('-a-|', { a: [] }),
80+
FIXTURE_CONTAINER
81+
)
82+
).toBe('-a-|', {
83+
a: null,
84+
})
85+
))
86+
})
87+
88+
it('returns result from provider', () =>
89+
scheduler().run(({ cold, expectObservable }) =>
90+
expectObservable(
91+
getViews(
92+
cold<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>('-a-|', {
93+
a: [FIXTURE_ENTRY_1],
94+
}),
95+
FIXTURE_CONTAINER
96+
)
97+
).toBe('-a-|', {
98+
a: [FIXTURE_RESULT_1],
99+
})
100+
))
101+
102+
it('continues if provider has error', () =>
103+
scheduler().run(({ cold, expectObservable }) =>
104+
expectObservable(
105+
getViews(
106+
cold<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>('-a-|', {
107+
a: [
108+
{
109+
registrationOptions: { container: FIXTURE_CONTAINER, id: 'err' },
110+
provider: throwError('err'),
111+
},
112+
FIXTURE_ENTRY_1,
113+
],
114+
}),
115+
FIXTURE_CONTAINER
116+
)
117+
).toBe('-a-|', {
118+
a: [FIXTURE_RESULT_1],
119+
})
120+
))
121+
122+
describe('multiple emissions', () => {
123+
it('returns stream of results', () =>
124+
scheduler().run(({ cold, expectObservable }) =>
125+
expectObservable(
126+
getViews(
127+
cold<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>('-a-b-|', {
128+
a: [FIXTURE_ENTRY_1],
129+
b: [FIXTURE_ENTRY_1, FIXTURE_ENTRY_2],
130+
}),
131+
FIXTURE_CONTAINER
132+
)
133+
).toBe('-a-b-|', {
134+
a: [FIXTURE_RESULT_1],
135+
b: [FIXTURE_RESULT_1, FIXTURE_RESULT_2],
136+
})
137+
))
138+
})
139+
})
Collapse file

‎src/client/providers/view.ts‎

Copy file name to clipboard
+95Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { combineLatest, Observable } from 'rxjs'
2+
import { catchError, map, switchMap } from 'rxjs/operators'
3+
import { ContributableViewContainer } from '../../protocol'
4+
import * as plain from '../../protocol/plainTypes'
5+
import { Entry, FeatureProviderRegistry } from './registry'
6+
7+
export interface ViewProviderRegistrationOptions {
8+
id: string
9+
container: ContributableViewContainer
10+
}
11+
12+
export type ProvideViewSignature = Observable<plain.PanelView>
13+
14+
/** Provides views from all extensions. */
15+
export class ViewProviderRegistry extends FeatureProviderRegistry<
16+
ViewProviderRegistrationOptions,
17+
ProvideViewSignature
18+
> {
19+
/**
20+
* Returns an observable that emits the specified view whenever it or the set of registered view providers
21+
* changes. If the provider emits an error, the returned observable also emits an error (and completes).
22+
*/
23+
public getView(id: string): Observable<plain.PanelView | null> {
24+
return getView(this.entries, id)
25+
}
26+
27+
/**
28+
* Returns an observable that emits all views whenever the set of registered view providers or their properties
29+
* change. If any provider emits an error, the error is logged and the provider is omitted from the emission of
30+
* the observable (the observable does not emit the error).
31+
*/
32+
public getViews(
33+
container: ContributableViewContainer
34+
): Observable<(plain.PanelView & ViewProviderRegistrationOptions)[] | null> {
35+
return getViews(this.entries, container)
36+
}
37+
}
38+
39+
/**
40+
* Exported for testing only. Most callers should use {@link ViewProviderRegistry#getView}, which uses the
41+
* registered view providers.
42+
*
43+
* @internal
44+
*/
45+
export function getView(
46+
entries: Observable<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>,
47+
id: string
48+
): Observable<(plain.PanelView & ViewProviderRegistrationOptions) | null> {
49+
return entries.pipe(
50+
map(entries => entries.find(entry => entry.registrationOptions.id === id)),
51+
switchMap(entry => (entry ? addRegistrationOptions(entry) : [null]))
52+
)
53+
}
54+
55+
/**
56+
* Exported for testing only. Most callers should use {@link ViewProviderRegistry#getViews}, which uses the
57+
* registered view providers.
58+
*
59+
* @internal
60+
*/
61+
export function getViews(
62+
entries: Observable<Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>[]>,
63+
container: ContributableViewContainer
64+
): Observable<(plain.PanelView & ViewProviderRegistrationOptions)[] | null> {
65+
return entries.pipe(
66+
switchMap(
67+
entries =>
68+
entries && entries.length > 0
69+
? combineLatest(
70+
entries.filter(e => e.registrationOptions.container === container).map(entry =>
71+
addRegistrationOptions(entry).pipe(
72+
catchError(err => {
73+
console.error(err)
74+
return [null]
75+
})
76+
)
77+
)
78+
).pipe(
79+
map(entries =>
80+
entries.filter(
81+
(result): result is plain.PanelView & ViewProviderRegistrationOptions =>
82+
result !== null
83+
)
84+
)
85+
)
86+
: [null]
87+
)
88+
)
89+
}
90+
91+
function addRegistrationOptions(
92+
entry: Entry<ViewProviderRegistrationOptions, Observable<plain.PanelView>>
93+
): Observable<plain.PanelView & ViewProviderRegistrationOptions> {
94+
return entry.provider.pipe(map(view => ({ ...view, ...entry.registrationOptions })))
95+
}
Collapse file

‎src/client/registries.ts‎

Copy file name to clipboardExpand all lines: src/client/registries.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TextDocumentDecorationProviderRegistry } from './providers/decoration'
88
import { TextDocumentHoverProviderRegistry } from './providers/hover'
99
import { TextDocumentLocationProviderRegistry, TextDocumentReferencesProviderRegistry } from './providers/location'
1010
import { QueryTransformerRegistry } from './providers/queryTransformer'
11+
import { ViewProviderRegistry } from './providers/view'
1112

1213
/**
1314
* Registries is a container for all provider registries.
@@ -27,4 +28,5 @@ export class Registries<X extends Extension, C extends ConfigurationCascade> {
2728
public readonly textDocumentHover = new TextDocumentHoverProviderRegistry()
2829
public readonly textDocumentDecoration = new TextDocumentDecorationProviderRegistry()
2930
public readonly queryTransformer = new QueryTransformerRegistry()
31+
public readonly views = new ViewProviderRegistry()
3032
}
Collapse file

‎src/extension/api/common.ts‎

Copy file name to clipboardExpand all lines: src/extension/api/common.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class ProviderMap<B> {
3838
*/
3939
public get<P extends B>(id: number): P {
4040
const provider = this.map.get(id) as P
41-
if (!provider) {
41+
if (provider === undefined) {
4242
throw new Error(`no provider with ID ${id}`)
4343
}
4444
return provider

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.