Skip to content
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Commit 9d6904e

Browse files
committed
feat: new extension API that only requires extensions to export 1 func
This makes it so that extension JS bundles are expected to merely export a single activate function. They are no longer bundled with the sourcegraph module, and they no longer know how to speak the RPC extension protocol themselves. The "extension host" knows how to invoke their activate function and inject the sourcegraph module (when it is imported). The goal is to make it easier to write extensions. You just need to write them against a TypeScript API. Your extension itself is not responsible anymore for executing itself, communicating with the client, or knowing anything about its JavaScript execution environment. For example, the following is now a valid extension: ``` import * as sourcegraph from 'sourcegraph' export function activate(): void { sourcegraph.registerHoverProvider(['*'], { provideHover: () => ({ contents: { value: ' Hello from an extension!' }, }), }) } ``` When bundled, it will contain ONLY that `activate` function; the `sourcegraph` module is dynamically linked at runtime. This means the extension bundle is significantly smaller, and the extension author only needs to care about adhering to the TypeScript API (not about also bundling their extension with the latest version of the `sourcegraph` package for wire protocol compatibility).
1 parent c3e771a commit 9d6904e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1698
-145
lines changed

package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
"bugs": {
1212
"url": "https://github.com/sourcegraph/sourcegraph-extension-api/issues"
1313
},
14-
"main": "module/index.js",
15-
"module": "module/index.js",
16-
"types": "module/index.d.ts",
14+
"main": "lib/extension/index.js",
15+
"module": "lib/extension/index.js",
16+
"types": "lib/sourcegraph.d.ts",
1717
"files": [
1818
"lib",
1919
"module"
@@ -33,6 +33,7 @@
3333
"watch:typecheck": "tsc -p tsconfig.json -w",
3434
"watch:build:assets": "npm run build:assets && nodemon --watch src/protocol/contribution.ts --exec 'npm run build:assets || exit 1'",
3535
"watch:build:module": "tsc -p tsconfig.dist.json -w",
36+
"watch:build:api": "nodemon --watch src/sourcegraph.d.ts --watch src/extension/index.js --ext ts,js --exec 'npm run build:api'",
3637
"watch:build": "concurrently --kill-others 'npm:watch:build:*'",
3738
"watch:test": "npm run test -- -w",
3839
"watch:cover": "nodemon --watch src/ --ext ts,js,json --watch package.json --exec 'npm run cover || exit 1'"

src/client/client.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BehaviorSubject, Observable, Unsubscribable } from 'rxjs'
2+
import { DocumentSelector } from 'sourcegraph'
23
import { MessageTransports } from '../jsonrpc2/connection'
34
import {
45
GenericNotificationHandler,
@@ -18,7 +19,6 @@ import {
1819
UnregistrationParams,
1920
UnregistrationRequest,
2021
} from '../protocol'
21-
import { DocumentSelector } from '../types/document'
2222
import { isFunction, tryCatchPromise } from '../util'
2323
import { Connection, createConnection } from './connection'
2424
import { CloseAction, DefaultErrorHandler, ErrorAction, ErrorHandler } from './errorHandler'
@@ -213,6 +213,8 @@ export class Client implements Unsubscribable {
213213
connection.onRequest(RegistrationRequest.type, params => this.handleRegistrationRequest(params))
214214
connection.onRequest(UnregistrationRequest.type, params => this.handleUnregistrationRequest(params))
215215

216+
connection.onRequest('ping', () => 'pong')
217+
216218
// Initialize static features.
217219
for (const feature of this.features) {
218220
if (!DynamicFeature.is(feature)) {

src/client/features/hover.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as assert from 'assert'
2+
import { MarkupKind } from 'sourcegraph'
23
import { TextDocumentHoverProviderRegistry } from '../../environment/providers/hover'
34
import { ClientCapabilities } from '../../protocol'
4-
import { MarkupKind } from '../../types/markup'
55
import { Client } from '../client'
66
import { TextDocumentHoverFeature } from './hover'
77

src/client/features/hover.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { from, Observable, Unsubscribable } from 'rxjs'
2+
import { MarkupKind } from 'sourcegraph'
23
import { ProvideTextDocumentHoverSignature } from '../../environment/providers/hover'
34
import { FeatureProviderRegistry } from '../../environment/providers/registry'
45
import {
@@ -7,8 +8,7 @@ import {
78
TextDocumentPositionParams,
89
TextDocumentRegistrationOptions,
910
} from '../../protocol'
10-
import { Hover } from '../../types/hover'
11-
import { MarkupKind } from '../../types/markup'
11+
import { Hover } from '../../protocol/plainTypes'
1212
import { Client } from '../client'
1313
import { ensure, Feature } from './common'
1414

src/client/features/location.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
TextDocumentRegistrationOptions,
1212
TypeDefinitionRequest,
1313
} from '../../protocol'
14-
import { Location } from '../../types/location'
14+
import { Location } from '../../protocol/plainTypes'
1515
import { Client } from '../client'
1616
import { ensure, Feature } from './common'
1717

src/client/features/textDocument.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as assert from 'assert'
22
import { BehaviorSubject, Subject } from 'rxjs'
3+
import { DocumentSelector } from 'sourcegraph'
34
import { createObservableEnvironment, EMPTY_ENVIRONMENT, Environment } from '../../environment/environment'
45
import { NotificationType } from '../../jsonrpc2/messages'
56
import {
@@ -10,7 +11,6 @@ import {
1011
DidOpenTextDocumentParams,
1112
TextDocumentRegistrationOptions,
1213
} from '../../protocol'
13-
import { DocumentSelector } from '../../types/document'
1414
import { TextDocumentItem } from '../../types/textDocument'
1515
import { Client } from '../client'
1616
import {

src/client/features/textDocument.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Observable, Subscription } from 'rxjs'
22
import { bufferCount, filter, map } from 'rxjs/operators'
3+
import { DocumentSelector, TextDocument } from 'sourcegraph'
34
import { MessageType as RPCMessageType, NotificationType } from '../../jsonrpc2/messages'
45
import {
56
ClientCapabilities,
@@ -9,8 +10,7 @@ import {
910
DidOpenTextDocumentParams,
1011
TextDocumentRegistrationOptions,
1112
} from '../../protocol'
12-
import { DocumentSelector } from '../../types/document'
13-
import { match, TextDocument, TextDocumentItem } from '../../types/textDocument'
13+
import { match, TextDocumentItem } from '../../types/textDocument'
1414
import { Client } from '../client'
1515
import { DynamicFeature, ensure, RegistrationData } from './common'
1616

src/environment/environment.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Observable, of } from 'rxjs'
22
import { distinctUntilChanged, map } from 'rxjs/operators'
3+
import { TextDocument } from 'sourcegraph'
34
import { ConfigurationCascade } from '../protocol'
4-
import { Range } from '../types/range'
5-
import { Selection } from '../types/selection'
6-
import { TextDocument, TextDocumentItem } from '../types/textDocument'
5+
import { Range, Selection } from '../protocol/plainTypes'
6+
import { TextDocumentItem } from '../types/textDocument'
77
import { isEqual } from '../util'
88
import { Context, EMPTY_CONTEXT } from './context/context'
99
import { Extension } from './extension'

src/environment/providers/hover.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as assert from 'assert'
22
import { of } from 'rxjs'
33
import { TestScheduler } from 'rxjs/testing'
4-
import { Hover, HoverMerged } from '../../types/hover'
5-
import { MarkupKind } from '../../types/markup'
4+
import { Hover, MarkupKind } from 'sourcegraph'
5+
import { HoverMerged } from '../../types/hover'
66
import { getHover, ProvideTextDocumentHoverSignature } from './hover'
77
import { FIXTURE } from './registry.test'
88

src/environment/providers/hover.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { combineLatest, from, Observable } from 'rxjs'
22
import { map, switchMap } from 'rxjs/operators'
33
import { TextDocumentPositionParams, TextDocumentRegistrationOptions } from '../../protocol'
4-
import { Hover, HoverMerged } from '../../types/hover'
4+
import { Hover } from '../../protocol/plainTypes'
5+
import { HoverMerged } from '../../types/hover'
56
import { FeatureProviderRegistry } from './registry'
67

78
export type ProvideTextDocumentHoverSignature = (params: TextDocumentPositionParams) => Observable<Hover | null>

src/environment/providers/location.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as assert from 'assert'
22
import { of } from 'rxjs'
33
import { TestScheduler } from 'rxjs/testing'
4-
import { Location } from '../../types/location'
4+
import { Location } from '../../protocol/plainTypes'
55
import {
66
getLocation,
77
getLocations,

src/environment/providers/location.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { combineLatest, from, Observable } from 'rxjs'
22
import { map, switchMap } from 'rxjs/operators'
33
import { ReferenceParams, TextDocumentPositionParams, TextDocumentRegistrationOptions } from '../../protocol'
4+
import { Location } from '../../protocol/plainTypes'
45
import { compact, flatten } from '../../util'
5-
import { Location } from '../../types/location'
66
import { FeatureProviderRegistry } from './registry'
77
import { flattenAndCompact } from './util'
88

src/extension/api/provider.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Unsubscribable } from 'sourcegraph'
2+
import * as sourcegraph from 'sourcegraph'
3+
import uuidv4 from 'uuid/v4'
4+
import { MessageConnection } from '../../jsonrpc2/connection'
5+
import { MessageType as RPCMessageType } from '../../jsonrpc2/messages'
6+
import {
7+
HoverRequest,
8+
RegistrationParams,
9+
RegistrationRequest,
10+
TextDocumentPositionParams,
11+
TextDocumentRegistrationOptions,
12+
UnregistrationParams,
13+
UnregistrationRequest,
14+
} from '../../protocol'
15+
import { Position } from '../../types/position'
16+
17+
/**
18+
* All `registerXyzProvider` functions in the Sourcegraph extension API.
19+
*/
20+
interface RegisterProviderFunctions extends Pick<typeof sourcegraph, 'registerHoverProvider'> {}
21+
22+
/**
23+
* Create all `registerXyzProvider` functions in the Sourcegraph extension API.
24+
*/
25+
export function createRegisterProviderFunctions(connection: MessageConnection): RegisterProviderFunctions {
26+
return {
27+
registerHoverProvider: (selector, provider) => {
28+
connection.onRequest(HoverRequest.type, (params: TextDocumentPositionParams) =>
29+
provider.provideHover(
30+
params.textDocument as sourcegraph.TextDocument,
31+
new Position(params.position.character, params.position.character)
32+
)
33+
)
34+
return registerProvider<TextDocumentRegistrationOptions>(connection, HoverRequest.type, {
35+
documentSelector: selector,
36+
extensionID: '', // TODO(sqs): use provider ID
37+
})
38+
},
39+
}
40+
}
41+
42+
/**
43+
* Registers a provider implemented by the extension with the client.
44+
*
45+
* @return An {@link Unsubscribable} that unregisters the provider.
46+
*/
47+
function registerProvider<RO>(
48+
connection: MessageConnection,
49+
type: RPCMessageType,
50+
registerOptions: RO
51+
): Unsubscribable {
52+
const id = uuidv4()
53+
// TODO(sqs): handle errors in sendRequest calls
54+
connection
55+
.sendRequest(RegistrationRequest.type, {
56+
registrations: [{ id, method: type.method, registerOptions }],
57+
} as RegistrationParams)
58+
.catch(err => console.error(err))
59+
return {
60+
unsubscribe: () =>
61+
connection
62+
.sendRequest(UnregistrationRequest.type, {
63+
unregisterations: [{ id, method: type.method }],
64+
} as UnregistrationParams)
65+
.catch(err => console.error(err)),
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import * as assert from 'assert'
2+
import { from } from 'rxjs'
3+
import { filter, map, switchMap, take } from 'rxjs/operators'
4+
import { MarkupKind } from 'sourcegraph'
5+
import * as sourcegraph from 'sourcegraph'
6+
import { Controller } from '../environment/controller'
7+
import { Environment } from '../environment/environment'
8+
import { clientStateIsActive } from '../test/helpers'
9+
import { createMessageTransports } from '../test/integration/helpers'
10+
import { HoverMerged } from '../types/hover'
11+
import { createExtensionHost } from './extensionHost'
12+
13+
const FIXTURE_ENVIRONMENT: Environment<any, any> = {
14+
component: {
15+
document: { uri: 'file:///f', languageId: 'l', version: 1, text: '' },
16+
selections: [],
17+
visibleRanges: [],
18+
},
19+
extensions: [{ id: 'x' }],
20+
configuration: {},
21+
context: {},
22+
}
23+
24+
describe('Extension host (integration)', () => {
25+
interface TestContext {
26+
clientController: Controller<any, any>
27+
extensionHost: typeof sourcegraph
28+
}
29+
30+
const clientIsActive = (clientController: Controller<any, any>): Promise<void> =>
31+
clientController.clientEntries
32+
.pipe(
33+
filter(entries => entries.length > 0),
34+
map(entries => entries[0]),
35+
switchMap(entry => from(clientStateIsActive(entry.client))),
36+
take(1)
37+
)
38+
.toPromise()
39+
40+
const ready = async ({ clientController, extensionHost }: TestContext): Promise<void> => {
41+
await extensionHost.internal.sync()
42+
await clientIsActive(clientController)
43+
}
44+
45+
const create = async (): Promise<TestContext & { ready: Promise<void> }> => {
46+
const [clientTransports, serverTransports] = createMessageTransports()
47+
48+
const clientController = new Controller({
49+
clientOptions: () => ({ createMessageTransports: () => clientTransports }),
50+
})
51+
clientController.setEnvironment(FIXTURE_ENVIRONMENT)
52+
53+
const extensionHost = await createExtensionHost(serverTransports)
54+
return { clientController, extensionHost, ready: ready({ clientController, extensionHost }) }
55+
}
56+
57+
it('registers and unregisters a provider', async () => {
58+
const { clientController, extensionHost, ready } = await create()
59+
60+
// Register the hover provider and call it.
61+
const unsubscribe = extensionHost.registerHoverProvider(['*'], {
62+
provideHover: () => ({ contents: { value: 'a', kind: MarkupKind.PlainText } }),
63+
})
64+
await ready
65+
assert.deepStrictEqual(
66+
await clientController.registries.textDocumentHover
67+
.getHover({
68+
textDocument: { uri: 'file:///f' },
69+
position: { line: 1, character: 2 },
70+
})
71+
.pipe(take(1))
72+
.toPromise(),
73+
{
74+
contents: [{ value: 'a', kind: MarkupKind.PlainText }],
75+
} as HoverMerged
76+
)
77+
78+
// Unregister the hover provider and ensure it's removed.
79+
unsubscribe.unsubscribe()
80+
await ready
81+
assert.deepStrictEqual(
82+
await clientController.registries.textDocumentHover
83+
.getHover({
84+
textDocument: { uri: 'file:///f' },
85+
position: { line: 1, character: 2 },
86+
})
87+
.pipe(take(1))
88+
.toPromise(),
89+
null
90+
)
91+
})
92+
93+
it.skip('supports multiple hover providers', async () => {
94+
const { clientController, extensionHost, ready } = await create()
95+
96+
extensionHost.registerHoverProvider(['*'], {
97+
provideHover: () => ({ contents: { value: 'a', kind: MarkupKind.PlainText } }),
98+
})
99+
extensionHost.registerHoverProvider(['*'], {
100+
provideHover: () => ({ contents: { value: 'b', kind: MarkupKind.PlainText } }),
101+
})
102+
await ready
103+
104+
const hover = await clientController.registries.textDocumentHover
105+
.getHover({
106+
textDocument: { uri: 'file:///f' },
107+
position: { line: 1, character: 2 },
108+
})
109+
.pipe(take(1))
110+
.toPromise()
111+
assert.deepStrictEqual(hover, {
112+
contents: [{ value: 'a', kind: MarkupKind.PlainText }, { value: 'b', kind: MarkupKind.PlainText }],
113+
} as HoverMerged)
114+
})
115+
})

src/extension/extensionHost.test.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as assert from 'assert'
2+
import * as sourcegraph from 'sourcegraph'
3+
import { createConnection as createClientConnection } from '../client/connection'
4+
import {
5+
InitializedNotification,
6+
InitializeParams,
7+
InitializeRequest,
8+
InitializeResult,
9+
RegistrationRequest,
10+
} from '../protocol'
11+
import { createMessageTransports } from '../test/integration/helpers'
12+
import { createExtensionHost } from './extensionHost'
13+
14+
describe('createExtensionHost', () => {
15+
it('initialize request parameters and result', async () => {
16+
const [clientTransports, serverTransports] = createMessageTransports()
17+
const clientConnection = createClientConnection(clientTransports)
18+
clientConnection.listen()
19+
20+
const initParams: InitializeParams = {
21+
capabilities: { decoration: true, experimental: { a: 1 } },
22+
configurationCascade: { merged: {} },
23+
}
24+
const initResult: InitializeResult = {}
25+
26+
clientConnection.onRequest(RegistrationRequest.type, () => void 0)
27+
28+
const [, result] = await Promise.all([
29+
createExtensionHost(serverTransports).then((extensionHost: typeof sourcegraph) => {
30+
assert.ok(extensionHost)
31+
}),
32+
clientConnection.sendRequest(InitializeRequest.type, initParams).then(result => {
33+
clientConnection.sendNotification(InitializedNotification.type, initParams)
34+
return result
35+
}),
36+
])
37+
assert.deepStrictEqual(result, initResult)
38+
})
39+
})

0 commit comments

Comments
 (0)