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

Commit 6d840c8

Browse files
committed
feat(extension): use new extension API handle (CXP + activateExtension)
BREAKING CHANGE: Extensions implemented using this package's `server` module must be ported to use the new `activateExtension` API and the `CXP` API handle type.
1 parent 3855cfd commit 6d840c8

23 files changed

+869
-1854
lines changed

src/client/client.integration.test.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import * as assert from 'assert'
2-
import { MessageTransports } from '../jsonrpc2/connection'
2+
import { createMessageConnection, MessageConnection, MessageTransports } from '../jsonrpc2/connection'
33
import { Trace } from '../jsonrpc2/trace'
44
import { ClientCapabilities, InitializeParams, InitializeRequest, InitializeResult } from '../protocol'
5-
import { Connection as ServerConnection, createConnection as createServerConnection } from '../server/server'
65
import { clientStateIsActive, getClientState } from '../test/helpers'
76
import { createMessageTransports } from '../test/integration/helpers'
87
import { Client, ClientState } from './client'
98

10-
const createClientTransportsForTestServer = (registerServer: (server: ServerConnection) => void): MessageTransports => {
9+
const createClientTransportsForTestServer = (
10+
registerServer: (server: MessageConnection) => void
11+
): MessageTransports => {
1112
const [clientTransports, serverTransports] = createMessageTransports()
12-
const serverConnection = createServerConnection(serverTransports)
13+
const serverConnection = createMessageConnection(serverTransports)
1314
serverConnection.listen()
1415
registerServer(serverConnection)
1516
return clientTransports

src/environment/providers/command.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,5 @@ export function executeCommand(commands: CommandEntry[], params: ExecuteCommandP
5858
if (!command) {
5959
throw new Error(`command not found: ${JSON.stringify(params.command)}`)
6060
}
61-
return command.run(...(params.arguments || []))
61+
return Promise.resolve(command.run(...(params.arguments || [])))
6262
}

src/extension/api.ts

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { Subscription } from 'rxjs'
2+
import { Context } from '../environment/context/context'
3+
import { MessageConnection } from '../jsonrpc2/connection'
4+
import { Settings } from '../protocol'
5+
import { URI } from '../types/textDocument'
6+
7+
/**
8+
* The CXP extension API, which extensions use to interact with the client.
9+
*
10+
* @template C the extension's settings
11+
*/
12+
export interface CXP<C = Settings> {
13+
/**
14+
* The root URI of the workspace in which this extension is running.
15+
*
16+
* TODO(sqs): Remove the strict association with the single initial root (support multi-root, changing roots,
17+
* etc.).
18+
*/
19+
root: URI | null
20+
21+
/**
22+
* The configuration settings from the client.
23+
*/
24+
configuration: Configuration<C>
25+
26+
/**
27+
* The application windows on the client.
28+
*/
29+
windows: Windows
30+
31+
/**
32+
* Command registration and execution.
33+
*/
34+
commands: Commands
35+
36+
/**
37+
* Arbitrary key-value pairs that describe application state in a namespace shared by the client and all other
38+
* extensions used by the client.
39+
*/
40+
context: ExtensionContext
41+
42+
/** The underlying CXP connection to the client. */
43+
readonly rawConnection: MessageConnection
44+
45+
/**
46+
* Immediately stops the extension and closes the connection to the client.
47+
*/
48+
close(): void
49+
}
50+
51+
/**
52+
* A stream of values that can be transformed (with {@link Observable#pipe}) and subscribed to (with
53+
* {@link Observable#subscribe}).
54+
*
55+
* This is a subset of the {@link module:rxjs.Observable} interface, for simplicity and compatibility with future
56+
* Observable standards.
57+
*
58+
* @template T The type of the values emitted by the {@link Observable}.
59+
*/
60+
export interface Observable<T> {
61+
/**
62+
* Registers callbacks that are called each time a certain event occurs in the stream of values.
63+
*
64+
* @param next Called when a new value is emitted in the stream.
65+
* @param error Called when an error occurs (which also causes the observable to be closed).
66+
* @param complete Called when the stream of values ends.
67+
* @return A subscription that frees resources used by the subscription upon unsubscription.
68+
*/
69+
subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription
70+
71+
/**
72+
* Returns the underlying Observable value, for compatibility with other Observable implementations (such as
73+
* RxJS).
74+
*
75+
* @internal
76+
*/
77+
[Symbol.observable]?(): any
78+
}
79+
80+
/**
81+
* Configuration settings for a specific resource (such as a file, directory, or repository) or subject (such as a
82+
* user or organization, depending on the client).
83+
*
84+
* It may be merged from the following sources of settings, in order:
85+
*
86+
* - Default settings
87+
* - Global settings
88+
* - Organization settings (for all organizations the user is a member of)
89+
* - User settings
90+
* - Client settings
91+
* - Repository settings
92+
* - Directory settings
93+
*
94+
* @template C configuration type
95+
*/
96+
export interface Configuration<C> extends Observable<C> {
97+
/**
98+
* Returns a value from the configuration.
99+
*
100+
* @template K Valid keys on the configuration object.
101+
* @param key The name of the configuration property to get.
102+
* @return The configuration value, or undefined.
103+
*/
104+
get<K extends keyof C>(key: K): C[K] | undefined
105+
106+
/**
107+
* Observes changes to the configuration values for the given keys.
108+
*
109+
* @template K Valid keys on the configuration object.
110+
* @param keys The names of the configuration properties to observe.
111+
* @return An observable that emits when any of the keys' values change (using deep comparison).
112+
*/
113+
watch<K extends keyof C>(...keys: K[]): Observable<Pick<C, K>>
114+
115+
/**
116+
* Updates the configuration value for the given key. The updated configuration value is sent to the client for
117+
* persistence.
118+
*
119+
* @template K Valid keys on the configuration object.
120+
* @param key The name of the configuration property to update.
121+
* @param value The new value, or undefined to remove it.
122+
* @return A promise that resolves when the client acknowledges the update.
123+
*/
124+
update<K extends keyof C>(key: K, value: C[K] | undefined): Promise<void>
125+
126+
// TODO: Future plans:
127+
//
128+
// - add a way to read configuration from a specific scope (aka subject, but "scope" is probably a better word)
129+
// - describe how configuration defaults are supported
130+
}
131+
132+
/**
133+
* The application windows on the client.
134+
*/
135+
export interface Windows extends Observable<Window[]> {
136+
/**
137+
* All application windows on the client.
138+
*/
139+
all: Window[]
140+
141+
/**
142+
* The active window, or `null` if there is no active window. The active window is the window that was
143+
* focused most recently.
144+
*/
145+
active: Window | null
146+
147+
/**
148+
* Display a prompt and request text input from the user.
149+
*
150+
* @todo TODO!(sqs): always shows on the active window if any; how to pass this as a param?
151+
*
152+
* @param message The message to show.
153+
* @param defaultValue The default value for the user input, or undefined for no default.
154+
* @returns The user's input, or null if the user (or the client) canceled the input request.
155+
*/
156+
showInputBox(message: string, defaultValue?: string): Promise<string | null>
157+
}
158+
159+
/**
160+
* The application window where the client is running.
161+
*/
162+
export interface Window {
163+
/**
164+
* Whether this window is the active window in the application. At most 1 window can be active.
165+
*/
166+
readonly isActive: boolean
167+
168+
/**
169+
* The active user interface component (such as a text editor) in this window, or null if there is no active
170+
* component.
171+
*/
172+
readonly activeComponent: Component | null
173+
}
174+
175+
/**
176+
* A user interface component in an application window (such as a text editor).
177+
*/
178+
export interface Component {
179+
/**
180+
* Whether this component is the active component in the application. At most 1 component can be active.
181+
*/
182+
readonly isActive: boolean
183+
184+
/**
185+
* The URI of the resource (such as a file) that this component is displaying, or null if there is none.
186+
*/
187+
resource: URI | null
188+
}
189+
190+
/**
191+
* Command registration and execution.
192+
*/
193+
export interface Commands {
194+
/**
195+
* Registers a command with the given identifier. The command can be invoked by this extension's contributions
196+
* (e.g., a contributed action that adds a toolbar item to invoke this command).
197+
*
198+
* @param command The unique identifier for the command.
199+
* @param run The function to invoke for this command.
200+
* @return A subscription that unregisters this command upon unsubscription.
201+
*/
202+
register(command: string, run: (...args: any[]) => any): Subscription
203+
}
204+
205+
/**
206+
* Arbitrary key-value pairs that describe application state in a namespace shared by the client and all other
207+
* extensions used by the client.
208+
*/
209+
export interface ExtensionContext {
210+
/**
211+
* Applies the given updates to the client's context, overwriting any existing values for the same key and
212+
* deleting any keys whose value is `null`.
213+
*
214+
* @param updates New values for context keys (or deletions for keys if the value is `null`).
215+
*/
216+
updateContext(updates: Context): void
217+
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import * as assert from 'assert'
22
import { createConnection as createClientConnection } from '../client/connection'
3-
import { InitializeParams, InitializeRequest, InitializeResult } from '../protocol'
3+
import { InitializedNotification, InitializeParams, InitializeRequest, InitializeResult } from '../protocol'
44
import { createMessageTransports } from '../test/integration/helpers'
5-
import { createConnection as createServerConnection } from './server'
5+
import { activateExtension } from './extension'
66

7-
describe('Connection', () => {
7+
describe('activateExtension', () => {
88
it('initialize request parameters and result', async () => {
99
const [clientTransports, serverTransports] = createMessageTransports()
10-
const serverConnection = createServerConnection(serverTransports)
1110
const clientConnection = createClientConnection(clientTransports)
12-
serverConnection.listen()
1311
clientConnection.listen()
1412

1513
const initParams: InitializeParams = {
@@ -19,14 +17,19 @@ describe('Connection', () => {
1917
workspaceFolders: null,
2018
}
2119
const initResult: InitializeResult = {
22-
capabilities: { contributions: { actions: [{ id: 'c', command: 'c' }] } },
20+
capabilities: {
21+
decorationProvider: true,
22+
textDocumentSync: { openClose: true },
23+
},
2324
}
2425

25-
serverConnection.onRequest(InitializeRequest.type, params => {
26-
assert.deepStrictEqual(params, initParams)
27-
return initResult
28-
})
29-
const result = await clientConnection.sendRequest(InitializeRequest.type, initParams)
26+
const [, result] = await Promise.all([
27+
activateExtension<{}>(serverTransports, () => void 0),
28+
clientConnection.sendRequest(InitializeRequest.type, initParams).then(result => {
29+
clientConnection.sendNotification(InitializedNotification.type, initParams)
30+
return result
31+
}),
32+
])
3033
assert.deepStrictEqual(result, initResult)
3134
})
3235
})

src/extension/extension.ts

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Subscription } from 'rxjs'
2+
import { createMessageConnection, Logger, MessageConnection, MessageTransports } from '../jsonrpc2/connection'
3+
import {
4+
ConfigurationCascade,
5+
InitializedNotification,
6+
InitializeParams,
7+
InitializeRequest,
8+
InitializeResult,
9+
} from '../protocol'
10+
import { URI } from '../types/textDocument'
11+
import { Commands, Configuration, CXP, ExtensionContext, Observable, Window, Windows } from './api'
12+
import { createExtCommands } from './features/commands'
13+
import { createExtConfiguration } from './features/configuration'
14+
import { createExtContext } from './features/context'
15+
import { createExtWindows } from './features/windows'
16+
17+
class ExtensionHandle<C> implements CXP<C> {
18+
public readonly configuration: Configuration<C> & Observable<C>
19+
public readonly windows: Windows & Observable<Window[]>
20+
public readonly commands: Commands
21+
public readonly context: ExtensionContext
22+
23+
private subscription = new Subscription()
24+
25+
constructor(public readonly rawConnection: MessageConnection, public readonly initializeParams: InitializeParams) {
26+
this.subscription.add(this.rawConnection)
27+
28+
this.configuration = createExtConfiguration<C>(
29+
this,
30+
initializeParams.configurationCascade as ConfigurationCascade<C>
31+
)
32+
this.windows = createExtWindows(this)
33+
this.commands = createExtCommands(this)
34+
this.context = createExtContext(this)
35+
}
36+
37+
public get root(): URI | null {
38+
return this.initializeParams.root
39+
}
40+
41+
public close(): void {
42+
this.subscription.unsubscribe()
43+
}
44+
}
45+
46+
const consoleLogger: Logger = {
47+
error(message: string): void {
48+
console.error(message)
49+
},
50+
warn(message: string): void {
51+
console.warn(message)
52+
},
53+
info(message: string): void {
54+
console.info(message)
55+
},
56+
log(message: string): void {
57+
console.log(message)
58+
},
59+
}
60+
61+
/**
62+
* Activates a CXP extension by calling its `run` entrypoint function with the CXP API handle as the first
63+
* argument.
64+
*
65+
* @template C the extension's settings
66+
* @param transports The message reader and writer to use for communication with the client.
67+
* @param run The extension's `run` entrypoint function.
68+
* @return A promise that resolves when the extension's `run` function has been called.
69+
*/
70+
export function activateExtension<C>(
71+
transports: MessageTransports,
72+
run: (cxp: CXP<C>) => void | Promise<void>
73+
): Promise<void> {
74+
const connection = createMessageConnection(transports, consoleLogger)
75+
return new Promise<void>(resolve => {
76+
let initializationParams!: InitializeParams
77+
connection.onRequest(InitializeRequest.type, params => {
78+
initializationParams = params
79+
return {
80+
capabilities: {
81+
textDocumentSync: { openClose: true },
82+
decorationProvider: true,
83+
},
84+
} as InitializeResult
85+
})
86+
connection.onNotification(InitializedNotification.type, () => {
87+
run(new ExtensionHandle<C>(connection, initializationParams))
88+
resolve()
89+
})
90+
91+
connection.listen()
92+
})
93+
}

0 commit comments

Comments
 (0)